diff --git a/.github/.domain/domain_update.py b/.github/.domain/domain_update.py index e9d04b8e0..7eaadb9a1 100644 --- a/.github/.domain/domain_update.py +++ b/.github/.domain/domain_update.py @@ -3,7 +3,7 @@ import os import json from datetime import datetime -from urllib.parse import urlparse, unquote +from urllib.parse import urlparse # External libraries @@ -22,34 +22,6 @@ def get_headers(): return ua.headers.get() -def get_tld(url_str): - try: - parsed = urlparse(unquote(url_str)) - domain = parsed.netloc.lower() - parts = domain.split('.') - return parts[-1] if len(parts) >= 2 else None - - except Exception: - return None - -def get_base_domain(url_str): - try: - parsed = urlparse(url_str) - domain = parsed.netloc.lower() - parts = domain.split('.') - return '.'.join(parts[:-1]) if len(parts) > 2 else parts[0] - - except Exception: - return None - -def get_base_url(url_str): - try: - parsed = urlparse(url_str) - return f"{parsed.scheme}://{parsed.netloc}" - - except Exception: - return None - def log(msg, level='INFO'): levels = { 'INFO': '[ ]', diff --git a/.github/.domain/domains.json b/.github/.domain/domains.json index 52a84666e..499c35d34 100644 --- a/.github/.domain/domains.json +++ b/.github/.domain/domains.json @@ -35,4 +35,4 @@ "old_domain": "cam", "time_change": "2025-12-18 15:25:18" } -} \ No newline at end of file +} diff --git a/.github/.domain/loc-badge.json b/.github/.domain/loc-badge.json deleted file mode 100644 index a863eb406..000000000 --- a/.github/.domain/loc-badge.json +++ /dev/null @@ -1 +0,0 @@ -{"schemaVersion": 1, "label": "Lines of Code", "message": "9110", "color": "green"} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 12f6323f1..1e9cb37c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,10 +109,8 @@ jobs: shell: bash run: | pyinstaller --onefile --hidden-import=pycryptodomex --hidden-import=ua_generator \ - --hidden-import=qbittorrentapi --hidden-import=qbittorrent \ --hidden-import=bs4 --hidden-import=httpx --hidden-import=rich --hidden-import=tqdm \ --hidden-import=m3u8 --hidden-import=psutil --hidden-import=unidecode \ - --hidden-import=python-dotenv --hidden-import=dotenv \ --hidden-import=jsbeautifier --hidden-import=jsbeautifier.core \ --hidden-import=jsbeautifier.javascript --hidden-import=jsbeautifier.javascript.beautifier \ --hidden-import=jsbeautifier.unpackers --hidden-import=jsbeautifier.unpackers.packer \ @@ -127,7 +125,6 @@ jobs: --hidden-import=Cryptodome.Cipher --hidden-import=Cryptodome.Cipher.AES \ --hidden-import=Cryptodome.Util --hidden-import=Cryptodome.Util.Padding \ --hidden-import=Cryptodome.Random \ - --hidden-import=telebot \ --hidden-import=curl_cffi --hidden-import=_cffi_backend \ --collect-all curl_cffi \ --additional-hooks-dir=pyinstaller/hooks \ diff --git a/.gitignore b/.gitignore index c435ea099..cc089b665 100644 --- a/.gitignore +++ b/.gitignore @@ -50,10 +50,9 @@ downloaded_files/ Video note.txt cmd.txt -bot_config.json scripts.json active_requests.json working_proxies.json start.sh .DS_Store -GUI/db.sqlite3 +GUI/db.sqlite3 \ No newline at end of file diff --git a/GUI/searchapp/api/animeunity.py b/GUI/searchapp/api/animeunity.py index fc28036d3..d61dda2e3 100644 --- a/GUI/searchapp/api/animeunity.py +++ b/GUI/searchapp/api/animeunity.py @@ -10,8 +10,9 @@ # External utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Api.Site.animeunity.util.ScrapeSerie import ScrapeSerieAnime +from StreamingCommunity.Util import config_manager +from StreamingCommunity.Api.Template.loader import get_folder_name +from StreamingCommunity.Api.Service.animeunity.util.ScrapeSerie import ScrapeSerieAnime @@ -29,7 +30,7 @@ def _load_config(self): def _get_search_fn(self): """Lazy load the search function.""" if self._search_fn is None: - module = importlib.import_module("StreamingCommunity.Api.Site.animeunity") + module = importlib.import_module(f"StreamingCommunity.Api.{get_folder_name()}.animeunity") self._search_fn = getattr(module, "search") return self._search_fn diff --git a/GUI/searchapp/api/streamingcommunity.py b/GUI/searchapp/api/streamingcommunity.py index f985b6c2f..1dff109f9 100644 --- a/GUI/searchapp/api/streamingcommunity.py +++ b/GUI/searchapp/api/streamingcommunity.py @@ -10,8 +10,9 @@ # External utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Api.Site.streamingcommunity.util.ScrapeSerie import GetSerieInfo +from StreamingCommunity.Util import config_manager +from StreamingCommunity.Api.Template.loader import get_folder_name +from StreamingCommunity.Api.Service.streamingcommunity.util.ScrapeSerie import GetSerieInfo class StreamingCommunityAPI(BaseStreamingAPI): @@ -28,7 +29,7 @@ def _load_config(self): def _get_search_fn(self): """Lazy load the search function.""" if self._search_fn is None: - module = importlib.import_module("StreamingCommunity.Api.Site.streamingcommunity") + module = importlib.import_module(f"StreamingCommunity.Api.{get_folder_name()}.streamingcommunity") self._search_fn = getattr(module, "search") return self._search_fn diff --git a/README.md b/README.md index c94b8a387..9cb5a846f 100644 --- a/README.md +++ b/README.md @@ -129,21 +129,6 @@ downloader.download() See [MP4 example](./Test/Downloads/MP4.py) for complete usage. -🧲 TOR - -Download content via torrent magnet links. - -```python -from StreamingCommunity import TOR_downloader - -client = TOR_downloader() - -client.add_magnet_link("magnet:?xt=urn:btih:example_hash&dn=example_name", save_path=".") -client.start_download() -``` - -See [Torrent example](./Test/Downloads/TOR.py) for complete usage. - 🎞️ DASH ```python @@ -223,21 +208,6 @@ You can change some behaviors by tweaking the configuration file. The configurat #### Additional Options - `add_siteName`: Appends site_name to root path (can be changed with `--add_siteName true/false`) -πŸ”„ QBIT_CONFIG Settings - -```json -{ - "QBIT_CONFIG": { - "host": "192.168.1.51", - "port": "6666", - "user": "admin", - "pass": "adminadmin" - } -} -``` - -To enable qBittorrent integration, follow the setup guide [here](https://github.com/lgallard/qBittorrent-Controller/wiki/How-to-enable-the-qBittorrent-Web-UI). - πŸ“₯ M3U8_DOWNLOAD Settings ```json @@ -440,7 +410,6 @@ python test_run.py --global -s "cars" python test_run.py --category 1 # Search in anime category python test_run.py --category 2 # Search in movies & series python test_run.py --category 3 # Search in series -python test_run.py --category 4 # Search in torrent category ``` ### PyPI Installation Usage @@ -590,78 +559,6 @@ make LOCAL_DIR=/path/to/download run-container The `run-container` command mounts also the `config.json` file, so any change to the configuration file is reflected immediately without having to rebuild the image. -# Telegram Usage - -βš™οΈ Basic Configuration - -The bot was created to replace terminal commands and allow interaction via Telegram. Each download runs within a screen session, enabling multiple downloads to run simultaneously. - -To run the bot in the background, simply start it inside a screen session and then press Ctrl + A, followed by D, to detach from the session without stopping the bot. - -Command Functions: - -πŸ”Ή /start – Starts a new search for a download. This command performs the same operations as manually running the script in the terminal with test_run.py. - -πŸ”Ή /list – Displays the status of active downloads, with options to: - -Stop an incorrect download using /stop . - -View the real-time output of a download using /screen . - -⚠ Warning: If a download is interrupted, incomplete files may remain in the folder specified in config.json. These files must be deleted manually to avoid storage or management issues. - -πŸ›  Configuration: Currently, the bot's settings are stored in the config.json file, which is located in the same directory as the telegram_bot.py script. - -## .env Example: - -You need to create an .env file and enter your Telegram token and user ID to authorize only one user to use it - -``` -TOKEN_TELEGRAM=IlTuo2131TOKEN$12D3Telegram -AUTHORIZED_USER_ID=12345678 -DEBUG=False -``` - -πŸ“₯ Dependencies & Launch - -Install dependencies: -```bash -pip install -r requirements.txt -``` - -Start the bot (from /StreamingCommunity/TelegramHelp): -```bash -python3 telegram_bot.py -```d -- πŸ”Ή `/list` – Displays the status of active downloads, with options to: - - Stop an incorrect download using `/stop ` - - View the real-time output of a download using `/screen ` - -⚠️ **Warning:** If a download is interrupted, incomplete files may remain in the folder specified in config.json. These files must be deleted manually. - -#### Setup -1. Create an `.env` file with your Telegram token and user ID: -```env -TOKEN_TELEGRAM=IlTuo2131TOKEN$12D3Telegram -AUTHORIZED_USER_ID=12345678 -DEBUG=False -``` - -2. Install dependencies: -```bash -pip install -r requirements.txt -``` - -3. Start the bot (from `/StreamingCommunity/TelegramHelp`): -```bash -python3 telegram_bot.py -``` - -**Running in background:** -Start the bot inside a screen session and press Ctrl + A, followed by D, to detach from the session without stopping the bot. - ---- - # Tutorials - [Windows](https://www.youtube.com/watch?v=mZGqK4wdN-k) diff --git a/StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py b/StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py deleted file mode 100644 index bd9e000e3..000000000 --- a/StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py +++ /dev/null @@ -1,143 +0,0 @@ -# 26.11.24 -# !!! DIO CANErino - -import re - - -class JavaScriptParser: - @staticmethod - def fix_string(ss): - if ss is None: - return None - - ss = str(ss) - ss = ss.encode('utf-8').decode('unicode-escape') - ss = ss.strip("\"'") - ss = ss.strip() - - return ss - - @staticmethod - def fix_url(url): - if url is None: - return None - - url = url.replace('\\/', '/') - return url - - @staticmethod - def parse_value(value): - value = JavaScriptParser.fix_string(value) - - if 'http' in str(value) or 'https' in str(value): - return JavaScriptParser.fix_url(value) - - if value is None or str(value).lower() == 'null': - return None - if str(value).lower() == 'true': - return True - if str(value).lower() == 'false': - return False - - try: - return int(value) - except ValueError: - try: - return float(value) - except ValueError: - pass - - return value - - @staticmethod - def parse_object(obj_str): - obj_str = obj_str.strip('{}').strip() - - result = {} - key_value_pairs = re.findall(r'([\'"]?[\w]+[\'"]?)\s*:\s*([^,{}]+|{[^}]*}|\[[^\]]*\]|\'[^\']*\'|"[^"]*")', obj_str) - - for key, value in key_value_pairs: - key = JavaScriptParser.fix_string(key) - value = value.strip() - - if value.startswith('{'): - result[key] = JavaScriptParser.parse_object(value) - elif value.startswith('['): - result[key] = JavaScriptParser.parse_array(value) - else: - result[key] = JavaScriptParser.parse_value(value) - - return result - - @staticmethod - def parse_array(arr_str): - arr_str = arr_str.strip('[]').strip() - result = [] - - elements = [] - current_elem = "" - brace_count = 0 - in_string = False - quote_type = None - - for char in arr_str: - if char in ['"', "'"]: - if not in_string: - in_string = True - quote_type = char - elif quote_type == char: - in_string = False - quote_type = None - - if not in_string: - if char == '{': - brace_count += 1 - elif char == '}': - brace_count -= 1 - elif char == ',' and brace_count == 0: - elements.append(current_elem.strip()) - current_elem = "" - continue - - current_elem += char - - if current_elem.strip(): - elements.append(current_elem.strip()) - - for elem in elements: - elem = elem.strip() - - if elem.startswith('{'): - result.append(JavaScriptParser.parse_object(elem)) - elif 'active' in elem or 'url' in elem: - key_value_match = re.search(r'([\w]+)\":([^,}]+)', elem) - - if key_value_match: - key = key_value_match.group(1) - value = key_value_match.group(2) - result[-1][key] = JavaScriptParser.parse_value(value.strip('"\\')) - else: - result.append(JavaScriptParser.parse_value(elem)) - - return result - - @classmethod - def parse(cls, js_string): - assignments = re.findall(r'window\.(\w+)\s*=\s*([^;]+);?', js_string, re.DOTALL) - result = {} - - for var_name, value in assignments: - value = value.strip() - - if value.startswith('{'): - result[var_name] = cls.parse_object(value) - elif value.startswith('['): - result[var_name] = cls.parse_array(value) - else: - result[var_name] = cls.parse_value(value) - - can_play_fhd_match = re.search(r'window\.canPlayFHD\s*=\s*(\w+);?', js_string) - if can_play_fhd_match: - result['canPlayFHD'] = cls.parse_value(can_play_fhd_match.group(1)) - - return result diff --git a/StreamingCommunity/Api/Player/hdplayer.py b/StreamingCommunity/Api/Player/hdplayer.py deleted file mode 100644 index 870f659ea..000000000 --- a/StreamingCommunity/Api/Player/hdplayer.py +++ /dev/null @@ -1,61 +0,0 @@ -# 29.04.25 - -import re - -# External library -from bs4 import BeautifulSoup - - -# Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client - - - -class VideoSource: - def __init__(self): - self.client = create_client(headers={'user-agent': get_userAgent()}) - - def extractLinkHdPlayer(self, response): - """Extract iframe source from the page.""" - soup = BeautifulSoup(response.content, 'html.parser') - iframes = soup.find_all("iframe") - if iframes: - return iframes[0].get('data-lazy-src') - return None - - def get_m3u8_url(self, page_url): - """ - Extract m3u8 URL from hdPlayer page. - """ - try: - base_domain = re.match(r'https?://(?:www\.)?([^/]+)', page_url).group(0) - self.client.headers.update({'referer': base_domain}) - - # Get the page content - response = self.client.get(page_url) - - # Extract HDPlayer iframe URL - iframe_url = self.extractLinkHdPlayer(response) - if not iframe_url: - return None - - # Get HDPlayer page content - response_hdplayer = self.client.get(iframe_url) - if response_hdplayer.status_code != 200: - return None - - sources_pattern = r'file:"([^"]+)"' - match = re.search(sources_pattern, response_hdplayer.text) - - if match: - return match.group(1) - - return None - - except Exception as e: - print(f"Error in HDPlayer: {str(e)}") - return None - - finally: - self.client.close() diff --git a/StreamingCommunity/Api/Player/mediapolisvod.py b/StreamingCommunity/Api/Player/mediapolisvod.py index 3ecc03ed8..9447dbe04 100644 --- a/StreamingCommunity/Api/Player/mediapolisvod.py +++ b/StreamingCommunity/Api/Player/mediapolisvod.py @@ -2,8 +2,7 @@ # Internal utilities -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.headers import get_headers +from StreamingCommunity.Util.http_client import create_client, get_headers class VideoSource: diff --git a/StreamingCommunity/Api/Player/supervideo.py b/StreamingCommunity/Api/Player/supervideo.py index e92624790..dc1320cd4 100644 --- a/StreamingCommunity/Api/Player/supervideo.py +++ b/StreamingCommunity/Api/Player/supervideo.py @@ -10,8 +10,7 @@ # Internal utilities -from StreamingCommunity.Util.http_client import create_client_curl -from StreamingCommunity.Util.headers import get_headers +from StreamingCommunity.Util.http_client import create_client_curl, get_headers class VideoSource: @@ -38,7 +37,7 @@ def make_request(self, url: str) -> str: try: response = create_client_curl(headers=self.headers).get(url) if response.status_code >= 400: - logging.error(f"Request failed with status code: {response.status_code}") + logging.error(f"Request failed with status code: {response.status_code}, to url: {url}") return None return response.text diff --git a/StreamingCommunity/Api/Player/sweetpixel.py b/StreamingCommunity/Api/Player/sweetpixel.py index a28ff30e4..9594f89e9 100644 --- a/StreamingCommunity/Api/Player/sweetpixel.py +++ b/StreamingCommunity/Api/Player/sweetpixel.py @@ -4,8 +4,7 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent class VideoSource: diff --git a/StreamingCommunity/Api/Player/vixcloud.py b/StreamingCommunity/Api/Player/vixcloud.py index c56665e35..da85a598d 100644 --- a/StreamingCommunity/Api/Player/vixcloud.py +++ b/StreamingCommunity/Api/Player/vixcloud.py @@ -1,8 +1,10 @@ # 01.03.24 +import re import time import logging from urllib.parse import urlparse, parse_qs, urlencode, urlunparse +from typing import Dict, Any # External libraries @@ -11,16 +13,185 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client -from .Helper.Vixcloud.util import WindowVideo, WindowParameter, StreamsCollection -from .Helper.Vixcloud.js_parser import JavaScriptParser +from StreamingCommunity.Util.http_client import create_client, get_userAgent # Variable console = Console() +class WindowVideo: + def __init__(self, data: Dict[str, Any]): + self.data = data + self.id: int = data.get('id', '') + self.name: str = data.get('name', '') + self.filename: str = data.get('filename', '') + self.size: str = data.get('size', '') + self.quality: str = data.get('quality', '') + self.duration: str = data.get('duration', '') + self.views: int = data.get('views', '') + self.is_viewable: bool = data.get('is_viewable', '') + self.status: str = data.get('status', '') + self.fps: float = data.get('fps', '') + self.legacy: bool = data.get('legacy', '') + self.folder_id: int = data.get('folder_id', '') + self.created_at_diff: str = data.get('created_at_diff', '') + + def __str__(self): + return f"WindowVideo(id={self.id}, name='{self.name}', filename='{self.filename}', size='{self.size}', quality='{self.quality}', duration='{self.duration}', views={self.views}, is_viewable={self.is_viewable}, status='{self.status}', fps={self.fps}, legacy={self.legacy}, folder_id={self.folder_id}, created_at_diff='{self.created_at_diff}')" + + +class WindowParameter: + def __init__(self, data: Dict[str, Any]): + self.data = data + params = data.get('params', {}) + self.token: str = params.get('token', '') + self.expires: str = str(params.get('expires', '')) + self.url = data.get('url') + + def __str__(self): + return (f"WindowParameter(token='{self.token}', expires='{self.expires}', url='{self.url}', data={self.data})") + + +class JavaScriptParser: + @staticmethod + def fix_string(ss): + if ss is None: + return None + + ss = str(ss) + ss = ss.encode('utf-8').decode('unicode-escape') + ss = ss.strip("\"'") + ss = ss.strip() + + return ss + + @staticmethod + def fix_url(url): + if url is None: + return None + + url = url.replace('\\/', '/') + return url + + @staticmethod + def parse_value(value): + value = JavaScriptParser.fix_string(value) + + if 'http' in str(value) or 'https' in str(value): + return JavaScriptParser.fix_url(value) + + if value is None or str(value).lower() == 'null': + return None + if str(value).lower() == 'true': + return True + if str(value).lower() == 'false': + return False + + try: + return int(value) + except ValueError: + try: + return float(value) + except ValueError: + pass + + return value + + @staticmethod + def parse_object(obj_str): + obj_str = obj_str.strip('{}').strip() + + result = {} + key_value_pairs = re.findall(r'([\'"]?[\w]+[\'"]?)\s*:\s*([^,{}]+|{[^}]*}|\[[^\]]*\]|\'[^\']*\'|"[^"]*")', obj_str) + + for key, value in key_value_pairs: + key = JavaScriptParser.fix_string(key) + value = value.strip() + + if value.startswith('{'): + result[key] = JavaScriptParser.parse_object(value) + elif value.startswith('['): + result[key] = JavaScriptParser.parse_array(value) + else: + result[key] = JavaScriptParser.parse_value(value) + + return result + + @staticmethod + def parse_array(arr_str): + arr_str = arr_str.strip('[]').strip() + result = [] + + elements = [] + current_elem = "" + brace_count = 0 + in_string = False + quote_type = None + + for char in arr_str: + if char in ['"', "'"]: + if not in_string: + in_string = True + quote_type = char + elif quote_type == char: + in_string = False + quote_type = None + + if not in_string: + if char == '{': + brace_count += 1 + elif char == '}': + brace_count -= 1 + elif char == ',' and brace_count == 0: + elements.append(current_elem.strip()) + current_elem = "" + continue + + current_elem += char + + if current_elem.strip(): + elements.append(current_elem.strip()) + + for elem in elements: + elem = elem.strip() + + if elem.startswith('{'): + result.append(JavaScriptParser.parse_object(elem)) + elif 'active' in elem or 'url' in elem: + key_value_match = re.search(r'([\w]+)\":([^,}]+)', elem) + + if key_value_match: + key = key_value_match.group(1) + value = key_value_match.group(2) + result[-1][key] = JavaScriptParser.parse_value(value.strip('"\\')) + else: + result.append(JavaScriptParser.parse_value(elem)) + + return result + + @classmethod + def parse(cls, js_string): + assignments = re.findall(r'window\.(\w+)\s*=\s*([^;]+);?', js_string, re.DOTALL) + result = {} + + for var_name, value in assignments: + value = value.strip() + + if value.startswith('{'): + result[var_name] = cls.parse_object(value) + elif value.startswith('['): + result[var_name] = cls.parse_array(value) + else: + result[var_name] = cls.parse_value(value) + + can_play_fhd_match = re.search(r'window\.canPlayFHD\s*=\s*(\w+);?', js_string) + if can_play_fhd_match: + result['canPlayFHD'] = cls.parse_value(can_play_fhd_match.group(1)) + + return result + + class VideoSource: def __init__(self, url: str, is_series: bool, media_id: int = None): """ @@ -78,7 +249,6 @@ def parse_script(self, script_text: str) -> None: # Create window video, streams and parameter objects self.canPlayFHD = bool(converter.get('canPlayFHD')) self.window_video = WindowVideo(converter.get('video')) - self.window_streams = StreamsCollection(converter.get('streams')) self.window_parameter = WindowParameter(converter.get('masterPlaylist')) time.sleep(0.5) diff --git a/StreamingCommunity/Api/Service/altadefinizione/__init__.py b/StreamingCommunity/Api/Service/altadefinizione/__init__.py new file mode 100644 index 000000000..b1c401946 --- /dev/null +++ b/StreamingCommunity/Api/Service/altadefinizione/__init__.py @@ -0,0 +1,104 @@ +# 16.03.25 + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, table_show_manager, media_search_manager +from .film import download_film +from .series import download_series + + +# Variable +indice = 2 +_useFor = "Film_&_Serie" +_deprecate = False + +msg = Prompt() +console = Console() + + +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} + + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + console.print("[yellow]No title selected or selection cancelled.") + return False + + 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) + media_search_manager.clear() + table_show_manager.clear() + return True + + else: + download_film(select_title) + table_show_manager.clear() + return True + + +# 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. + + 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) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Handle empty input + if not actual_search_query: + return False + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/altadefinizione/film.py b/StreamingCommunity/Api/Service/altadefinizione/film.py similarity index 63% rename from StreamingCommunity/Api/Site/altadefinizione/film.py rename to StreamingCommunity/Api/Service/altadefinizione/film.py index ad52671fd..3cfb72fcb 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/film.py +++ b/StreamingCommunity/Api/Service/altadefinizione/film.py @@ -10,21 +10,13 @@ # Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession +from StreamingCommunity.Util import os_manager, start_message, config_manager +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - - -# Player -from StreamingCommunity import HLS_Downloader +# Logic from StreamingCommunity.Api.Player.supervideo import VideoSource @@ -44,7 +36,7 @@ def download_film(select_title: MediaItem) -> str: - str: output path if successful, otherwise None """ start_message() - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{select_title.name}[/cyan] \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") # Extract mostraguarda URL try: @@ -56,7 +48,7 @@ def download_film(select_title: MediaItem) -> str: mostraguarda = iframes[0]['src'] except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get mostraguarda") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request error: {e}, get mostraguarda") return None # Extract supervideo URL @@ -71,8 +63,8 @@ def download_film(select_title: MediaItem) -> str: supervideo_url = 'https:' + supervideo_match.group(0) except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get supervideo URL") - console.print("[yellow]This content will be available soon![/yellow]") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request error: {e}, get supervideo URL") + console.print("[yellow]This content will be available soon!") return None # Init class @@ -81,7 +73,7 @@ def download_film(select_title: MediaItem) -> str: # Define the filename and path for the downloaded film title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output - mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, title_name.replace(extension_output, "")) # Download the film using the m3u8 playlist, and output filename hls_process = HLS_Downloader( @@ -89,13 +81,6 @@ def download_film(select_title: MediaItem) -> str: output_path=os.path.join(mp4_path, title_name) ).start() - if site_constant.TELEGRAM_BOT: - - # Delete script_id - script_id = TelegramSession.get_session() - if script_id != "unknown": - TelegramSession.deleteScriptId(script_id) - if hls_process['error'] is not None: try: os.remove(hls_process['path']) diff --git a/StreamingCommunity/Api/Site/altadefinizione/series.py b/StreamingCommunity/Api/Service/altadefinizione/series.py similarity index 64% rename from StreamingCommunity/Api/Site/altadefinizione/series.py rename to StreamingCommunity/Api/Service/altadefinizione/series.py index 389a6e764..d7971a020 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/series.py +++ b/StreamingCommunity/Api/Service/altadefinizione/series.py @@ -10,14 +10,10 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance, TelegramSession - - -# Logic class -from .util.ScrapeSerie import GetSerieInfo -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util.message import start_message, config_manager +from StreamingCommunity.Lib.HLS import HLS_Downloader +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -25,12 +21,10 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem -# Player -from StreamingCommunity import HLS_Downloader +# Logic +from .util.ScrapeSerie import GetSerieInfo from StreamingCommunity.Api.Player.supervideo import VideoSource @@ -57,26 +51,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [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() - - # Invio a telegram - bot.send_message( - f"Download in corso\nSerie: {scrape_serie.series_name}\nStagione: {index_season_selected}\nEpisodio: {index_episode_selected}\nTitolo: {obj_episode.name}", - None - ) - - # Get script_id and update it - script_id = TelegramSession.get_session() - if script_id != "unknown": - TelegramSession.updateScriptId(script_id, f"{scrape_serie.series_name} - S{index_season_selected} - E{index_episode_selected} - {obj_episode.name}") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.name} ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.{extension_output}" - mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") + mp4_path = os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") # Retrieve scws and if available master playlist video_source = VideoSource(obj_episode.url) @@ -155,30 +134,10 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis """ scrape_serie = GetSerieInfo(select_season.url) seasons_count = scrape_serie.getNumberSeason() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() # 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) - - 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 = display_seasons_list(scrape_serie.seasons_manager) + index_season_selected = display_seasons_list(scrape_serie.seasons_manager) else: index_season_selected = season_selection console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}") @@ -192,12 +151,4 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis if len(list_season_select) > 1 or index_season_selected == "*": download_episode(i_season, scrape_serie, download_all=True) else: - download_episode(i_season, scrape_serie, download_all=False, episode_selection=episode_selection) - - if site_constant.TELEGRAM_BOT: - bot.send_message("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 + download_episode(i_season, scrape_serie, download_all=False, episode_selection=episode_selection) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/altadefinizione/site.py b/StreamingCommunity/Api/Service/altadefinizione/site.py similarity index 61% rename from StreamingCommunity/Api/Site/altadefinizione/site.py rename to StreamingCommunity/Api/Service/altadefinizione/site.py index fe2dacdfa..073f3f757 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/site.py +++ b/StreamingCommunity/Api/Service/altadefinizione/site.py @@ -7,15 +7,9 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Api.Template import site_constants, MediaManager from StreamingCommunity.Util.table import TVShowManager -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager # Variable @@ -34,28 +28,19 @@ def title_search(query: str) -> int: Returns: int: The number of titles found. """ - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - media_search_manager.clear() table_show_manager.clear() - search_url = f"{site_constant.FULL_URL}/?story={query}&do=search&subaction=search" + search_url = f"{site_constants.FULL_URL}/?story={query}&do=search&subaction=search" console.print(f"[cyan]Search url: [yellow]{search_url}") try: response = create_client(headers={'user-agent': get_userAgent()}).get(search_url) response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") - if site_constant.TELEGRAM_BOT: - bot.send_message(f"ERRORE\n\nErrore nella richiesta di ricerca:\n\n{e}", None) + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 - # Prepara le scelte per l'utente - if site_constant.TELEGRAM_BOT: - choices = [] - # Create soup instance soup = BeautifulSoup(response.text, "html.parser") @@ -74,7 +59,7 @@ def title_search(query: str) -> int: if img_tag: img_src = img_tag.get("src") if img_src and img_src.startswith("/"): - image_url = f"{site_constant.FULL_URL}{img_src}" + image_url = f"{site_constants.FULL_URL}{img_src}" else: image_url = img_src @@ -89,13 +74,5 @@ def title_search(query: str) -> int: } media_search_manager.add_media(media_dict) - if site_constant.TELEGRAM_BOT: - choice_text = f"{i} - {title} ({tipo})" - choices.append(choice_text) - - if site_constant.TELEGRAM_BOT: - if choices: - bot.send_message("Lista dei risultati:", choices) - # Return the number of titles found return media_search_manager.get_length() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/altadefinizione/util/ScrapeSerie.py similarity index 95% rename from StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/altadefinizione/util/ScrapeSerie.py index 585a9a950..55f9da968 100644 --- a/StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/altadefinizione/util/ScrapeSerie.py @@ -8,9 +8,8 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Api.Template.object import SeasonManager class GetSerieInfo: diff --git a/StreamingCommunity/Api/Service/animeunity/__init__.py b/StreamingCommunity/Api/Service/animeunity/__init__.py new file mode 100644 index 000000000..1485faa56 --- /dev/null +++ b/StreamingCommunity/Api/Service/animeunity/__init__.py @@ -0,0 +1,98 @@ +# 21.05.24 + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, media_search_manager, table_show_manager +from .film import download_film +from .serie import download_series + + +# Variable +indice = 1 +_useFor = "Anime" +_deprecate = False + +msg = Prompt() +console = Console() + + +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} + + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + console.print("[yellow]No title selected or selection cancelled.") + return False + + if select_title.type == 'Movie': + download_film(select_title) + return True + + 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) + media_search_manager.clear() + table_show_manager.clear() + return True + + +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) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/film.py b/StreamingCommunity/Api/Service/animeunity/film.py similarity index 71% rename from StreamingCommunity/Api/Site/animeunity/film.py rename to StreamingCommunity/Api/Service/animeunity/film.py index e91077590..f89d94aaf 100644 --- a/StreamingCommunity/Api/Site/animeunity/film.py +++ b/StreamingCommunity/Api/Service/animeunity/film.py @@ -4,17 +4,18 @@ 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 +# Internal utilities +from StreamingCommunity.Api.Template import site_constants +from StreamingCommunity.Api.Template.object import MediaItem -# Player +# Logic +from .serie import download_episode +from .util.ScrapeSerie import ScrapeSerieAnime from StreamingCommunity.Api.Player.vixcloud import VideoSourceAnime + # Variable console = Console() @@ -29,8 +30,8 @@ def download_film(select_title: MediaItem): """ # Init class - scrape_serie = ScrapeSerieAnime(site_constant.FULL_URL) - video_source = VideoSourceAnime(site_constant.FULL_URL) + scrape_serie = ScrapeSerieAnime(site_constants.FULL_URL) + video_source = VideoSourceAnime(site_constants.FULL_URL) # Set up video source (only configure scrape_serie now) scrape_serie.setup(None, select_title.id, select_title.slug) diff --git a/StreamingCommunity/Api/Service/animeunity/serie.py b/StreamingCommunity/Api/Service/animeunity/serie.py new file mode 100644 index 000000000..2ae24d70e --- /dev/null +++ b/StreamingCommunity/Api/Service/animeunity/serie.py @@ -0,0 +1,111 @@ +# 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 import os_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import manage_selection, dynamic_format_number +from StreamingCommunity.Lib.MP4 import MP4_Downloader + + +# Logis +from .util.ScrapeSerie import ScrapeSerieAnime +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"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} ([cyan]E{obj_episode.number}) \n") + + # 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_constants.ANIME_FOLDER, scrape_serie.series_name)) + else: + mp4_path = os_manager.get_sanitize_path(os.path.join(site_constants.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() + scrape_serie = ScrapeSerieAnime(site_constants.FULL_URL) + video_source = VideoSourceAnime(site_constants.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"\n[green]Episodes count: [red]{episoded_count}") + + # 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) + + # 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) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/site.py b/StreamingCommunity/Api/Service/animeunity/site.py similarity index 67% rename from StreamingCommunity/Api/Site/animeunity/site.py rename to StreamingCommunity/Api/Service/animeunity/site.py index 739f42ead..0d9c0d744 100644 --- a/StreamingCommunity/Api/Site/animeunity/site.py +++ b/StreamingCommunity/Api/Service/animeunity/site.py @@ -8,15 +8,9 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client_curl +from StreamingCommunity.Util.http_client import create_client_curl, get_userAgent from StreamingCommunity.Util.table import TVShowManager -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager +from StreamingCommunity.Api.Template import site_constants, MediaManager # Variable @@ -29,7 +23,7 @@ def get_token(user_agent: str) -> dict: """ Retrieve session cookies from the site. """ - response = create_client_curl(headers={'user-agent': user_agent}).get(site_constant.FULL_URL) + response = create_client_curl(headers={'user-agent': user_agent}).get(site_constants.FULL_URL) response.raise_for_status() all_cookies = {name: value for name, value in response.cookies.items()} @@ -52,14 +46,9 @@ def title_search(query: str) -> int: """ Perform anime search on animeunity.so. """ - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - media_search_manager.clear() table_show_manager.clear() seen_titles = set() - choices = [] if site_constant.TELEGRAM_BOT else None - user_agent = get_userAgent() data = get_token(user_agent) @@ -69,20 +58,20 @@ def title_search(query: str) -> int: } headers = { - 'origin': site_constant.FULL_URL, - 'referer': f"{site_constant.FULL_URL}/", + 'origin': site_constants.FULL_URL, + 'referer': f"{site_constants.FULL_URL}/", 'user-agent': user_agent, 'x-xsrf-token': data.get('XSRF-TOKEN', ''), } # First call: /livesearch try: - response1 = create_client_curl(headers=headers).post(f'{site_constant.FULL_URL}/livesearch', cookies=cookies, data={'title': query}) + response1 = create_client_curl(headers=headers).post(f'{site_constants.FULL_URL}/livesearch', cookies=cookies, data={'title': query}) response1.raise_for_status() - process_results(response1.json().get('records', []), seen_titles, media_search_manager, choices) + process_results(response1.json().get('records', []), seen_titles, media_search_manager) except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 # Second call: /archivio/get-animes @@ -98,26 +87,20 @@ def title_search(query: str) -> int: 'dubbed': False, 'season': False, } - response2 = create_client_curl(headers=headers).post(f'{site_constant.FULL_URL}/archivio/get-animes', cookies=cookies, json=json_data) + response2 = create_client_curl(headers=headers).post(f'{site_constants.FULL_URL}/archivio/get-animes', cookies=cookies, json=json_data) response2.raise_for_status() - process_results(response2.json().get('records', []), seen_titles, media_search_manager, choices) + process_results(response2.json().get('records', []), seen_titles, media_search_manager) except Exception as e: - console.print(f"Site: {site_constant.SITE_NAME}, archivio search error: {e}") - - if site_constant.TELEGRAM_BOT and choices and len(choices) > 0: - bot.send_message("List of results:", choices) + console.print(f"Site: {site_constants.SITE_NAME}, archivio search error: {e}") result_count = media_search_manager.get_length() - if result_count == 0: - console.print(f"Nothing matching was found for: {query}") - return result_count -def process_results(records: list, seen_titles: set, media_manager: MediaManager, choices: list = None) -> None: +def process_results(records: list, seen_titles: set, media_manager: MediaManager) -> None: """ - Add unique results to the media manager and to choices. + Add unique results to the media manager. """ for dict_title in records: try: @@ -138,8 +121,5 @@ def process_results(records: list, seen_titles: set, media_manager: MediaManager 'image': dict_title.get('imageurl') }) - if choices is not None: - choice_text = f"{len(choices)} - {dict_title.get('name')} ({dict_title.get('type')}) - Episodes: {dict_title.get('episodes_count')}" - choices.append(choice_text) except Exception as e: print(f"Error parsing a title entry: {e}") \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/animeunity/util/ScrapeSerie.py similarity index 95% rename from StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/animeunity/util/ScrapeSerie.py index 0cdfcccae..aad55c360 100644 --- a/StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/animeunity/util/ScrapeSerie.py @@ -4,9 +4,8 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client_curl -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import EpisodeManager, Episode +from StreamingCommunity.Util.http_client import create_client_curl, get_headers +from StreamingCommunity.Api.Template.object import EpisodeManager, Episode class ScrapeSerieAnime: diff --git a/StreamingCommunity/Api/Service/animeworld/__init__.py b/StreamingCommunity/Api/Service/animeworld/__init__.py new file mode 100644 index 000000000..84ece6798 --- /dev/null +++ b/StreamingCommunity/Api/Service/animeworld/__init__.py @@ -0,0 +1,99 @@ +# 21.03.25 + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, media_search_manager, table_show_manager +from .serie import download_series +from .film import download_film + + +# Variable +indice = 6 +_useFor = "Anime" +_deprecate = False + +msg = Prompt() +console = Console() + + +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} + + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + return False + + if select_title.type == "TV": + episode_selection = None + if selections: + episode_selection = selections.get('episode') + + download_series(select_title, episode_selection) + media_search_manager.clear() + table_show_manager.clear() + return True + + else: + download_film(select_title) + table_show_manager.clear() + return True + + +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) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Handle empty input + if not actual_search_query: + return False + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeworld/film.py b/StreamingCommunity/Api/Service/animeworld/film.py similarity index 57% rename from StreamingCommunity/Api/Site/animeworld/film.py rename to StreamingCommunity/Api/Service/animeworld/film.py index 79a2adfec..33c0ff2ce 100644 --- a/StreamingCommunity/Api/Site/animeworld/film.py +++ b/StreamingCommunity/Api/Service/animeworld/film.py @@ -8,18 +8,13 @@ # Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.message import start_message +from StreamingCommunity.Util import os_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.MP4 import MP4_Downloader -# Logic class +# Logic 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 import MP4_downloader from StreamingCommunity.Api.Player.sweetpixel import VideoSource @@ -37,27 +32,27 @@ def download_film(select_title: MediaItem): """ start_message() - scrape_serie = ScrapSerie(select_title.url, site_constant.FULL_URL) + scrape_serie = ScrapSerie(select_title.url, site_constants.FULL_URL) episodes = scrape_serie.get_episodes() # Get episode information episode_data = episodes[0] - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] ([cyan]{scrape_serie.get_name()}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} ([cyan]{scrape_serie.get_name()}) \n") # Define filename and path for the downloaded video serie_name_with_year = os_manager.get_sanitize_file(scrape_serie.get_name(), select_title.date) mp4_name = f"{serie_name_with_year}.mp4" - mp4_path = os.path.join(site_constant.ANIME_FOLDER, serie_name_with_year.replace('.mp4', '')) + mp4_path = os.path.join(site_constants.ANIME_FOLDER, serie_name_with_year.replace('.mp4', '')) # 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) + video_source = VideoSource(site_constants.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( + path, kill_handler = MP4_Downloader( url=str(mp4_link).strip(), path=os.path.join(mp4_path, mp4_name) ) diff --git a/StreamingCommunity/Api/Site/animeworld/serie.py b/StreamingCommunity/Api/Service/animeworld/serie.py similarity index 72% rename from StreamingCommunity/Api/Site/animeworld/serie.py rename to StreamingCommunity/Api/Service/animeworld/serie.py index b4147ec47..e68969fa7 100644 --- a/StreamingCommunity/Api/Site/animeworld/serie.py +++ b/StreamingCommunity/Api/Service/animeworld/serie.py @@ -10,19 +10,14 @@ # Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.message import start_message +from StreamingCommunity.Util import os_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import manage_selection, dynamic_format_number +from StreamingCommunity.Lib.MP4 import MP4_Downloader -# Logic class +# Logic 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 -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - - -# Player -from StreamingCommunity import MP4_downloader from StreamingCommunity.Api.Player.sweetpixel import VideoSource @@ -47,21 +42,21 @@ def download_episode(index_select: int, scrape_serie: ScrapSerie) -> Tuple[str,b # Get episode information episode_data = scrape_serie.selectEpisode(1, index_select) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.get_name()}[/cyan] ([cyan]E{str(index_select+1)}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.get_name()} ([cyan]E{str(index_select+1)}) \n") # Define filename and path for the downloaded video mp4_name = f"{scrape_serie.get_name()}_EP_{dynamic_format_number(str(index_select+1))}.mp4" - mp4_path = os.path.join(site_constant.ANIME_FOLDER, scrape_serie.get_name()) + mp4_path = os.path.join(site_constants.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) + video_source = VideoSource(site_constants.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( + path, kill_handler = MP4_Downloader( url=str(mp4_link).strip(), path=os.path.join(mp4_path, mp4_name) ) @@ -80,11 +75,11 @@ def download_series(select_title: MediaItem, episode_selection: str = None): start_message() # Create scrap instance - scrape_serie = ScrapSerie(select_title.url, site_constant.FULL_URL) + scrape_serie = ScrapSerie(select_title.url, site_constants.FULL_URL) episodes = scrape_serie.get_episodes() # Get episode count - console.print(f"\n[green]Episodes count:[/green] [red]{len(episodes)}[/red]") + console.print(f"\n[green]Episodes count: [red]{len(episodes)}") # Display episodes list and get user selection if episode_selection is None: diff --git a/StreamingCommunity/Api/Site/animeworld/site.py b/StreamingCommunity/Api/Service/animeworld/site.py similarity index 84% rename from StreamingCommunity/Api/Site/animeworld/site.py rename to StreamingCommunity/Api/Service/animeworld/site.py index 3bbf1c4ef..36660d3ab 100644 --- a/StreamingCommunity/Api/Site/animeworld/site.py +++ b/StreamingCommunity/Api/Service/animeworld/site.py @@ -9,15 +9,11 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Api.Template import site_constants, MediaManager from StreamingCommunity.Util.table import TVShowManager -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager - # Variable console = Console() @@ -31,7 +27,7 @@ def get_session_and_csrf() -> dict: """ # Send an initial GET request to the website client = create_client(headers=get_headers()) - response = client.get(site_constant.FULL_URL) + response = client.get(site_constants.FULL_URL) # Extract the sessionId from the cookies session_id = response.cookies.get('sessionId') @@ -65,14 +61,14 @@ def title_search(query: str) -> int: Returns: - int: A number containing the length of media search manager. """ - search_url = f"{site_constant.FULL_URL}/search?keyword={query}" + search_url = f"{site_constants.FULL_URL}/search?keyword={query}" console.print(f"[cyan]Search url: [yellow]{search_url}") # Make the GET request try: response = create_client(headers=get_headers()).get(search_url) except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 # Create soup istance @@ -82,7 +78,7 @@ def title_search(query: str) -> int: for element in soup.find_all('a', class_='poster'): try: title = element.find('img').get('alt') - url = f"{site_constant.FULL_URL}{element.get('href')}" + url = f"{site_constants.FULL_URL}{element.get('href')}" status_div = element.find('div', class_='status') is_dubbed = False anime_type = 'TV' diff --git a/StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/animeworld/util/ScrapeSerie.py similarity index 82% rename from StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/animeworld/util/ScrapeSerie.py index b07c5e962..a8e554afc 100644 --- a/StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/animeworld/util/ScrapeSerie.py @@ -8,14 +8,12 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent from StreamingCommunity.Util.os import os_manager # Player from ..site import get_session_and_csrf -from StreamingCommunity.Api.Player.sweetpixel import VideoSource @@ -62,17 +60,6 @@ def get_episodes(self, nums=None): episodes = [episode_data for episode_data in raw_eps.values()] return episodes - - def get_episode(self, index): - """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 VideoSource(episode_data, self.session_id, self.csrf_token) - - else: - raise IndexError("Episode index out of range") # ------------- FOR GUI ------------- diff --git a/StreamingCommunity/Api/Service/crunchyroll/__init__.py b/StreamingCommunity/Api/Service/crunchyroll/__init__.py new file mode 100644 index 000000000..bd5345929 --- /dev/null +++ b/StreamingCommunity/Api/Service/crunchyroll/__init__.py @@ -0,0 +1,103 @@ +# 16.03.25 + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, table_show_manager, media_search_manager +from .film import download_film +from .series import download_series + + +# Variable +indice = 7 +_useFor = "Anime" +_deprecate = False + +msg = Prompt() +console = Console() + + +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} + + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + return False + + 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) + media_search_manager.clear() + table_show_manager.clear() + return True + + else: + download_film(select_title) + table_show_manager.clear() + return True + + +# 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. + + 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) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Handle empty input + if not actual_search_query: + return False + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/crunchyroll/film.py b/StreamingCommunity/Api/Service/crunchyroll/film.py similarity index 74% rename from StreamingCommunity/Api/Site/crunchyroll/film.py rename to StreamingCommunity/Api/Service/crunchyroll/film.py index 5e13a58cb..c9da604fc 100644 --- a/StreamingCommunity/Api/Site/crunchyroll/film.py +++ b/StreamingCommunity/Api/Service/crunchyroll/film.py @@ -9,18 +9,12 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.os import os_manager +from StreamingCommunity.Util import config_manager, os_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.DASH.downloader import DASH_Downloader -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - - -# Player -from StreamingCommunity import DASH_Downloader +# Logi from .util.get_license import get_playback_session, CrunchyrollClient @@ -40,17 +34,17 @@ def download_film(select_title: MediaItem) -> str: - str: output path if successful, otherwise None """ start_message() - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{select_title.name}[/cyan] \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") # Initialize Crunchyroll client client = CrunchyrollClient() if not client.start(): - console.print("[bold red]Failed to authenticate with Crunchyroll.[/bold red]") + console.print("[red]Failed to authenticate with Crunchyroll.") return None, True # Define filename and path for the downloaded video mp4_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output - mp4_path = os.path.join(site_constant.MOVIE_FOLDER, mp4_name.replace(extension_output, "")) + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, mp4_name.replace(extension_output, "")) # Generate mpd and license URLs url_id = select_title.get('url').split('/')[-1] @@ -61,13 +55,13 @@ def download_film(select_title: MediaItem) -> str: # Check if access was denied (403) if playback_result is None: - console.print("[bold red]βœ— Access denied:[/bold red] This content requires a premium subscription") + console.print("[red]βœ— Access denied: This content requires a premium subscription") return None, False mpd_url, mpd_headers, mpd_list_sub, token, audio_locale = playback_result except Exception as e: - console.print(f"[bold red]βœ— Error getting playback session:[/bold red] {str(e)}") + console.print(f"[red]βœ— Error getting playback session: {str(e)}") return None, False # Parse playback token from mpd_url diff --git a/StreamingCommunity/Api/Site/crunchyroll/series.py b/StreamingCommunity/Api/Service/crunchyroll/series.py similarity index 87% rename from StreamingCommunity/Api/Site/crunchyroll/series.py rename to StreamingCommunity/Api/Service/crunchyroll/series.py index 767dd8537..9eee5a145 100644 --- a/StreamingCommunity/Api/Site/crunchyroll/series.py +++ b/StreamingCommunity/Api/Service/crunchyroll/series.py @@ -11,14 +11,9 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.config_json import config_manager - - -# Logic class -from .util.ScrapeSerie import GetSerieInfo -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import os_manager, config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -26,12 +21,11 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.DASH.downloader import DASH_Downloader -# Player -from StreamingCommunity import DASH_Downloader +# Logic +from .util.ScrapeSerie import GetSerieInfo from .util.get_license import get_playback_session @@ -59,11 +53,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.get('name')}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.get('name')}[/magenta] ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.get('name'))}.{extension_output}" - mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}")) + mp4_path = os_manager.get_sanitize_path(os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}")) # Generate mpd and license URLs url_id = obj_episode.get('url').split('/')[-1] @@ -75,13 +69,13 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Check if access was denied (403) if playback_result is None: - console.print("[bold red]βœ— Access denied:[/bold red] This episode requires a premium subscription") + console.print("[red]βœ— Access denied: This episode requires a premium subscription") return None, False mpd_url, mpd_headers, mpd_list_sub, token, audio_locale = playback_result except Exception as e: - console.print(f"[bold red]βœ— Error getting playback session:[/bold red] {str(e)}") + console.print(f"[red]βœ— Error getting playback session: {str(e)}") return None, False parsed_url = urlparse(mpd_url) diff --git a/StreamingCommunity/Api/Site/crunchyroll/site.py b/StreamingCommunity/Api/Service/crunchyroll/site.py similarity index 86% rename from StreamingCommunity/Api/Site/crunchyroll/site.py rename to StreamingCommunity/Api/Service/crunchyroll/site.py index ceee7c7d1..2d545dc57 100644 --- a/StreamingCommunity/Api/Site/crunchyroll/site.py +++ b/StreamingCommunity/Api/Service/crunchyroll/site.py @@ -7,11 +7,10 @@ # Internal utilities from StreamingCommunity.Util.config_json import config_manager from StreamingCommunity.Util.table import TVShowManager +from StreamingCommunity.Api.Template import site_constants, MediaManager -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager +# Logic from .util.get_license import CrunchyrollClient @@ -36,12 +35,12 @@ def title_search(query: str) -> int: config = config_manager.get_dict("SITE_LOGIN", "crunchyroll") if not config.get('device_id') or not config.get('etp_rt'): - console.print("[bold red] device_id or etp_rt is missing or empty in config.json.[/bold red]") + console.print("[red] device_id or etp_rt is missing or empty in config.json.") raise Exception("device_id or etp_rt is missing or empty in config.json.") client = CrunchyrollClient() if not client.start(): - console.print("[bold red] Failed to authenticate with Crunchyroll.[/bold red]") + console.print("[red] Failed to authenticate with Crunchyroll.") raise Exception("Failed to authenticate with Crunchyroll.") api_url = "https://www.crunchyroll.com/content/v2/discover/search" @@ -62,7 +61,7 @@ def title_search(query: str) -> int: response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 data = response.json() diff --git a/StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/crunchyroll/util/ScrapeSerie.py similarity index 98% rename from StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/crunchyroll/util/ScrapeSerie.py index b80e7e1c3..54d64d00d 100644 --- a/StreamingCommunity/Api/Site/crunchyroll/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/crunchyroll/util/ScrapeSerie.py @@ -5,11 +5,11 @@ # Internal utilities -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager +from StreamingCommunity.Api.Template.object import SeasonManager from .get_license import CrunchyrollClient -# Static configuration +# Variable NORMALIZE_SEASON_NUMBERS = False # Set to True to remap seasons to 1..N range diff --git a/StreamingCommunity/Api/Site/crunchyroll/util/get_license.py b/StreamingCommunity/Api/Service/crunchyroll/util/get_license.py similarity index 98% rename from StreamingCommunity/Api/Site/crunchyroll/util/get_license.py rename to StreamingCommunity/Api/Service/crunchyroll/util/get_license.py index 7446cf907..233f78e55 100644 --- a/StreamingCommunity/Api/Site/crunchyroll/util/get_license.py +++ b/StreamingCommunity/Api/Service/crunchyroll/util/get_license.py @@ -7,8 +7,7 @@ # Internal utilities from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.http_client import create_client_curl -from StreamingCommunity.Util.headers import get_userAgent +from StreamingCommunity.Util.http_client import create_client_curl, get_userAgent # Variable @@ -45,8 +44,8 @@ def wait(self): class CrunchyrollClient: def __init__(self) -> None: config = config_manager.get_dict("SITE_LOGIN", "crunchyroll") - self.device_id = config.get('device_id') - self.etp_rt = config.get('etp_rt') + self.device_id = str(config.get('device_id')).strip() + self.etp_rt = str(config.get('etp_rt')).strip() self.locale = "it-IT" self.access_token: Optional[str] = None diff --git a/StreamingCommunity/Api/Site/dmax/__init__.py b/StreamingCommunity/Api/Service/dmax/__init__.py similarity index 80% rename from StreamingCommunity/Api/Site/dmax/__init__.py rename to StreamingCommunity/Api/Service/dmax/__init__.py index 8addbbbf9..65fed939c 100644 --- a/StreamingCommunity/Api/Site/dmax/__init__.py +++ b/StreamingCommunity/Api/Service/dmax/__init__.py @@ -6,12 +6,10 @@ # 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.Api.Template import site_constants, MediaItem, get_select_title -# Logic class +# Logic from .site import title_search, table_show_manager, media_search_manager from .series import download_series @@ -19,26 +17,12 @@ # Variable indice = 9 _useFor = "Serie" -_priority = 0 -_engineDownload = "hls" _deprecate = False msg = Prompt() console = Console() -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - else: - 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. @@ -85,7 +69,11 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ return result # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() # Handle empty input if not actual_search_query: diff --git a/StreamingCommunity/Api/Site/dmax/series.py b/StreamingCommunity/Api/Service/dmax/series.py similarity index 88% rename from StreamingCommunity/Api/Site/dmax/series.py rename to StreamingCommunity/Api/Service/dmax/series.py index fe95b9e26..f77288716 100644 --- a/StreamingCommunity/Api/Site/dmax/series.py +++ b/StreamingCommunity/Api/Service/dmax/series.py @@ -10,13 +10,9 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager - - -# Logic class -from ..realtime.util.ScrapeSerie import GetSerieInfo -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -24,12 +20,11 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Player -from StreamingCommunity import HLS_Downloader +# Logic +from ..realtime.util.ScrapeSerie import GetSerieInfo from ..realtime.util.get_license import get_bearer_token, get_playback_url @@ -56,11 +51,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.name}[/magenta] ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.{extension_output}" - mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") + mp4_path = os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") # Get m3u8 playlist bearer_token = get_bearer_token() diff --git a/StreamingCommunity/Api/Site/dmax/site.py b/StreamingCommunity/Api/Service/dmax/site.py similarity index 84% rename from StreamingCommunity/Api/Site/dmax/site.py rename to StreamingCommunity/Api/Service/dmax/site.py index d2a41cbf2..b836c795e 100644 --- a/StreamingCommunity/Api/Site/dmax/site.py +++ b/StreamingCommunity/Api/Service/dmax/site.py @@ -6,14 +6,9 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent from StreamingCommunity.Util.table import TVShowManager - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager +from StreamingCommunity.Api.Template import site_constants, MediaManager # Variable @@ -43,7 +38,7 @@ def title_search(query: str) -> int: response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 # Collect json data diff --git a/StreamingCommunity/Api/Site/guardaserie/__init__.py b/StreamingCommunity/Api/Service/guardaserie/__init__.py similarity index 52% rename from StreamingCommunity/Api/Site/guardaserie/__init__.py rename to StreamingCommunity/Api/Service/guardaserie/__init__.py index a520293fb..7caeddc51 100644 --- a/StreamingCommunity/Api/Site/guardaserie/__init__.py +++ b/StreamingCommunity/Api/Service/guardaserie/__init__.py @@ -1,7 +1,5 @@ # 09.06.24 -import sys -import subprocess from urllib.parse import quote_plus @@ -11,13 +9,10 @@ # 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 +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title -# Logic class +# Logic from .site import title_search, media_search_manager, table_show_manager from .series import download_series @@ -25,53 +20,12 @@ # Variable indice = 4 _useFor = "Serie" -_priority = 0 -_engineDownload = "hls" _deprecate = False msg = Prompt() console = Console() -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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. @@ -85,11 +39,7 @@ def process_search_result(select_title, selections=None): bool: True if processing was successful, False otherwise """ if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") + console.print("[yellow]No title selected or selection cancelled.") return False season_selection = None @@ -116,23 +66,20 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ selections (dict, optional): Dictionary containing selection inputs that bypass manual input {'season': season_selection, 'episode': episode_selection} """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - if direct_item: select_title = MediaItem(**direct_item) result = process_search_result(select_title, selections) return result # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + # Handle empty input if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) return False # Search on database @@ -148,8 +95,5 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ return result else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/guardaserie/series.py b/StreamingCommunity/Api/Service/guardaserie/series.py similarity index 88% rename from StreamingCommunity/Api/Site/guardaserie/series.py rename to StreamingCommunity/Api/Service/guardaserie/series.py index aa403a11e..95046e694 100644 --- a/StreamingCommunity/Api/Site/guardaserie/series.py +++ b/StreamingCommunity/Api/Service/guardaserie/series.py @@ -10,12 +10,9 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager - - -# Logic class -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, dynamic_format_number, @@ -24,13 +21,11 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Player +# Logic from .util.ScrapeSerie import GetSerieInfo -from StreamingCommunity import HLS_Downloader from StreamingCommunity.Api.Player.supervideo import VideoSource @@ -58,11 +53,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) index_season_selected_formatted = dynamic_format_number(str(index_season_selected)) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.tv_name}[/cyan] \\ [bold magenta]{obj_episode.get('name')}[/bold magenta] ([cyan]S{index_season_selected_formatted}E{index_episode_selected}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.tv_name} \\ [magenta]{obj_episode.get('name')}[/magenta] ([cyan]S{index_season_selected_formatted}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.tv_name, index_season_selected_formatted, index_episode_selected, obj_episode.get('name'))}.{extension_output}" - mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.tv_name, f"S{index_season_selected_formatted}") + mp4_path = os.path.join(site_constants.SERIES_FOLDER, scrape_serie.tv_name, f"S{index_season_selected_formatted}") # Setup video source video_source = VideoSource(obj_episode.get('url')) diff --git a/StreamingCommunity/Api/Site/guardaserie/site.py b/StreamingCommunity/Api/Service/guardaserie/site.py similarity index 72% rename from StreamingCommunity/Api/Site/guardaserie/site.py rename to StreamingCommunity/Api/Service/guardaserie/site.py index 75daf4138..ea3854908 100644 --- a/StreamingCommunity/Api/Site/guardaserie/site.py +++ b/StreamingCommunity/Api/Service/guardaserie/site.py @@ -7,14 +7,9 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent from StreamingCommunity.Util.table import TVShowManager - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager +from StreamingCommunity.Api.Template import site_constants, MediaManager # Variable @@ -36,14 +31,14 @@ def title_search(query: str) -> int: media_search_manager.clear() table_show_manager.clear() - search_url = f"{site_constant.FULL_URL}/?story={query}&do=search&subaction=search" + search_url = f"{site_constants.FULL_URL}/?story={query}&do=search&subaction=search" console.print(f"[cyan]Search url: [yellow]{search_url}") try: response = create_client(headers={'user-agent': get_userAgent()}).get(search_url) response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 # Create soup and find table @@ -55,7 +50,7 @@ def title_search(query: str) -> int: 'name': serie_div.find('a').get("title").replace("streaming guardaserie", ""), 'type': 'tv', 'url': serie_div.find('a').get("href"), - 'image': f"{site_constant.FULL_URL}/{serie_div.find('img').get('src')}" + 'image': f"{site_constants.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/Service/guardaserie/util/ScrapeSerie.py similarity index 94% rename from StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/guardaserie/util/ScrapeSerie.py index a6f38f488..a5b40952a 100644 --- a/StreamingCommunity/Api/Site/guardaserie/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/guardaserie/util/ScrapeSerie.py @@ -9,14 +9,10 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Api.Template.object import SeasonManager, MediaItem -# Logic class -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - class GetSerieInfo: def __init__(self, dict_serie: MediaItem) -> None: diff --git a/StreamingCommunity/Api/Site/hd4me/__init__.py b/StreamingCommunity/Api/Service/hd4me/__init__.py similarity index 78% rename from StreamingCommunity/Api/Site/hd4me/__init__.py rename to StreamingCommunity/Api/Service/hd4me/__init__.py index 450e5f67f..fdbbe626f 100644 --- a/StreamingCommunity/Api/Site/hd4me/__init__.py +++ b/StreamingCommunity/Api/Service/hd4me/__init__.py @@ -7,40 +7,24 @@ # 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.Api.Template import site_constants, get_select_title +from StreamingCommunity.Api.Template.object import MediaItem -# Logic class +# Logic from .site import title_search, table_show_manager, media_search_manager from .film import download_film # Variable indice = 10 -_useFor = "Film" -_priority = 0 -_engineDownload = "hls" +_useFor = "Film_&_Serie" _deprecate = False msg = Prompt() console = Console() -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - else: - 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. @@ -80,7 +64,11 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ return result # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() # Handle empty input if not actual_search_query: diff --git a/StreamingCommunity/Api/Site/hd4me/film.py b/StreamingCommunity/Api/Service/hd4me/film.py similarity index 55% rename from StreamingCommunity/Api/Site/hd4me/film.py rename to StreamingCommunity/Api/Service/hd4me/film.py index c980f4059..4fa66a65c 100644 --- a/StreamingCommunity/Api/Site/hd4me/film.py +++ b/StreamingCommunity/Api/Service/hd4me/film.py @@ -9,20 +9,11 @@ # Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client_curl -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - - -# Player -from StreamingCommunity import Mega_Downloader +from StreamingCommunity.Util.http_client import create_client_curl, get_headers +from StreamingCommunity.Util import config_manager, os_manager, start_message +from StreamingCommunity.Api.Template import site_constants +from StreamingCommunity.Api.Template.object import MediaItem +from StreamingCommunity.Lib.MEGA import MEGA_Downloader # Variable @@ -41,7 +32,7 @@ def download_film(select_title: MediaItem) -> str: - str: output path if successful, otherwise None """ start_message() - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{select_title.name}[/cyan] \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") mega_link = None try: @@ -53,30 +44,29 @@ def download_film(select_title: MediaItem) -> str: for a in soup.find_all("a", href=True): if "?!" in a["href"].lower().strip(): - mega_link = "https://mega.nz/file/" + a["href"].split("/")[-1].replace('?!', '') + mega_link = "https://mega.nz/#!" + a["href"].split("/")[-1].replace('?!', '') break if "/?file/" in a["href"].lower().strip(): - mega_link = "https://mega.nz/file/" + a["href"].split("/")[-1].replace('/?file/', '') + mega_link = "https://mega.nz/#!" + a["href"].split("/")[-1].replace('/?file/', '') break except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get mostraguarda") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request error: {e}, get mostraguarda") return None # Define the filename and path for the downloaded film title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output - mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, title_name.replace(extension_output, "")) # Download the film using the mega downloader - mega = Mega_Downloader() - m = mega.login() + mega = MEGA_Downloader(choose_files=True) if mega_link is None: - console.print(f"[red]Site: {site_constant.SITE_NAME}, error: Mega link not found for url: {select_title.url}[/red]") + console.print(f"[red]Site: {site_constants.SITE_NAME}, error: Mega link not found for url: {select_title.url}") return None - output_path = m.download_url( + output_path = mega.download_url( url=mega_link, dest_path=os.path.join(mp4_path, title_name) ) diff --git a/StreamingCommunity/Api/Site/hd4me/site.py b/StreamingCommunity/Api/Service/hd4me/site.py similarity index 81% rename from StreamingCommunity/Api/Site/hd4me/site.py rename to StreamingCommunity/Api/Service/hd4me/site.py index 8101d7365..dacfaf87c 100644 --- a/StreamingCommunity/Api/Site/hd4me/site.py +++ b/StreamingCommunity/Api/Service/hd4me/site.py @@ -7,16 +7,11 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Api.Template import site_constants, MediaManager from StreamingCommunity.Util.table import TVShowManager -# 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() @@ -44,7 +39,7 @@ def title_search(query: str) -> int: response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 # Create soup instance diff --git a/StreamingCommunity/Api/Service/ipersphera/__init__.py b/StreamingCommunity/Api/Service/ipersphera/__init__.py new file mode 100644 index 000000000..a776e0374 --- /dev/null +++ b/StreamingCommunity/Api/Service/ipersphera/__init__.py @@ -0,0 +1,92 @@ +# 16.12.25 + + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, table_show_manager, media_search_manager +from .film import download_film + + +# Variable +indice = 12 +_useFor = "Film_&_Serie" +_deprecate = False + +msg = Prompt() +console = Console() + + +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} + + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + console.print("[yellow]No title selected or selection cancelled.") + return False + + if select_title.type == 'film' or select_title.type == 'tv': + download_film(select_title) + table_show_manager.clear() + return True + + + +# 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. + + 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) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Handle empty input + if not actual_search_query: + return False + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Service/ipersphera/film.py b/StreamingCommunity/Api/Service/ipersphera/film.py new file mode 100644 index 000000000..d7af415e5 --- /dev/null +++ b/StreamingCommunity/Api/Service/ipersphera/film.py @@ -0,0 +1,83 @@ +# 16.12.25 + +import os + +# External library +from bs4 import BeautifulSoup +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Util import config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.MEGA import MEGA_Downloader + + +# Variable +console = Console() +extension_output = config_manager.get("M3U8_CONVERSION", "extension") + + +def download_film(select_title: MediaItem) -> str: + """ + Downloads a film using the provided film ID, title name, and domain. + + Parameters: + - select_title (MediaItem): The selected media item. + + Return: + - str: output path if successful, otherwise None + """ + start_message() + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") + + # Extract proton url + proton_url = None + try: + response = create_client(headers=get_headers()).get(select_title.url) + response.raise_for_status() + + soup = BeautifulSoup(response.text, 'html.parser') + for link in soup.find_all('a', href=True): + href = link['href'] + if 'uprot' in href: + proton_url = href + break + + except Exception as e: + console.print(f"[red]Site: {site_constants.SITE_NAME}, request error: {e}, get proton URL") + return None + + # Extract mega link + mega_link = None + try: + response = create_client(headers=get_headers()).get(proton_url) + response.raise_for_status() + + soup = BeautifulSoup(response.text, 'html.parser') + for link in soup.find_all('a', href=True): + href = link['href'] + if 'mega' in href: + mega_link = href + break + + except Exception as e: + console.print(f"[red]Site: {site_constants.SITE_NAME}, request error: {e}, get mega link") + return None + + # Define the filename and path for the downloaded film + if select_title.type == "film": + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, str(select_title.name).replace(extension_output, "")) + else: + mp4_path = os.path.join(site_constants.SERIES_FOLDER, str(select_title.name).replace(extension_output, "")) + + # Download from MEGA + mega = MEGA_Downloader( + choose_files=True + ) + output_path = mega.download_url( + url=mega_link, + dest_path=mp4_path + ) + return output_path \ No newline at end of file diff --git a/StreamingCommunity/Api/Service/ipersphera/site.py b/StreamingCommunity/Api/Service/ipersphera/site.py new file mode 100644 index 000000000..01c79843b --- /dev/null +++ b/StreamingCommunity/Api/Service/ipersphera/site.py @@ -0,0 +1,86 @@ +# 16.12.25 + + +# External libraries +from bs4 import BeautifulSoup +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util.http_client import create_client_curl, get_userAgent +from StreamingCommunity.Util.table import TVShowManager +from StreamingCommunity.Api.Template import site_constants, MediaManager + + +# Variable +console = Console() +media_search_manager = MediaManager() +table_show_manager = TVShowManager() + + +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.ipersphera.com/?s={query}" + console.print(f"[cyan]Search url: [yellow]{search_url}") + + try: + response = create_client_curl(headers={'user-agent': get_userAgent()}).get(search_url) + response.raise_for_status() + except Exception as e: + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") + return 0 + + # Create soup instance + soup = BeautifulSoup(response.text, "html.parser") + table = soup.find("div", id="content") + + # Track seen URLs to avoid duplicates + seen_urls = set() + + for i, element in enumerate(table.find_all("div")): + + # Extract title and URL from h2.posttitle > a + title_element = element.find("h2", class_="posttitle") + if not title_element: + continue + + link = title_element.find("a") + if not link: + continue + + title = link.text.strip() + url = link.get('href', '') + + # Skip duplicates + if url in seen_urls: + continue + seen_urls.add(url) + + # Determine type based on categories + categs_div = element.find("div", class_="categs") + tipo = "film" + if categs_div: + categs_text = categs_div.get_text().lower() + if "serie" in categs_text or "tv" in categs_text: + tipo = "tv" + + media_dict = { + 'url': url, + 'name': title, + 'type': tipo + } + media_search_manager.add_media(media_dict) + + # Return the number of titles found + return media_search_manager.get_length() \ No newline at end of file diff --git a/StreamingCommunity/Api/Service/mediasetinfinity/__init__.py b/StreamingCommunity/Api/Service/mediasetinfinity/__init__.py new file mode 100644 index 000000000..60e2f7542 --- /dev/null +++ b/StreamingCommunity/Api/Service/mediasetinfinity/__init__.py @@ -0,0 +1,103 @@ +# 21.05.24 + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, table_show_manager, media_search_manager +from .series import download_series +from .film import download_film + + +# Variable +indice = 3 +_useFor = "Film_&_Serie" +_deprecate = False + +msg = Prompt() +console = Console() + + +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} + + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + console.print("[yellow]No title selected or selection cancelled.") + return False + + 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) + media_search_manager.clear() + table_show_manager.clear() + return True + + else: + download_film(select_title) + table_show_manager.clear() + return True + + +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) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Handle empty input + if not actual_search_query: + return False + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/mediasetinfinity/film.py b/StreamingCommunity/Api/Service/mediasetinfinity/film.py similarity index 72% rename from StreamingCommunity/Api/Site/mediasetinfinity/film.py rename to StreamingCommunity/Api/Service/mediasetinfinity/film.py index 92d644864..c1cbb3e66 100644 --- a/StreamingCommunity/Api/Site/mediasetinfinity/film.py +++ b/StreamingCommunity/Api/Service/mediasetinfinity/film.py @@ -9,23 +9,18 @@ # Internal utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.headers import get_headers +from StreamingCommunity.Util import os_manager, config_manager, start_message +from StreamingCommunity.Util.http_client import get_headers +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.DASH.downloader import DASH_Downloader -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - - -# Player +# Logic from .util.fix_mpd import get_manifest -from StreamingCommunity import DASH_Downloader from .util.get_license import get_playback_url, get_tracking_info, generate_license_url + # Variable console = Console() extension_output = config_manager.get("M3U8_CONVERSION", "extension") @@ -42,11 +37,11 @@ def download_film(select_title: MediaItem) -> Tuple[str, bool]: - str: output path if successful, otherwise None """ start_message() - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{select_title.name}[/cyan] \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") # Define the filename and path for the downloaded film title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output - mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, title_name.replace(extension_output, "")) # Get playback URL and tracking info playback_json = get_playback_url(select_title.id) diff --git a/StreamingCommunity/Api/Site/mediasetinfinity/series.py b/StreamingCommunity/Api/Service/mediasetinfinity/series.py similarity index 87% rename from StreamingCommunity/Api/Site/mediasetinfinity/series.py rename to StreamingCommunity/Api/Service/mediasetinfinity/series.py index abfec9d24..71f9b2553 100644 --- a/StreamingCommunity/Api/Site/mediasetinfinity/series.py +++ b/StreamingCommunity/Api/Service/mediasetinfinity/series.py @@ -10,15 +10,10 @@ # Internal utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.os import os_manager - - -# Logic class -from .util.ScrapeSerie import GetSerieInfo -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import os_manager, config_manager, start_message +from StreamingCommunity.Util.http_client import get_headers +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -26,13 +21,12 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.DASH.downloader import DASH_Downloader -# Player +# Logic +from .util.ScrapeSerie import GetSerieInfo from .util.fix_mpd import get_manifest -from StreamingCommunity import DASH_Downloader from .util.get_license import get_playback_url, get_tracking_info, generate_license_url @@ -59,11 +53,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.name}[/magenta] ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.{extension_output}" - mp4_path = os_manager.get_sanitize_path(os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}")) + mp4_path = os_manager.get_sanitize_path(os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}")) # Generate mpd and license URLs playback_json = get_playback_url(obj_episode.id) diff --git a/StreamingCommunity/Api/Site/mediasetinfinity/site.py b/StreamingCommunity/Api/Service/mediasetinfinity/site.py similarity index 91% rename from StreamingCommunity/Api/Site/mediasetinfinity/site.py rename to StreamingCommunity/Api/Service/mediasetinfinity/site.py index 6b42dcb05..db0231fee 100644 --- a/StreamingCommunity/Api/Site/mediasetinfinity/site.py +++ b/StreamingCommunity/Api/Service/mediasetinfinity/site.py @@ -9,12 +9,11 @@ # Internal utilities from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Api.Template import site_constants, MediaManager from StreamingCommunity.Util.table import TVShowManager -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager -# Logic class +# Logic from .util.get_license import get_bearer_token @@ -49,7 +48,7 @@ def title_search(query: str) -> int: response = create_client(headers=class_mediaset_api.generate_request_headers()).get(search_url, params=params) response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 # Parse response diff --git a/StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/mediasetinfinity/util/ScrapeSerie.py similarity index 95% rename from StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/mediasetinfinity/util/ScrapeSerie.py index 7daa70c95..17d4affa6 100644 --- a/StreamingCommunity/Api/Site/mediasetinfinity/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/mediasetinfinity/util/ScrapeSerie.py @@ -9,13 +9,12 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_headers, get_userAgent -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager +from StreamingCommunity.Util.http_client import create_client, get_userAgent, get_headers +from StreamingCommunity.Api.Template.object import SeasonManager class GetSerieInfo: - def __init__(self, url, min_duration=10): + def __init__(self, url): """ Initialize the GetSerieInfo class for scraping TV series information. @@ -25,7 +24,6 @@ def __init__(self, url, min_duration=10): """ self.headers = get_headers() self.url = url - self.min_duration = min_duration self.seasons_manager = SeasonManager() self.serie_id = None self.public_id = None @@ -145,16 +143,10 @@ def _get_season_episodes(self, season, sb_id, category_name): episode_data = episode_response.json() episodes = [] - filtered_count = 0 for entry in episode_data.get('entries', []): duration = int(entry.get('mediasetprogram$duration', 0) / 60) if entry.get('mediasetprogram$duration') else 0 - # Filter episodes by minimum duration - if duration < self.min_duration: - filtered_count += 1 - continue - episode_info = { 'id': entry.get('guid'), 'title': entry.get('title'), diff --git a/StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py b/StreamingCommunity/Api/Service/mediasetinfinity/util/fix_mpd.py similarity index 100% rename from StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py rename to StreamingCommunity/Api/Service/mediasetinfinity/util/fix_mpd.py diff --git a/StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py b/StreamingCommunity/Api/Service/mediasetinfinity/util/get_license.py similarity index 98% rename from StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py rename to StreamingCommunity/Api/Service/mediasetinfinity/util/get_license.py index 89bad570b..da3850125 100644 --- a/StreamingCommunity/Api/Site/mediasetinfinity/util/get_license.py +++ b/StreamingCommunity/Api/Service/mediasetinfinity/util/get_license.py @@ -11,8 +11,7 @@ # Internal utilities -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.headers import get_headers, get_userAgent +from StreamingCommunity.Util.http_client import create_client, get_headers, get_userAgent # Variable diff --git a/StreamingCommunity/Api/Service/raiplay/__init__.py b/StreamingCommunity/Api/Service/raiplay/__init__.py new file mode 100644 index 000000000..027a8ccdb --- /dev/null +++ b/StreamingCommunity/Api/Service/raiplay/__init__.py @@ -0,0 +1,103 @@ +# 21.05.24 + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, table_show_manager, media_search_manager +from .series import download_series +from .film import download_film + + +# Variable +indice = 5 +_useFor = "Film_&_Serie" +_deprecate = False + +msg = Prompt() +console = Console() + + +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} + + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + console.print("[yellow]No title selected or selection cancelled.") + return False + + 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) + media_search_manager.clear() + table_show_manager.clear() + return True + + else: + download_film(select_title) + table_show_manager.clear() + return True + + +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) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Handle empty input + if not actual_search_query: + return False + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/film.py b/StreamingCommunity/Api/Service/raiplay/film.py similarity index 68% rename from StreamingCommunity/Api/Site/raiplay/film.py rename to StreamingCommunity/Api/Service/raiplay/film.py index d361f6811..8223ac9ca 100644 --- a/StreamingCommunity/Api/Site/raiplay/film.py +++ b/StreamingCommunity/Api/Service/raiplay/film.py @@ -9,20 +9,16 @@ # Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.message import start_message - -# Logic class -from .util.get_license import generate_license_url -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Util import os_manager, config_manager, start_message +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.DASH.downloader import DASH_Downloader +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Player -from StreamingCommunity import HLS_Downloader, DASH_Downloader +# Logic +from .util.get_license import generate_license_url +from .util.fix_mpd import fix_manifest_url from StreamingCommunity.Api.Player.mediapolisvod import VideoSource @@ -43,21 +39,21 @@ def download_film(select_title: MediaItem) -> Tuple[str, bool]: - bool: Whether download was stopped """ start_message() - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{select_title.name}[/cyan] \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") # Extract m3u8 URL from the film's URL response = create_client(headers=get_headers()).get(select_title.url + ".json") - first_item_path = "https://www.raiplay.it" + response.json().get("first_item_path") + 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 mp4_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output - mp4_path = os.path.join(site_constant.MOVIE_FOLDER, mp4_name.replace(extension_output, "")) + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, mp4_name.replace(extension_output, "")) # HLS if ".mpd" not in master_playlist: r_proc = HLS_Downloader( - m3u8_url=master_playlist, + m3u8_url=fix_manifest_url(master_playlist), output_path=os.path.join(mp4_path, mp4_name) ).start() diff --git a/StreamingCommunity/Api/Site/raiplay/series.py b/StreamingCommunity/Api/Service/raiplay/series.py similarity index 87% rename from StreamingCommunity/Api/Site/raiplay/series.py rename to StreamingCommunity/Api/Service/raiplay/series.py index 200c709c8..030c51eef 100644 --- a/StreamingCommunity/Api/Site/raiplay/series.py +++ b/StreamingCommunity/Api/Service/raiplay/series.py @@ -10,16 +10,10 @@ # Internal utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.headers import get_headers, get_userAgent -from StreamingCommunity.Util.message import start_message - - - -# Logic class -from .util.ScrapeSerie import GetSerieInfo -from .util.get_license import generate_license_url -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import config_manager, start_message +from StreamingCommunity.Util.http_client import get_headers, get_userAgent +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -27,15 +21,18 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.DASH.downloader import DASH_Downloader +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Player -from StreamingCommunity import HLS_Downloader, DASH_Downloader +# Logic +from .util.ScrapeSerie import GetSerieInfo +from .util.get_license import generate_license_url +from .util.fix_mpd import fix_manifest_url from StreamingCommunity.Api.Player.mediapolisvod import VideoSource + # Variable msg = Prompt() console = Console() @@ -59,11 +56,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.name}[/magenta] ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.{extension_output}" - mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") + mp4_path = os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") # Get streaming URL master_playlist = VideoSource.extract_m3u8_url(obj_episode.url) @@ -71,7 +68,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # HLS if ".mpd" not in master_playlist: r_proc = HLS_Downloader( - m3u8_url=master_playlist, + m3u8_url=fix_manifest_url(master_playlist), output_path=os.path.join(mp4_path, mp4_name) ).start() diff --git a/StreamingCommunity/Api/Site/raiplay/site.py b/StreamingCommunity/Api/Service/raiplay/site.py similarity index 83% rename from StreamingCommunity/Api/Site/raiplay/site.py rename to StreamingCommunity/Api/Service/raiplay/site.py index a6599a17f..06393326d 100644 --- a/StreamingCommunity/Api/Site/raiplay/site.py +++ b/StreamingCommunity/Api/Service/raiplay/site.py @@ -5,11 +5,9 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Api.Template import site_constants, MediaManager from StreamingCommunity.Util.table import TVShowManager -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager # Variable @@ -50,7 +48,7 @@ def title_search(query: str) -> int: response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 try: @@ -59,10 +57,10 @@ def title_search(query: str) -> int: # Limit to only 15 results for performance data = cards[:15] - console.print(f"[cyan]Found {len(cards)} results, processing first {len(data)}...[/cyan]") + console.print(f"[cyan]Found {len(cards)} results, processing first {len(data)}...") except Exception as e: - console.print(f"[red]Error parsing search results: {e}[/red]") + console.print(f"[red]Error parsing search results: {e}") return 0 # Process each item and add to media manager @@ -71,7 +69,7 @@ def title_search(query: str) -> int: # Get path_id path_id = item.get('path_id', '') if not path_id: - console.print("[yellow]Skipping item due to missing path_id[/yellow]") + console.print("[yellow]Skipping item due to missing path_id") continue # Get image URL - handle both relative and absolute URLs @@ -95,7 +93,7 @@ def title_search(query: str) -> int: }) except Exception as e: - console.print(f"[red]Error processing item '{item.get('titolo', 'Unknown')}': {e}[/red]") + console.print(f"[red]Error processing item '{item.get('titolo', 'Unknown')}': {e}") continue 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/Service/raiplay/util/ScrapeSerie.py similarity index 97% rename from StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/raiplay/util/ScrapeSerie.py index 3d8745926..da2727b4a 100644 --- a/StreamingCommunity/Api/Site/raiplay/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/raiplay/util/ScrapeSerie.py @@ -4,9 +4,8 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Api.Template.object import SeasonManager diff --git a/StreamingCommunity/Api/Service/raiplay/util/fix_mpd.py b/StreamingCommunity/Api/Service/raiplay/util/fix_mpd.py new file mode 100644 index 000000000..793c26c0b --- /dev/null +++ b/StreamingCommunity/Api/Service/raiplay/util/fix_mpd.py @@ -0,0 +1,27 @@ +# 18.12.25 + +import re + + +def fix_manifest_url(manifest_url: str) -> str: + """ + Fixes RaiPlay manifest URLs to include all available quality levels. + + Args: + manifest_url (str): Original manifest URL from RaiPlay + """ + STANDARD_QUALITIES = "1200,1800,2400,3600,5000" + pattern = r'(_,[\d,]+)(/playlist\.m3u8)' + + # Check if URL contains quality specification + match = re.search(pattern, manifest_url) + + if match: + fixed_url = re.sub( + pattern, + f'_,{STANDARD_QUALITIES}\\2', + manifest_url + ) + return fixed_url + + return manifest_url \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/util/get_license.py b/StreamingCommunity/Api/Service/raiplay/util/get_license.py similarity index 86% rename from StreamingCommunity/Api/Site/raiplay/util/get_license.py rename to StreamingCommunity/Api/Service/raiplay/util/get_license.py index 50010bb4c..0dc191e62 100644 --- a/StreamingCommunity/Api/Site/raiplay/util/get_license.py +++ b/StreamingCommunity/Api/Service/raiplay/util/get_license.py @@ -2,8 +2,7 @@ # Internal utilities -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.headers import get_headers +from StreamingCommunity.Util.http_client import create_client, get_headers def generate_license_url(mpd_id: str): diff --git a/StreamingCommunity/Api/Site/realtime/__init__.py b/StreamingCommunity/Api/Service/realtime/__init__.py similarity index 80% rename from StreamingCommunity/Api/Site/realtime/__init__.py rename to StreamingCommunity/Api/Service/realtime/__init__.py index ed3e8002c..ed4f1143c 100644 --- a/StreamingCommunity/Api/Site/realtime/__init__.py +++ b/StreamingCommunity/Api/Service/realtime/__init__.py @@ -7,12 +7,10 @@ # 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.Api.Template import site_constants, MediaItem, get_select_title -# Logic class +# Logic from .site import title_search, table_show_manager, media_search_manager from .series import download_series @@ -20,25 +18,12 @@ # Variable indice = 8 _useFor = "Serie" -_priority = 0 -_engineDownload = "hls" _deprecate = False msg = Prompt() console = Console() -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - else: - 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. @@ -85,7 +70,11 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ return result # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() # Handle empty input if not actual_search_query: diff --git a/StreamingCommunity/Api/Site/realtime/series.py b/StreamingCommunity/Api/Service/realtime/series.py similarity index 88% rename from StreamingCommunity/Api/Site/realtime/series.py rename to StreamingCommunity/Api/Service/realtime/series.py index 30f2facc2..84b2c4f8c 100644 --- a/StreamingCommunity/Api/Site/realtime/series.py +++ b/StreamingCommunity/Api/Service/realtime/series.py @@ -10,13 +10,9 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager - - -# Logic class -from .util.ScrapeSerie import GetSerieInfo -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -24,12 +20,11 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Player -from StreamingCommunity import HLS_Downloader +# Logic +from .util.ScrapeSerie import GetSerieInfo from .util.get_license import get_bearer_token, get_playback_url @@ -56,11 +51,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.name}[/magenta] ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.{extension_output}" - mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") + mp4_path = os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") # Get hls url bearer_token = get_bearer_token() diff --git a/StreamingCommunity/Api/Site/realtime/site.py b/StreamingCommunity/Api/Service/realtime/site.py similarity index 83% rename from StreamingCommunity/Api/Site/realtime/site.py rename to StreamingCommunity/Api/Service/realtime/site.py index 6f0bb9a85..682540b3e 100644 --- a/StreamingCommunity/Api/Site/realtime/site.py +++ b/StreamingCommunity/Api/Service/realtime/site.py @@ -6,15 +6,11 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Api.Template import site_constants, MediaManager from StreamingCommunity.Util.table import TVShowManager -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager - # Variable console = Console() @@ -43,7 +39,7 @@ def title_search(query: str) -> int: response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 # Collect json data diff --git a/StreamingCommunity/Api/Site/realtime/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/realtime/util/ScrapeSerie.py similarity index 96% rename from StreamingCommunity/Api/Site/realtime/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/realtime/util/ScrapeSerie.py index e083c65d9..f207a0525 100644 --- a/StreamingCommunity/Api/Site/realtime/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/realtime/util/ScrapeSerie.py @@ -4,9 +4,8 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Api.Template.object import SeasonManager class GetSerieInfo: diff --git a/StreamingCommunity/Api/Site/realtime/util/get_license.py b/StreamingCommunity/Api/Service/realtime/util/get_license.py similarity index 83% rename from StreamingCommunity/Api/Site/realtime/util/get_license.py rename to StreamingCommunity/Api/Service/realtime/util/get_license.py index 6d70da15f..cbcd821a9 100644 --- a/StreamingCommunity/Api/Site/realtime/util/get_license.py +++ b/StreamingCommunity/Api/Service/realtime/util/get_license.py @@ -1,10 +1,15 @@ # 26.11.2025 +# External library +from rich.console import Console + # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent, get_headers -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent, get_headers + +# Variable +console = Console() def get_playback_url(video_id: str, bearer_token: str, get_dash: bool, channel: str = "") -> str: @@ -26,8 +31,11 @@ def get_playback_url(video_id: str, bearer_token: str, get_dash: bool, channel: }, 'videoId': video_id, } - response = create_client().post(bearer_token[channel]['endpoint'], headers=headers, json=json_data) + response.raise_for_status() + + if response.status_code == 403: + console.print("[red]Set vpn to IT to download this content.") if not get_dash: return response.json()['data']['attributes']['streaming'][0]['url'] diff --git a/StreamingCommunity/Api/Service/streamingcommunity/__init__.py b/StreamingCommunity/Api/Service/streamingcommunity/__init__.py new file mode 100644 index 000000000..f2ae7a1dd --- /dev/null +++ b/StreamingCommunity/Api/Service/streamingcommunity/__init__.py @@ -0,0 +1,103 @@ +# 21.05.24 + +# External library +from rich.console import Console +from rich.prompt import Prompt + + +# Internal utilities +from StreamingCommunity.Api.Template import site_constants, MediaItem, get_select_title + + +# Logic +from .site import title_search, table_show_manager, media_search_manager +from .film import download_film +from .series import download_series + + +# Variable +indice = 0 +_useFor = "Film_&_Serie" +_deprecate = False + +msg = Prompt() +console = Console() + + +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. Can be None if selection fails. + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + e.g., {'season': season_selection, 'episode': episode_selection} + Returns: + bool: True if processing was successful, False otherwise + """ + if not select_title: + console.print("[yellow]No title selected or selection cancelled.") + return False + + 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) + media_search_manager.clear() + table_show_manager.clear() + return True + + else: + download_film(select_title) + table_show_manager.clear() + return True + + +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. Can be passed from run.py. + If 'back', special handling might occur in get_user_input. + get_onlyDatabase (bool, optional): If True, return only the database search manager object. + direct_item (dict, optional): Direct item to process (bypasses search). + selections (dict, optional): Dictionary containing selection inputs that bypass manual input + for series (season/episode). + """ + if direct_item: + select_title = MediaItem(**direct_item) + result = process_search_result(select_title, selections) + return result + + # Get the user input for the search term + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() + + # Handle empty input + if not actual_search_query: + return False + + # Search on database + len_database = title_search(actual_search_query) + + # 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, len_database) + result = process_search_result(select_title, selections) + return result + + else: + console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") + return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/film.py b/StreamingCommunity/Api/Service/streamingcommunity/film.py similarity index 55% rename from StreamingCommunity/Api/Site/streamingcommunity/film.py rename to StreamingCommunity/Api/Service/streamingcommunity/film.py index 6afaffd81..b27e9e58c 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/film.py +++ b/StreamingCommunity/Api/Service/streamingcommunity/film.py @@ -8,19 +8,12 @@ # Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession +from StreamingCommunity.Util import os_manager, config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem - - -# Player -from StreamingCommunity import HLS_Downloader +# Logic from StreamingCommunity.Api.Player.vixcloud import VideoSource @@ -41,10 +34,10 @@ def download_film(select_title: MediaItem) -> str: - str: output path """ start_message() - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{select_title.name}[/cyan] \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") # Init class - video_source = VideoSource(f"{site_constant.FULL_URL}/it", False, select_title.id) + video_source = VideoSource(f"{site_constants.FULL_URL}/it", False, select_title.id) # Retrieve scws and if available master playlist video_source.get_iframe(select_title.id) @@ -52,12 +45,12 @@ def download_film(select_title: MediaItem) -> str: master_playlist = video_source.get_playlist() if master_playlist is None: - console.print(f"[red]Site: {site_constant.SITE_NAME}, error: No master playlist found[/red]") + console.print(f"[red]Site: {site_constants.SITE_NAME}, error: No master playlist found") return None # Define the filename and path for the downloaded film title_name = f"{os_manager.get_sanitize_file(select_title.name, select_title.date)}.{extension_output}" - mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, title_name.replace(extension_output, "")) # Download the film using the m3u8 playlist, and output filename hls_process = HLS_Downloader( @@ -65,13 +58,6 @@ def download_film(select_title: MediaItem) -> str: output_path=os.path.join(mp4_path, title_name) ).start() - if site_constant.TELEGRAM_BOT: - - # Delete script_id - script_id = TelegramSession.get_session() - if script_id != "unknown": - TelegramSession.deleteScriptId(script_id) - if hls_process['error'] is not None: try: os.remove(hls_process['path']) diff --git a/StreamingCommunity/Api/Site/streamingcommunity/series.py b/StreamingCommunity/Api/Service/streamingcommunity/series.py similarity index 66% rename from StreamingCommunity/Api/Site/streamingcommunity/series.py rename to StreamingCommunity/Api/Service/streamingcommunity/series.py index 936ac2650..5a3611433 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/series.py +++ b/StreamingCommunity/Api/Service/streamingcommunity/series.py @@ -10,14 +10,9 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession, get_bot_instance - - -# Logic class -from .util.ScrapeSerie import GetSerieInfo -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -25,12 +20,11 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Player -from StreamingCommunity import HLS_Downloader +# Logic +from .util.ScrapeSerie import GetSerieInfo from StreamingCommunity.Api.Player.vixcloud import VideoSource @@ -58,25 +52,11 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - # Invio a telegram - bot.send_message( - f"Download in corso\nSerie: {scrape_serie.series_name}\nStagione: {index_season_selected}\nEpisodio: {index_episode_selected}\nTitolo: {obj_episode.name}", - None - ) - - # Get script_id and update it - script_id = TelegramSession.get_session() - if script_id != "unknown": - TelegramSession.updateScriptId(script_id, f"{scrape_serie.series_name} - S{index_season_selected} - E{index_episode_selected} - {obj_episode.name}") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.name}[/magenta] ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.{extension_output}" - mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") + mp4_path = os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") # Retrieve scws and if available master playlist video_source.get_iframe(obj_episode.id) @@ -159,37 +139,16 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis start_message() # Init class - video_source = VideoSource(f"{site_constant.FULL_URL}/it", True, select_season.id) - scrape_serie = GetSerieInfo(f"{site_constant.FULL_URL}/it", select_season.id, select_season.slug) + video_source = VideoSource(f"{site_constants.FULL_URL}/it", True, select_season.id) + scrape_serie = GetSerieInfo(f"{site_constants.FULL_URL}/it", select_season.id, select_season.slug) # Collect information about season scrape_serie.getNumberSeason() seasons_count = len(scrape_serie.seasons_manager) - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - # 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) - - 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 = display_seasons_list(scrape_serie.seasons_manager) - + index_season_selected = display_seasons_list(scrape_serie.seasons_manager) else: index_season_selected = season_selection console.print(f"\n[cyan]Using provided season selection: [yellow]{season_selection}") @@ -210,12 +169,4 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis if len(list_season_select) > 1 or index_season_selected == "*": download_episode(season_number, scrape_serie, video_source, download_all=True) else: - download_episode(season_number, scrape_serie, video_source, download_all=False, episode_selection=episode_selection) - - if site_constant.TELEGRAM_BOT: - bot.send_message("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 + download_episode(season_number, scrape_serie, video_source, download_all=False, episode_selection=episode_selection) \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/site.py b/StreamingCommunity/Api/Service/streamingcommunity/site.py similarity index 65% rename from StreamingCommunity/Api/Site/streamingcommunity/site.py rename to StreamingCommunity/Api/Service/streamingcommunity/site.py index 82ffe682e..fc13808b6 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/site.py +++ b/StreamingCommunity/Api/Service/streamingcommunity/site.py @@ -9,15 +9,9 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Api.Template import site_constants, MediaManager from StreamingCommunity.Util.table import TVShowManager -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager # Variable @@ -36,24 +30,21 @@ def title_search(query: str) -> int: Returns: int: The number of titles found. """ - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - media_search_manager.clear() table_show_manager.clear() try: - response = create_client(headers={'user-agent': get_userAgent()}).get(f"{site_constant.FULL_URL}/it") + response = create_client(headers={'user-agent': get_userAgent()}).get(f"{site_constants.FULL_URL}/it") response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') version = json.loads(soup.find('div', {'id': "app"}).get("data-page"))['version'] except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME} version, request error: {e}") + console.print(f"[red]Site: {site_constants.SITE_NAME} version, request error: {e}") return 0 - search_url = f"{site_constant.FULL_URL}/it/search?q={query}" + search_url = f"{site_constants.FULL_URL}/it/search?q={query}" console.print(f"[cyan]Search url: [yellow]{search_url}") try: @@ -61,15 +52,9 @@ def title_search(query: str) -> int: response.raise_for_status() except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") - if site_constant.TELEGRAM_BOT: - bot.send_message(f"ERRORE\n\nErrore nella richiesta di ricerca:\n\n{e}", None) + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") return 0 - # Prepara le scelte per l'utente - if site_constant.TELEGRAM_BOT: - choices = [] - # Collect json data try: data = response.json().get('props').get('titles') @@ -96,7 +81,7 @@ def title_search(query: str) -> int: image_url = None if filename: - image_url = f"{site_constant.FULL_URL.replace('stream', 'cdn.stream')}/images/{filename}" + image_url = f"{site_constants.FULL_URL.replace('stream', 'cdn.stream')}/images/{filename}" # Extract date: prefer last_air_date, otherwise try translations (last_air_date or release_date) date = dict_title.get('last_air_date') @@ -114,20 +99,9 @@ def title_search(query: str) -> int: 'date': date, 'image': image_url }) - - if site_constant.TELEGRAM_BOT: - choice_date = date if date else "N/A" - choice_text = f"{i} - {dict_title.get('name')} ({dict_title.get('type')}) - {choice_date}" - choices.append(choice_text) except Exception as e: print(f"Error parsing a film entry: {e}") - if site_constant.TELEGRAM_BOT: - bot.send_message(f"ERRORE\n\nErrore nell'analisi del film:\n\n{e}", None) - - if site_constant.TELEGRAM_BOT: - if choices: - bot.send_message("Lista dei risultati:", choices) - + # Return the number of titles found return media_search_manager.get_length() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/streamingcommunity/util/ScrapeSerie.py similarity index 96% rename from StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py rename to StreamingCommunity/Api/Service/streamingcommunity/util/ScrapeSerie.py index bc215d232..6a7882ee8 100644 --- a/StreamingCommunity/Api/Site/streamingcommunity/util/ScrapeSerie.py +++ b/StreamingCommunity/Api/Service/streamingcommunity/util/ScrapeSerie.py @@ -9,9 +9,8 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager +from StreamingCommunity.Util.http_client import create_client, get_headers +from StreamingCommunity.Api.Template.object import SeasonManager class GetSerieInfo: diff --git a/StreamingCommunity/Api/Site/plutotv/__init__.py b/StreamingCommunity/Api/Service/tubitv/__init__.py similarity index 80% rename from StreamingCommunity/Api/Site/plutotv/__init__.py rename to StreamingCommunity/Api/Service/tubitv/__init__.py index 229dc0c17..d4bda8649 100644 --- a/StreamingCommunity/Api/Site/plutotv/__init__.py +++ b/StreamingCommunity/Api/Service/tubitv/__init__.py @@ -1,4 +1,4 @@ -# 26.11.2025 +# 16.12.25 # External library @@ -7,38 +7,24 @@ # 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.Api.Template import site_constants, MediaItem, get_select_title -# Logic class +# Logic from .site import title_search, table_show_manager, media_search_manager from .series import download_series +from .film import download_film # Variable indice = 11 _useFor = "Serie" -_priority = 0 -_engineDownload = "dash" -_deprecate = True +_deprecate = False msg = Prompt() console = Console() -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - else: - 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. @@ -65,6 +51,11 @@ def process_search_result(select_title, selections=None): media_search_manager.clear() table_show_manager.clear() return True + + else: + download_film(select_title) + table_show_manager.clear() + return True def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): @@ -85,7 +76,11 @@ def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_ return result # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) + actual_search_query = None + if string_to_search is not None: + actual_search_query = string_to_search.strip() + else: + actual_search_query = msg.ask(f"\n[purple]Insert a word to search in [green]{site_constants.SITE_NAME}").strip() # Handle empty input if not actual_search_query: diff --git a/StreamingCommunity/Api/Service/tubitv/film.py b/StreamingCommunity/Api/Service/tubitv/film.py new file mode 100644 index 000000000..a4bc22ec0 --- /dev/null +++ b/StreamingCommunity/Api/Service/tubitv/film.py @@ -0,0 +1,81 @@ +# 16.12.25 + +import os +import re +from typing import Tuple + + +# External library +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util import os_manager, config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader + + +# Logic +from .util.get_license import get_bearer_token, get_playback_url + + +# Variable +console = Console() +extension_output = config_manager.get("M3U8_CONVERSION", "extension") + + +def extract_content_id(url: str) -> str: + """Extract content ID from Tubi TV URL""" + # URL format: https://tubitv.com/movies/{content_id}/{slug} + match = re.search(r'/movies/(\d+)/', url) + if match: + return match.group(1) + return None + + +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"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{select_title.name} \n") + + # Extract content ID from URL + content_id = extract_content_id(select_title.url) + if not content_id: + console.print("[red]Error: Could not extract content ID from URL") + return None, True + + # Get bearer token + try: + bearer_token = get_bearer_token() + except Exception as e: + console.print(f"[red]Error getting bearer token: {e}") + return None, True + + # Get master playlist URL + try: + master_playlist, license_url = get_playback_url(content_id, bearer_token) + except Exception as e: + console.print(f"[red]Error getting playback URL: {e}") + return None, True + + # Define the filename and path for the downloaded film + mp4_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output + mp4_path = os.path.join(site_constants.MOVIE_FOLDER, mp4_name.replace(extension_output, "")) + + # HLS Download + r_proc = HLS_Downloader( + m3u8_url=master_playlist, + output_path=os.path.join(mp4_path, mp4_name), + license_url=license_url + ).start() + + return r_proc['path'], r_proc['stopped'] \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/plutotv/series.py b/StreamingCommunity/Api/Service/tubitv/series.py similarity index 69% rename from StreamingCommunity/Api/Site/plutotv/series.py rename to StreamingCommunity/Api/Service/tubitv/series.py index bf54a0d7b..f718a7a21 100644 --- a/StreamingCommunity/Api/Site/plutotv/series.py +++ b/StreamingCommunity/Api/Service/tubitv/series.py @@ -1,4 +1,4 @@ -# 26.11.2025 +# 16.12.25 import os from typing import Tuple @@ -10,14 +10,9 @@ # Internal utilities -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.config_json import config_manager - - -# Logic class -from .util.ScrapeSerie import GetSerieInfo -from StreamingCommunity.Api.Template.Util import ( +from StreamingCommunity.Util import config_manager, start_message +from StreamingCommunity.Api.Template import site_constants, MediaItem +from StreamingCommunity.Api.Template.episode_manager import ( manage_selection, map_episode_title, validate_selection, @@ -25,13 +20,12 @@ display_episodes_list, display_seasons_list ) -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaItem +from StreamingCommunity.Lib.HLS import HLS_Downloader -# Player -from StreamingCommunity import DASH_Downloader -from .util.get_license import get_playback_url_episode, get_bearer_token +# Logic +from .util.ScrapeSerie import GetSerieInfo +from .util.get_license import get_bearer_token, get_playback_url # Variable @@ -40,7 +34,7 @@ extension_output = config_manager.get("M3U8_CONVERSION", "extension") -def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo) -> Tuple[str,bool]: +def download_video(index_season_selected: int, index_episode_selected: int, scrape_serie: GetSerieInfo, bearer_token: str) -> Tuple[str, bool]: """ Downloads a specific episode from the specified season. @@ -48,6 +42,7 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra - index_season_selected (int): Season number - index_episode_selected (int): Episode index - scrape_serie (GetSerieInfo): Scraper object with series information + - bearer_token (str): Bearer token for authentication Returns: - str: Path to downloaded file @@ -57,47 +52,43 @@ def download_video(index_season_selected: int, index_episode_selected: int, scra # Get episode information obj_episode = scrape_serie.selectEpisode(index_season_selected, index_episode_selected-1) - console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] \\ [bold magenta]{obj_episode.name}[/bold magenta] ([cyan]S{index_season_selected}E{index_episode_selected}[/cyan]) \n") + console.print(f"\n[yellow]Download: [red]{site_constants.SITE_NAME} β†’ [cyan]{scrape_serie.series_name} \\ [magenta]{obj_episode.name}[/magenta] ([cyan]S{index_season_selected}E{index_episode_selected}) \n") # Define filename and path for the downloaded video mp4_name = f"{map_episode_title(scrape_serie.series_name, index_season_selected, index_episode_selected, obj_episode.name)}.{extension_output}" - mp4_path = os.path.join(site_constant.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") - - # Generate headers - headers = get_headers() - headers_2 = get_headers() - headers['authorization'] = f'Bearer {get_bearer_token()}' - - # Start download process - dash_process = DASH_Downloader( - license_url="https://service-concierge.clusters.pluto.tv/v1/wv/alt", - mpd_url=get_playback_url_episode(obj_episode.id) + f"?jwt={get_bearer_token()}", + mp4_path = os.path.join(site_constants.SERIES_FOLDER, scrape_serie.series_name, f"S{index_season_selected}") + + # Get master playlist URL + try: + master_playlist, license_url = get_playback_url(obj_episode.id, bearer_token) + except Exception as e: + console.print(f"[red]Error getting playback URL: {e}") + return None, True + + # Download the episode + hls_process = HLS_Downloader( + m3u8_url=master_playlist, output_path=os.path.join(mp4_path, mp4_name), - ) - dash_process.parse_manifest(custom_headers=headers) - - if dash_process.download_and_decrypt(custom_headers=headers_2): - dash_process.finalize_output() - - # Get final output path and status - r_proc = dash_process.get_status() + license_url=license_url + ).start() - if r_proc['error'] is not None: + if hls_process['error'] is not None: try: - os.remove(r_proc['path']) + os.remove(hls_process['path']) except Exception: pass - return r_proc['path'], r_proc['stopped'] + return hls_process['path'], hls_process['stopped'] -def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, download_all: bool = False, episode_selection: str = None) -> None: +def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, bearer_token: str, 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 + - bearer_token (str): Bearer token for authentication - download_all (bool): Whether to download all episodes - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ @@ -112,7 +103,7 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, dow if download_all: # 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) + path, stopped = download_video(index_season_selected, i_episode, scrape_serie, bearer_token) if stopped: break @@ -133,7 +124,7 @@ def download_episode(index_season_selected: int, scrape_serie: GetSerieInfo, dow # Download selected episodes if not stopped for i_episode in list_episode_select: - path, stopped = download_video(index_season_selected, i_episode, scrape_serie) + path, stopped = download_video(index_season_selected, i_episode, scrape_serie, bearer_token) if stopped: break @@ -149,9 +140,10 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis - episode_selection (str, optional): Pre-defined episode selection that bypasses manual input """ start_message() + bearer_token = get_bearer_token() # Init class - scrape_serie = GetSerieInfo(select_season.url) + scrape_serie = GetSerieInfo(select_season.url, bearer_token, select_season.name) # Collect information about season scrape_serie.getNumberSeason() @@ -179,6 +171,6 @@ def download_series(select_season: MediaItem, season_selection: str = None, epis season_number = season.number if len(list_season_select) > 1 or index_season_selected == "*": - download_episode(season_number, scrape_serie, download_all=True) + download_episode(season_number, scrape_serie, bearer_token, download_all=True) else: - download_episode(season_number, scrape_serie, download_all=False, episode_selection=episode_selection) \ No newline at end of file + download_episode(season_number, scrape_serie, bearer_token, download_all=False, episode_selection=episode_selection) \ No newline at end of file diff --git a/StreamingCommunity/Api/Service/tubitv/site.py b/StreamingCommunity/Api/Service/tubitv/site.py new file mode 100644 index 000000000..b46c83b64 --- /dev/null +++ b/StreamingCommunity/Api/Service/tubitv/site.py @@ -0,0 +1,131 @@ +# 16.12.25 + +import re + + +# External libraries +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Api.Template import site_constants, MediaManager +from StreamingCommunity.Util.table import TVShowManager + + +# Logic +from .util.get_license import get_bearer_token + + +# Variable +console = Console() +media_search_manager = MediaManager() +table_show_manager = TVShowManager() + + + +def title_to_slug(title): + """Convert a title to a URL-friendly slug""" + slug = title.lower() + slug = re.sub(r'[^a-z0-9\s-]', '', slug) + slug = re.sub(r'\s+', '-', slug) + slug = slug.strip('-') + return slug + + +def affinity_score(element, keyword): + """Calculate relevance score for search results""" + score = 0 + title = element.get("title", "").lower() + description = element.get("description", "").lower() + tags = [t.lower() for t in element.get("tags", [])] + + if keyword.lower() in title: + score += 10 + if keyword.lower() in description: + score += 5 + if keyword.lower() in tags: + score += 3 + + return score + + +def title_search(query: str) -> int: + """ + Search for titles on Tubi TV 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() + + try: + headers = { + 'authorization': f"Bearer {get_bearer_token()}", + 'user-agent': get_userAgent(), + } + + search_url = 'https://search.production-public.tubi.io/api/v2/search' + console.print(f"[cyan]Search url: [yellow]{search_url}") + + params = {'search': query} + response = create_client(headers=headers).get(search_url, params=params) + response.raise_for_status() + + except Exception as e: + console.print(f"[red]Site: {site_constants.SITE_NAME}, request search error: {e}") + return 0 + + # Collect json data + try: + contents_dict = response.json().get('contents', {}) + elements = list(contents_dict.values()) + + # Sort by affinity score + elements_sorted = sorted( + elements, + key=lambda x: affinity_score(x, query), + reverse=True + ) + + except Exception as e: + console.log(f"Error parsing JSON response: {e}") + return 0 + + # Process results + for element in elements_sorted[:20]: + try: + type_content = "tv" if element.get("type", "") == "s" else "movie" + year = element.get("year", "") + content_id = element.get("id", "") + title = element.get("title", "") + + # Build URL + if type_content == "tv": + url = f"https://tubitv.com/series/{content_id}/{title_to_slug(title)}" + else: + url = f"https://tubitv.com/movies/{content_id}/{title_to_slug(title)}" + + # Get thumbnail + thumbnail = "" + if "thumbnails" in element and element["thumbnails"]: + thumbnail = element["thumbnails"][0] + + media_search_manager.add_media({ + 'name': title, + 'type': type_content, + 'date': str(year) if year else "", + 'image': thumbnail, + 'url': url, + }) + + except Exception as e: + console.print(f"[yellow]Error parsing a title entry: {e}") + continue + + # Return the number of titles found + return media_search_manager.get_length() \ No newline at end of file diff --git a/StreamingCommunity/Api/Service/tubitv/util/ScrapeSerie.py b/StreamingCommunity/Api/Service/tubitv/util/ScrapeSerie.py new file mode 100644 index 000000000..5976a48d3 --- /dev/null +++ b/StreamingCommunity/Api/Service/tubitv/util/ScrapeSerie.py @@ -0,0 +1,187 @@ +# 16.12.25 + +import re +import logging + + +# Internal utilities +from StreamingCommunity.Util.http_client import create_client +from StreamingCommunity.Api.Template.object import SeasonManager + + +def extract_content_id(url: str) -> str: + """Extract content ID from Tubi TV URL""" + # URL format: https://tubitv.com/series/{content_id}/{slug} + match = re.search(r'/series/(\d+)/', url) + if match: + return match.group(1) + return None + + +class GetSerieInfo: + def __init__(self, url, bearer_token=None, series_name=None): + """ + Initialize the GetSerieInfo class for scraping Tubi TV series information. + + Args: + - url (str): The URL of the series + - bearer_token (str, optional): Bearer token for authentication + """ + self.url = url + self.content_id = extract_content_id(url) + self.bearer_token = bearer_token + self.series_name = series_name + self.seasons_manager = SeasonManager() + self.all_episodes_by_season = {} + + # Setup headers + self.headers = { + 'accept': '*/*', + 'accept-language': 'en-US', + 'content-type': 'application/json', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', + } + + if self.bearer_token: + self.headers['authorization'] = f"Bearer {self.bearer_token}" + + def collect_info_title(self) -> None: + """ + Retrieve general information about the TV series from Tubi TV. + """ + try: + # Get series info and total seasons + response = create_client(headers=self.headers).get( + f'https://content-cdn.production-public.tubi.io/cms/series/{self.content_id}/episodes' + ) + response.raise_for_status() + + json_data = response.json() + episodes_by_season = json_data.get('episodes_by_season', {}) + + if not episodes_by_season: + logging.warning("No seasons found in response") + return + + # Store episodes by season + self.all_episodes_by_season = episodes_by_season + + # Create seasons in SeasonManager + for season_num in sorted(episodes_by_season.keys(), key=int): + season_data = { + 'id': f"season-{season_num}", + 'number': int(season_num), + 'name': f"Season {season_num}", + 'slug': f"season-{season_num}", + } + self.seasons_manager.add_season(season_data) + + except Exception as e: + logging.error(f"Error collecting series info: {e}") + raise + + def collect_info_season(self, number_season: int) -> None: + """ + Retrieve episode information for a specific season. + + Args: + number_season (int): Season number to fetch episodes for + """ + try: + season = self.seasons_manager.get_season_by_number(number_season) + if not season: + logging.error(f"Season {number_season} not found") + return + + params = { + 'app_id': 'tubitv', + 'platform': 'web', + 'content_id': self.content_id, + 'pagination[season]': str(number_season), + } + + response = create_client(headers=self.headers).get( + 'https://content-cdn.production-public.tubi.io/api/v2/content', + params=params + ) + response.raise_for_status() + json_data = response.json() + + # Extract episodes from children + episodes = [] + for season_data in json_data.get('children', []): + for episode in season_data.get('children', []): + episodes.append(episode) + + if not episodes: + logging.warning(f"No episodes found for season {number_season}") + return + + # Sort episodes by episode number + episodes.sort(key=lambda x: x.get('episode_number', 0)) + + # Transform episodes to match the expected format + for episode in episodes: + + # Get thumbnail + thumbnail = "" + thumbnails = episode.get('thumbnails', []) + if thumbnails and len(thumbnails) > 0: + thumbnail = thumbnails[0] + + # Convert duration from seconds to minutes + duration_seconds = episode.get('duration', 0) + duration_minutes = round(duration_seconds / 60) if duration_seconds else 0 + + episode_data = { + 'id': episode.get('id'), + 'name': episode.get('title', f"Episode {episode.get('episode_number')}"), + 'number': episode.get('episode_number'), + 'image': thumbnail, + 'year': episode.get('year'), + 'duration': duration_minutes, + 'needs_login': episode.get('needs_login', False), + 'country': episode.get('country'), + 'imdb_id': episode.get('imdb_id'), + } + season.episodes.add(episode_data) + + 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/Service/tubitv/util/get_license.py b/StreamingCommunity/Api/Service/tubitv/util/get_license.py new file mode 100644 index 000000000..7ab089aa5 --- /dev/null +++ b/StreamingCommunity/Api/Service/tubitv/util/get_license.py @@ -0,0 +1,96 @@ +# 16.12.25 + +import uuid +from typing import Tuple, Optional + + +# Internal utilities +from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Util.http_client import create_client, get_userAgent, get_headers + + +# Variable +config = config_manager.get_dict("SITE_LOGIN", "tubi") + + +def generate_device_id(): + """Generate a unique device ID""" + return str(uuid.uuid4()) + + +def get_bearer_token(): + """ + Get the Bearer token required for Tubi TV authentication. + + Returns: + str: Bearer token + """ + if not config.get('email') or not config.get('password'): + raise Exception("Email or Password not set in configuration.") + + json_data = { + 'type': 'email', + 'platform': 'web', + 'device_id': generate_device_id(), + 'credentials': { + 'email': str(config.get('email')).strip(), + 'password': str(config.get('password')).strip() + }, + } + + response = create_client(headers=get_headers()).post( + 'https://account.production-public.tubi.io/user/login', + json=json_data + ) + + if response.status_code == 503: + raise Exception("Service Unavailable: Set VPN to America.") + + return response.json()['access_token'] + + +def get_playback_url(content_id: str, bearer_token: str) -> Tuple[str, Optional[str]]: + """ + Get the playback URL (HLS) and license URL for a given content ID. + + Parameters: + - content_id (str): ID of the video content + - bearer_token (str): Bearer token for authentication + + Returns: + - Tuple[str, Optional[str]]: (master_playlist_url, license_url) + """ + headers = { + 'authorization': f"Bearer {bearer_token}", + 'user-agent': get_userAgent(), + } + + params = { + 'content_id': content_id, + 'limit_resolutions[]': [ + 'h264_1080p', + 'h265_1080p', + ], + 'video_resources[]': [ + 'hlsv6_widevine_nonclearlead', + 'hlsv6_playready_psshv0', + 'hlsv6', + ] + } + + response = create_client(headers=headers).get( + 'https://content-cdn.production-public.tubi.io/api/v2/content', + params=params + ) + + json_data = response.json() + + # Get master playlist URL + master_playlist_url = json_data['video_resources'][0]['manifest']['url'] + + # Get license URL if available + license_url = None + if 'license_server' in json_data['video_resources'][0]: + license_url = json_data['video_resources'][0]['license_server']['url'] + + return master_playlist_url, license_url \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/altadefinizione/__init__.py b/StreamingCommunity/Api/Site/altadefinizione/__init__.py deleted file mode 100644 index 7f3c97d50..000000000 --- a/StreamingCommunity/Api/Site/altadefinizione/__init__.py +++ /dev/null @@ -1,162 +0,0 @@ -# 16.03.25 - -import sys -import subprocess - - -# 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 .site import title_search, table_show_manager, media_search_manager -from .film import download_film -from .series import download_series - - -# Variable -indice = 2 -_useFor = "Film_&_Serie" -_priority = 0 -_engineDownload = "hls" -_deprecate = False - -msg = Prompt() -console = Console() - - -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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} - - Returns: - bool: True if processing was successful, False otherwise - """ - if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") - return False - - 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) - media_search_manager.clear() - table_show_manager.clear() - return True - - else: - download_film(select_title) - table_show_manager.clear() - return True - - -# 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. - - 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} - """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if direct_item: - select_title = MediaItem(**direct_item) - result = process_search_result(select_title, selections) - return result - - # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - - # Handle empty input - if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) - return False - - # Search on database - len_database = title_search(actual_search_query) - - # 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, len_database) - result = process_search_result(select_title, selections) - return result - - else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") - return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/__init__.py b/StreamingCommunity/Api/Site/animeunity/__init__.py deleted file mode 100644 index ab55ae577..000000000 --- a/StreamingCommunity/Api/Site/animeunity/__init__.py +++ /dev/null @@ -1,160 +0,0 @@ -# 21.05.24 - -import sys -import subprocess - - -# 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 .site import title_search, media_search_manager, table_show_manager -from .film import download_film -from .serie import download_series - - -# Variable -indice = 1 -_useFor = "Anime" -_priority = 0 -_engineDownload = "mp4" -_deprecate = False - -msg = Prompt() -console = Console() - - -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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} - - Returns: - bool: True if processing was successful, False otherwise - """ - if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") - return False - - if select_title.type == 'Movie': - download_film(select_title) - return True - - 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) - media_search_manager.clear() - table_show_manager.clear() - return True - - -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} - """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if direct_item: - select_title = MediaItem(**direct_item) - result = process_search_result(select_title, selections) - return result - - # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - - # Handle empty input - if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) - return False - - # Search on database - len_database = title_search(actual_search_query) - - # 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, len_database) - result = process_search_result(select_title, selections) - return result - - else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") - return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/animeunity/serie.py b/StreamingCommunity/Api/Site/animeunity/serie.py deleted file mode 100644 index 69aeeee91..000000000 --- a/StreamingCommunity/Api/Site/animeunity/serie.py +++ /dev/null @@ -1,153 +0,0 @@ -# 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.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 import MP4_downloader -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"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] β†’ [cyan]{scrape_serie.series_name}[/cyan] ([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"\n[green]Episodes count:[/green] [red]{episoded_count}[/red]") - - # Telegram bot integration - if episode_selection is None: - if site_constant.TELEGRAM_BOT: - console.print("\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("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/animeworld/__init__.py b/StreamingCommunity/Api/Site/animeworld/__init__.py deleted file mode 100644 index 387aa760c..000000000 --- a/StreamingCommunity/Api/Site/animeworld/__init__.py +++ /dev/null @@ -1,158 +0,0 @@ -# 21.03.25 - -import sys -import subprocess - - -# 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 .site import title_search, media_search_manager, table_show_manager -from .serie import download_series -from .film import download_film - - -# Variable -indice = 6 -_useFor = "Anime" -_priority = 0 -_engineDownload = "mp4" -_deprecate = False - -msg = Prompt() -console = Console() - - -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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} - - Returns: - bool: True if processing was successful, False otherwise - """ - if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") - return False - - if select_title.type == "TV": - episode_selection = None - if selections: - episode_selection = selections.get('episode') - - download_series(select_title, episode_selection) - media_search_manager.clear() - table_show_manager.clear() - return True - - else: - download_film(select_title) - table_show_manager.clear() - return True - - -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} - """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if direct_item: - select_title = MediaItem(**direct_item) - result = process_search_result(select_title, selections) - return result - - # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - - # Handle empty input - if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) - return False - - # Search on database - len_database = title_search(actual_search_query) - - # 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, len_database) - result = process_search_result(select_title, selections) - return result - - else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") - return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/crunchyroll/__init__.py b/StreamingCommunity/Api/Site/crunchyroll/__init__.py deleted file mode 100644 index b568b7771..000000000 --- a/StreamingCommunity/Api/Site/crunchyroll/__init__.py +++ /dev/null @@ -1,162 +0,0 @@ -# 16.03.25 - -import sys -import subprocess - - -# 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 .site import title_search, table_show_manager, media_search_manager -from .film import download_film -from .series import download_series - - -# Variable -indice = 7 -_useFor = "Anime" -_priority = 0 -_engineDownload = "dash" -_deprecate = False - -msg = Prompt() -console = Console() - - -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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} - - Returns: - bool: True if processing was successful, False otherwise - """ - if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") - return False - - 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) - media_search_manager.clear() - table_show_manager.clear() - return True - - else: - download_film(select_title) - table_show_manager.clear() - return True - - -# 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. - - 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} - """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if direct_item: - select_title = MediaItem(**direct_item) - result = process_search_result(select_title, selections) - return result - - # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - - # Handle empty input - if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) - return False - - # Search on database - len_database = title_search(actual_search_query) - - # 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, len_database) - result = process_search_result(select_title, selections) - return result - - else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") - return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/mediasetinfinity/__init__.py b/StreamingCommunity/Api/Site/mediasetinfinity/__init__.py deleted file mode 100644 index 587222481..000000000 --- a/StreamingCommunity/Api/Site/mediasetinfinity/__init__.py +++ /dev/null @@ -1,161 +0,0 @@ -# 21.05.24 - -import sys -import subprocess - - -# 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 .site import title_search, table_show_manager, media_search_manager -from .series import download_series -from .film import download_film - - -# Variable -indice = 3 -_useFor = "Film_&_Serie" -_priority = 0 -_engineDownload = "dash" -_deprecate = False - -msg = Prompt() -console = Console() - - -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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} - - Returns: - bool: True if processing was successful, False otherwise - """ - if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") - return False - - 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) - media_search_manager.clear() - table_show_manager.clear() - return True - - else: - download_film(select_title) - table_show_manager.clear() - return True - - -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} - """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if direct_item: - select_title = MediaItem(**direct_item) - result = process_search_result(select_title, selections) - return result - - # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - - # Handle empty input - if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) - return False - - # Search on database - len_database = title_search(actual_search_query) - - # 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, len_database) - result = process_search_result(select_title, selections) - return result - - else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") - return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/plutotv/site.py b/StreamingCommunity/Api/Site/plutotv/site.py deleted file mode 100644 index cdd2748ce..000000000 --- a/StreamingCommunity/Api/Site/plutotv/site.py +++ /dev/null @@ -1,76 +0,0 @@ -# 26.11.2025 - - -# External libraries -from rich.console import Console - - -# Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.table import TVShowManager - - -# Logic class -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.Api.Template.Class.SearchType import MediaManager -from .util.get_license import get_bearer_token - - -# Variable -console = Console() -media_search_manager = MediaManager() -table_show_manager = TVShowManager() - - -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://service-media-search.clusters.pluto.tv/v1/search?q={query}&limit=10" - console.print(f"[cyan]Search url: [yellow]{search_url}") - - try: - response = create_client(headers={'user-agent': get_userAgent(), 'Authorization': f"Bearer {get_bearer_token()}"}).get(search_url) - response.raise_for_status() - - except Exception as e: - console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") - return 0 - - # Collect json data - try: - data = response.json().get('data') - except Exception as e: - console.log(f"Error parsing JSON response: {e}") - return 0 - - for dict_title in data: - try: - if dict_title.get('type') == 'channel': - continue - - define_type = 'tv' if dict_title.get('type') == 'series' else dict_title.get('type') - - media_search_manager.add_media({ - 'id': dict_title.get('id'), - 'name': dict_title.get('name'), - 'type': define_type, - 'image': None, - 'url': f"https://service-vod.clusters.pluto.tv/v4/vod/{dict_title.get('type')}/{dict_title.get('id')}" - }) - - except Exception as e: - print(f"Error parsing a film entry: {e}") - - # Return the number of titles found - return media_search_manager.get_length() \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/plutotv/util/ScrapeSerie.py b/StreamingCommunity/Api/Site/plutotv/util/ScrapeSerie.py deleted file mode 100644 index 2a54a729c..000000000 --- a/StreamingCommunity/Api/Site/plutotv/util/ScrapeSerie.py +++ /dev/null @@ -1,162 +0,0 @@ -# 26.11.2025 - -import logging - -# Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager - -from .get_license import get_bearer_token - - -class GetSerieInfo: - def __init__(self, url): - """ - Initialize the GetSerieInfo class for scraping TV series information. - - Args: - - url (str): The URL of the streaming site. - """ - self.url = url + "/seasons" - self.headers = get_headers() - self.series_name = None - self.seasons_manager = SeasonManager() - self.seasons_data = {} - - def collect_info_title(self) -> None: - """ - Retrieve general information about the TV series from the streaming site. - """ - try: - # Add Bearer token to headers - bearer_token = get_bearer_token() - self.headers['authorization'] = f'Bearer {bearer_token}' - - response = create_client(headers=self.headers).get(self.url) - response.raise_for_status() - - # Parse JSON response - json_response = response.json() - self.series_name = json_response.get('name', 'Unknown Series') - seasons_array = json_response.get('seasons', []) - - if not seasons_array: - logging.warning("No seasons found in JSON response") - return - - # Process each season in the array - for season_obj in seasons_array: - season_number = season_obj.get('number') - if season_number is None: - logging.warning("Season without number found, skipping") - continue - - # Store season data indexed by season number - self.seasons_data[str(season_number)] = season_obj - - # Build season structure for SeasonManager - season_info = { - 'id': f"season-{season_number}", - 'number': season_number, - 'name': f"Season {season_number}", - 'slug': f"season-{season_number}", - } - self.seasons_manager.add_season(season_info) - - except Exception as e: - logging.error(f"Error collecting series info: {e}") - raise - - def collect_info_season(self, number_season: int) -> None: - """ - Retrieve episode information for a specific season. - - Args: - number_season (int): Season number to fetch episodes for - - Raises: - Exception: If there's an error fetching episode information - """ - try: - season = self.seasons_manager.get_season_by_number(number_season) - if not season: - logging.error(f"Season {number_season} not found") - return - - # Get episodes for this season from stored data - season_key = str(number_season) - season_data = self.seasons_data.get(season_key, {}) - episodes = season_data.get('episodes', []) - - if not episodes: - logging.warning(f"No episodes found for season {number_season}") - return - - # Sort episodes by episode number in ascending order - episodes.sort(key=lambda x: x.get('number', 0), reverse=False) - - # Transform episodes to match the expected format - for episode in episodes: - duration_ms = episode.get('duration', 0) - duration_minutes = round(duration_ms / 1000 / 60) if duration_ms else 0 - - episode_data = { - 'id': episode.get('_id'), - 'number': episode.get('number'), - 'name': episode.get('name', f"Episode {episode.get('number')}"), - 'description': episode.get('description', ''), - 'duration': duration_minutes, - 'slug': episode.get('slug', '') - } - - # Add episode to the season's episode manager - season.episodes.add(episode_data) - - 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] - - def get_series_name(self) -> str: - """ - Get the name of the series. - """ - if not self.series_name: - self.collect_info_title() - return self.series_name \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/plutotv/util/get_license.py b/StreamingCommunity/Api/Site/plutotv/util/get_license.py deleted file mode 100644 index 6fcdde5a4..000000000 --- a/StreamingCommunity/Api/Site/plutotv/util/get_license.py +++ /dev/null @@ -1,40 +0,0 @@ -# 26.11.2025 - -import uuid -import random - - -# Internal utilities -from StreamingCommunity.Util.headers import get_headers -from StreamingCommunity.Util.http_client import create_client - - -def generate_params(): - """Generate all params automatically""" - device_makes = ['opera', 'chrome', 'firefox', 'safari', 'edge'] - - return { - 'appName': 'web', - 'appVersion': str(random.randint(100, 999)), - 'deviceVersion': str(random.randint(100, 999)), - 'deviceModel': 'web', - 'deviceMake': random.choice(device_makes), - 'deviceType': 'web', - 'clientID': str(uuid.uuid4()), - 'clientModelNumber': f"{random.randint(1, 9)}.{random.randint(0, 9)}.{random.randint(0, 9)}", - 'channelID': ''.join(random.choice('0123456789abcdef') for _ in range(24)) - } - -def get_bearer_token(): - """ - Get the Bearer token required for authentication. - - Returns: - str: Token Bearer - """ - response = create_client(headers=get_headers()).get('https://boot.pluto.tv/v4/start', params=generate_params()) - return response.json()['sessionToken'] - - -def get_playback_url_episode(id_episode): - return f"https://cfd-v4-service-stitcher-dash-use1-1.prd.pluto.tv/v2/stitch/dash/episode/{id_episode}/main.mpd" \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/raiplay/__init__.py b/StreamingCommunity/Api/Site/raiplay/__init__.py deleted file mode 100644 index a462b74f2..000000000 --- a/StreamingCommunity/Api/Site/raiplay/__init__.py +++ /dev/null @@ -1,161 +0,0 @@ -# 21.05.24 - -import sys -import subprocess - - -# 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 .site import title_search, table_show_manager, media_search_manager -from .series import download_series -from .film import download_film - - -# Variable -indice = 5 -_useFor = "Film_&_Serie" -_priority = 0 -_engineDownload = "hls_dash" -_deprecate = False - -msg = Prompt() -console = Console() - - -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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} - - Returns: - bool: True if processing was successful, False otherwise - """ - if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") - return False - - 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) - media_search_manager.clear() - table_show_manager.clear() - return True - - else: - download_film(select_title) - table_show_manager.clear() - return True - - -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} - """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if direct_item: - select_title = MediaItem(**direct_item) - result = process_search_result(select_title, selections) - return result - - # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - - # Handle empty input - if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) - return False - - # Search on database - len_database = title_search(actual_search_query) - - # 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, len_database) - result = process_search_result(select_title, selections) - return result - - else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") - return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Site/streamingcommunity/__init__.py b/StreamingCommunity/Api/Site/streamingcommunity/__init__.py deleted file mode 100644 index a19b705bf..000000000 --- a/StreamingCommunity/Api/Site/streamingcommunity/__init__.py +++ /dev/null @@ -1,160 +0,0 @@ -# 21.05.24 - -import sys -import subprocess - - -# 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 .site import title_search, table_show_manager, media_search_manager -from .film import download_film -from .series import download_series - - -# Variable -indice = 0 -_useFor = "Film_&_Serie" -_priority = 0 -_engineDownload = "hls" -_deprecate = False - -msg = Prompt() -console = Console() - - -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 provided, it's returned directly (after stripping). - """ - if string_to_search is not None: - return string_to_search.strip() - - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - user_response = bot.ask( - "key_search", # Request type - "Enter the search term\nor type 'back' to return to the menu: ", - None - ) - - if user_response is None: - bot.send_message("Timeout: No search term entered.", None) - return None - - if user_response.lower() == 'back': - bot.send_message("Returning to the main menu...", None) - - try: - # Restart the script - subprocess.Popen([sys.executable] + sys.argv) - sys.exit() - - except Exception as e: - bot.send_message(f"Error during restart attempt: {e}", None) - return None # Return None if restart fails - - return user_response.strip() - - else: - 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. Can be None if selection fails. - selections (dict, optional): Dictionary containing selection inputs that bypass manual input - e.g., {'season': season_selection, 'episode': episode_selection} - Returns: - bool: True if processing was successful, False otherwise - """ - if not select_title: - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - bot.send_message("No title selected or selection cancelled.", None) - else: - console.print("[yellow]No title selected or selection cancelled.") - return False - - 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) - media_search_manager.clear() - table_show_manager.clear() - return True - - else: - download_film(select_title) - table_show_manager.clear() - return True - - -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. Can be passed from run.py. - If 'back', special handling might occur in get_user_input. - get_onlyDatabase (bool, optional): If True, return only the database search manager object. - direct_item (dict, optional): Direct item to process (bypasses search). - selections (dict, optional): Dictionary containing selection inputs that bypass manual input - for series (season/episode). - """ - bot = None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - - if direct_item: - select_title = MediaItem(**direct_item) - result = process_search_result(select_title, selections) - return result - - # Get the user input for the search term - actual_search_query = get_user_input(string_to_search) - - # Handle empty input - if not actual_search_query: - if bot: - if actual_search_query is None: - bot.send_message("Search term not provided or operation cancelled. Returning.", None) - return False - - # Search on database - len_database = title_search(actual_search_query) - - # 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, len_database) - result = process_search_result(select_title, selections) - return result - - else: - if bot: - bot.send_message(f"No results found for: '{actual_search_query}'", None) - else: - console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") - return False \ No newline at end of file diff --git a/StreamingCommunity/Api/Template/Class/SearchType.py b/StreamingCommunity/Api/Template/Class/SearchType.py deleted file mode 100644 index 8de60cc65..000000000 --- a/StreamingCommunity/Api/Template/Class/SearchType.py +++ /dev/null @@ -1,100 +0,0 @@ -# 07.07.24 - -from typing import List, TypedDict - - -class MediaItemData(TypedDict, total=False): - id: int # GENERAL - name: str # GENERAL - type: str # GENERAL - url: str # GENERAL - size: str # GENERAL - score: str # GENERAL - date: str # GENERAL - desc: str # GENERAL - - seeder: int # TOR - leecher: int # TOR - - slug: str # SC - - -class MediaItemMeta(type): - def __new__(cls, name, bases, dct): - def init(self, **kwargs): - for key, value in kwargs.items(): - setattr(self, key, value) - - dct['__init__'] = init - - def get_attr(self, item): - return self.__dict__.get(item, None) - - dct['__getattr__'] = get_attr - - def set_attr(self, key, value): - self.__dict__[key] = value - - dct['__setattr__'] = set_attr - - return super().__new__(cls, name, bases, dct) - - -class MediaItem(metaclass=MediaItemMeta): - id: int # GENERAL - name: str # GENERAL - type: str # GENERAL - url: str # GENERAL - size: str # GENERAL - score: str # GENERAL - date: str # GENERAL - desc: str # GENERAL - - seeder: int # TOR - leecher: int # TOR - - slug: str # SC - - -class MediaManager: - def __init__(self): - self.media_list: List[MediaItem] = [] - - def add_media(self, data: dict) -> None: - """ - Add media to the list. - - Args: - data (dict): Media data to add. - """ - self.media_list.append(MediaItem(**data)) - - def get(self, index: int) -> MediaItem: - """ - Get a media item from the list by index. - - Args: - index (int): The index of the media item to retrieve. - - Returns: - MediaItem: The media item at the specified index. - """ - return self.media_list[index] - - def get_length(self) -> int: - """ - Get the number of media items in the list. - - Returns: - int: Number of media items. - """ - return len(self.media_list) - - def clear(self) -> None: - """ - This method clears the media list. - """ - self.media_list.clear() - - def __str__(self): - return f"MediaManager(num_media={len(self.media_list)})" \ No newline at end of file diff --git a/StreamingCommunity/Api/Template/Util/__init__.py b/StreamingCommunity/Api/Template/Util/__init__.py deleted file mode 100644 index cea737d50..000000000 --- a/StreamingCommunity/Api/Template/Util/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# 23.11.24 - -from .manage_ep import ( - manage_selection, - map_episode_title, - validate_episode_selection, - validate_selection, - dynamic_format_number, - display_episodes_list, - display_seasons_list -) - -__all__ = [ - "manage_selection", - "map_episode_title", - "validate_episode_selection", - "validate_selection", - "dynamic_format_number", - "display_episodes_list", - display_seasons_list -] \ No newline at end of file diff --git a/StreamingCommunity/Api/Template/__init__.py b/StreamingCommunity/Api/Template/__init__.py index 68b80ddd2..f1a70fada 100644 --- a/StreamingCommunity/Api/Template/__init__.py +++ b/StreamingCommunity/Api/Template/__init__.py @@ -1,7 +1,15 @@ # 19.06.24 from .site import get_select_title +from .config_loader import site_constants +from .loader import load_search_functions +from .object import MediaManager, MediaItem __all__ = [ - "get_select_title" + "get_select_title", + "site_constants", + "load_search_functions", + "get_select_title", + "MediaManager", + "MediaItem" ] \ No newline at end of file diff --git a/StreamingCommunity/Api/Template/config_loader.py b/StreamingCommunity/Api/Template/config_loader.py index 5d68ffde8..f70ebb2f1 100644 --- a/StreamingCommunity/Api/Template/config_loader.py +++ b/StreamingCommunity/Api/Template/config_loader.py @@ -6,6 +6,7 @@ # Internal utilities from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Api.Template.loader import folder_name as lazy_loader_folder def get_site_name_from_stack(): @@ -13,7 +14,7 @@ def get_site_name_from_stack(): file_path = frame_info.filename if "__init__" in file_path: - parts = file_path.split(f"Site{os.sep}") + parts = file_path.split(f"{lazy_loader_folder}{os.sep}") if len(parts) > 1: site_name = parts[1].split(os.sep)[0] @@ -55,10 +56,5 @@ def ANIME_FOLDER(self): if config_manager.get_bool("OUT_FOLDER", "add_siteName"): base_path = os.path.join(base_path, self.SITE_NAME) return os.path.join(base_path, config_manager.get('OUT_FOLDER', 'anime_folder_name')) - - @property - def TELEGRAM_BOT(self): - return config_manager.get_bool('DEFAULT', 'telegram_bot') - -site_constant = SiteConstant() \ No newline at end of file +site_constants = SiteConstant() \ No newline at end of file diff --git a/StreamingCommunity/Api/Template/Util/manage_ep.py b/StreamingCommunity/Api/Template/episode_manager.py similarity index 98% rename from StreamingCommunity/Api/Template/Util/manage_ep.py rename to StreamingCommunity/Api/Template/episode_manager.py index 15869ce57..e20f4296e 100644 --- a/StreamingCommunity/Api/Template/Util/manage_ep.py +++ b/StreamingCommunity/Api/Template/episode_manager.py @@ -12,8 +12,7 @@ # Internal utilities -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Util import config_manager, os_manager from StreamingCommunity.Util.table import TVShowManager @@ -222,7 +221,7 @@ def display_seasons_list(seasons_manager) -> str: last_command (str): Last command entered by the user. """ if len(seasons_manager.seasons) == 1: - console.print("\n[green]Only one season available, selecting it automatically[/green]") + console.print("\n[green]Only one season available, selecting it automatically") time.sleep(1) return "1" diff --git a/StreamingCommunity/Api/Template/loader.py b/StreamingCommunity/Api/Template/loader.py index c38185f6f..f8fc9f141 100644 --- a/StreamingCommunity/Api/Template/loader.py +++ b/StreamingCommunity/Api/Template/loader.py @@ -14,12 +14,14 @@ # Variable console = Console() +folder_name = "Service" class LazySearchModule: def __init__(self, module_name: str, indice: int): """ Lazy loader for a search module. + Args: module_name: Name of the site module (e.g., 'streamingcommunity') indice: Sort index for the module @@ -35,7 +37,7 @@ def _load_module(self): if self._module is None: try: self._module = importlib.import_module( - f'StreamingCommunity.Api.Site.{self.module_name}' + f'StreamingCommunity.Api.{folder_name}.{self.module_name}' ) self._search_func = getattr(self._module, 'search') self._use_for = getattr(self._module, '_useFor') @@ -77,9 +79,6 @@ def __getitem__(self, index: int): Returns: Self (as callable) for index 0, use_for for index 1 - - Raises: - IndexError: If index is not 0 or 1 """ if index == 0: return self @@ -92,15 +91,8 @@ def __getitem__(self, index: int): def load_search_functions() -> Dict[str, LazySearchModule]: """Load and return all available search functions from site modules. - This function uses lazy loading - modules are only imported when first used. - Returns instantly (~0.001s) instead of ~0.2s with full imports. - Returns: Dictionary mapping '{module_name}_search' to LazySearchModule instances - - Example: - >>> search_funcs = load_search_functions() # Instant! - >>> results = search_funcs['streamingcommunity_search']("breaking bad") # Import happens here """ loaded_functions = {} @@ -109,13 +101,13 @@ def load_search_functions() -> Dict[str, LazySearchModule]: # When frozen (exe), sys._MEIPASS points to temporary extraction directory base_path = os.path.join(sys._MEIPASS, "StreamingCommunity") - api_dir = os.path.join(base_path, 'Api', 'Site') + api_dir = os.path.join(base_path, 'Api', folder_name) else: # When not frozen, __file__ is in StreamingCommunity/Api/Template/loader.py # Go up two levels to get to StreamingCommunity/Api base_path = os.path.dirname(os.path.dirname(__file__)) - api_dir = os.path.join(base_path, 'Site') + api_dir = os.path.join(base_path, folder_name) # Quick scan: just read directory structure and module metadata modules_metadata = [] @@ -171,4 +163,13 @@ def load_search_functions() -> Dict[str, LazySearchModule]: console.print(f"[yellow]Warning: Could not update indice in {module_name}: {str(e)}") logging.info(f"Loaded {len(loaded_functions)} search modules") - return loaded_functions \ No newline at end of file + return loaded_functions + + +def get_folder_name() -> str: + """Get the folder name where site modules are located. + + Returns: + The folder name as a string + """ + return folder_name \ No newline at end of file diff --git a/StreamingCommunity/Api/Player/Helper/Vixcloud/util.py b/StreamingCommunity/Api/Template/object.py similarity index 56% rename from StreamingCommunity/Api/Player/Helper/Vixcloud/util.py rename to StreamingCommunity/Api/Template/object.py index 9a7621418..6fdf34c61 100644 --- a/StreamingCommunity/Api/Player/Helper/Vixcloud/util.py +++ b/StreamingCommunity/Api/Template/object.py @@ -1,6 +1,6 @@ # 23.11.24 -from typing import Dict, Any, List, Optional +from typing import Dict, Any, List, Optional, TypedDict class Episode: @@ -70,7 +70,6 @@ def __init__(self, data: Dict[str, Any]): def __str__(self): return f"Season(id={self.id}, number={self.number}, name='{self.name}', episodes={self.episodes.length()})" - class SeasonManager: def __init__(self): self.seasons: List[Season] = [] @@ -107,58 +106,97 @@ def __len__(self) -> int: Return the number of seasons managed. """ return len(self.seasons) - + -class Stream: - def __init__(self, name: str, url: str, active: bool): - self.name = name - self.url = url - self.active = active +class MediaItemData(TypedDict, total=False): + id: int # GENERAL + name: str # GENERAL + type: str # GENERAL + url: str # GENERAL + size: str # GENERAL + score: str # GENERAL + date: str # GENERAL + desc: str # GENERAL - def __repr__(self): - return f"Stream(name={self.name!r}, url={self.url!r}, active={self.active!r})" + seeder: int # TOR + leecher: int # TOR -class StreamsCollection: - def __init__(self, streams: list): - self.streams = [Stream(**stream) for stream in streams] + slug: str # SC + +class MediaItemMeta(type): + def __new__(cls, name, bases, dct): + def init(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) - def __repr__(self): - return f"StreamsCollection(streams={self.streams})" + dct['__init__'] = init - def add_stream(self, name: str, url: str, active: bool): - self.streams.append(Stream(name, url, active)) + def get_attr(self, item): + return self.__dict__.get(item, None) - def get_streams(self): - return self.streams + dct['__getattr__'] = get_attr - -class WindowVideo: - def __init__(self, data: Dict[str, Any]): - self.data = data - self.id: int = data.get('id', '') - self.name: str = data.get('name', '') - self.filename: str = data.get('filename', '') - self.size: str = data.get('size', '') - self.quality: str = data.get('quality', '') - self.duration: str = data.get('duration', '') - self.views: int = data.get('views', '') - self.is_viewable: bool = data.get('is_viewable', '') - self.status: str = data.get('status', '') - self.fps: float = data.get('fps', '') - self.legacy: bool = data.get('legacy', '') - self.folder_id: int = data.get('folder_id', '') - self.created_at_diff: str = data.get('created_at_diff', '') + def set_attr(self, key, value): + self.__dict__[key] = value - def __str__(self): - return f"WindowVideo(id={self.id}, name='{self.name}', filename='{self.filename}', size='{self.size}', quality='{self.quality}', duration='{self.duration}', views={self.views}, is_viewable={self.is_viewable}, status='{self.status}', fps={self.fps}, legacy={self.legacy}, folder_id={self.folder_id}, created_at_diff='{self.created_at_diff}')" + dct['__setattr__'] = set_attr -class WindowParameter: - def __init__(self, data: Dict[str, Any]): - self.data = data - params = data.get('params', {}) - self.token: str = params.get('token', '') - self.expires: str = str(params.get('expires', '')) - self.url = data.get('url') + return super().__new__(cls, name, bases, dct) + +class MediaItem(metaclass=MediaItemMeta): + id: int # GENERAL + name: str # GENERAL + type: str # GENERAL + url: str # GENERAL + size: str # GENERAL + score: str # GENERAL + date: str # GENERAL + desc: str # GENERAL + + seeder: int # TOR + leecher: int # TOR + + slug: str # SC + +class MediaManager: + def __init__(self): + self.media_list: List[MediaItem] = [] + + def add_media(self, data: dict) -> None: + """ + Add media to the list. + + Args: + data (dict): Media data to add. + """ + self.media_list.append(MediaItem(**data)) + + def get(self, index: int) -> MediaItem: + """ + Get a media item from the list by index. + + Args: + index (int): The index of the media item to retrieve. + + Returns: + MediaItem: The media item at the specified index. + """ + return self.media_list[index] + + def get_length(self) -> int: + """ + Get the number of media items in the list. + + Returns: + int: Number of media items. + """ + return len(self.media_list) + + def clear(self) -> None: + """ + This method clears the media list. + """ + self.media_list.clear() def __str__(self): - return (f"WindowParameter(token='{self.token}', expires='{self.expires}', url='{self.url}', data={self.data})") \ No newline at end of file + return f"MediaManager(num_media={len(self.media_list)})" \ No newline at end of file diff --git a/StreamingCommunity/Api/Template/site.py b/StreamingCommunity/Api/Template/site.py index bde89c8bf..886e57c01 100644 --- a/StreamingCommunity/Api/Template/site.py +++ b/StreamingCommunity/Api/Template/site.py @@ -4,11 +4,6 @@ from rich.console import Console -# Internal utilities -from StreamingCommunity.Api.Template.config_loader import site_constant -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance - - # Variable console = Console() available_colors = ['red', 'magenta', 'yellow', 'cyan', 'green', 'blue', 'white'] @@ -18,7 +13,6 @@ def get_select_title(table_show_manager, media_search_manager, num_results_available): """ Display a selection of titles and prompt the user to choose one. - Handles both console and Telegram bot input. Parameters: table_show_manager: Manager for console table display. @@ -29,104 +23,63 @@ def get_select_title(table_show_manager, media_search_manager, num_results_avail MediaItem: The selected media item, or None if no selection is made or an error occurs. """ if not media_search_manager.media_list: - - # console.print("\n[red]No media items available.") return None - if site_constant.TELEGRAM_BOT: - bot = get_bot_instance() - prompt_message = f"Inserisci il numero del titolo che vuoi selezionare (da 0 a {num_results_available - 1}):" - - user_input_str = bot.ask( - "select_title_from_list_number", - prompt_message, - None - ) - - if user_input_str is None: - bot.send_message("Timeout: nessuna selezione ricevuta.", None) - return None + if not media_search_manager.media_list: + console.print("\n[red]No media items available.") + return None + + first_media_item = media_search_manager.media_list[0] + column_info = {"Index": {'color': available_colors[0]}} - try: - chosen_index = int(user_input_str) - if 0 <= chosen_index < num_results_available: - selected_item = media_search_manager.get(chosen_index) - if selected_item: - return selected_item - - else: - bot.send_message(f"Errore interno: Impossibile recuperare il titolo con indice {chosen_index}.", None) - return None - else: - bot.send_message(f"Selezione '{chosen_index}' non valida. Inserisci un numero compreso tra 0 e {num_results_available - 1}.", None) - return None + color_index = 1 + for key in first_media_item.__dict__.keys(): + + if key.capitalize() in column_to_hide: + continue + + if key in ('id', 'type', 'name', 'score'): + if key == 'type': + column_info["Type"] = {'color': 'yellow'} + + elif key == 'name': + column_info["Name"] = {'color': 'magenta'} + elif key == 'score': + column_info["Score"] = {'color': 'cyan'} - except ValueError: - bot.send_message(f"Input '{user_input_str}' non valido. Devi inserire un numero.", None) - return None - - except Exception as e: - bot.send_message(f"Si Γ¨ verificato un errore durante la selezione: {e}", None) - return None + else: + column_info[key.capitalize()] = {'color': available_colors[color_index % len(available_colors)]} + color_index += 1 - else: - - # Logica originale per la console - if not media_search_manager.media_list: - console.print("\n[red]No media items available.") - return None - - first_media_item = media_search_manager.media_list[0] - column_info = {"Index": {'color': available_colors[0]}} + table_show_manager.clear() + table_show_manager.add_column(column_info) - color_index = 1 + for i, media in enumerate(media_search_manager.media_list): + media_dict = {'Index': str(i)} for key in first_media_item.__dict__.keys(): - if key.capitalize() in column_to_hide: continue + media_dict[key.capitalize()] = str(getattr(media, key)) + table_show_manager.add_tv_show(media_dict) + + last_command_str = table_show_manager.run(force_int_input=True, max_int_input=len(media_search_manager.media_list)) + table_show_manager.clear() + + if last_command_str is None or last_command_str.lower() in ["q", "quit"]: + console.print("\n[red]Selezione annullata o uscita.") + return None - if key in ('id', 'type', 'name', 'score'): - if key == 'type': - column_info["Type"] = {'color': 'yellow'} - - elif key == 'name': - column_info["Name"] = {'color': 'magenta'} - elif key == 'score': - column_info["Score"] = {'color': 'cyan'} - - else: - column_info[key.capitalize()] = {'color': available_colors[color_index % len(available_colors)]} - color_index += 1 - - table_show_manager.clear() - table_show_manager.add_column(column_info) - - for i, media in enumerate(media_search_manager.media_list): - media_dict = {'Index': str(i)} - for key in first_media_item.__dict__.keys(): - if key.capitalize() in column_to_hide: - continue - media_dict[key.capitalize()] = str(getattr(media, key)) - table_show_manager.add_tv_show(media_dict) - - last_command_str = table_show_manager.run(force_int_input=True, max_int_input=len(media_search_manager.media_list)) - table_show_manager.clear() - - if last_command_str is None or last_command_str.lower() in ["q", "quit"]: - console.print("\n[red]Selezione annullata o uscita.") - return None - - try: - - selected_index = int(last_command_str) + try: + + selected_index = int(last_command_str) + + if 0 <= selected_index < len(media_search_manager.media_list): + return media_search_manager.get(selected_index) - if 0 <= selected_index < len(media_search_manager.media_list): - return media_search_manager.get(selected_index) - - else: - console.print("\n[red]Indice errato o non valido.") - return None - - except ValueError: - console.print("\n[red]Input non numerico ricevuto dalla tabella.") - return None \ No newline at end of file + else: + console.print("\n[red]Indice errato o non valido.") + return None + + except ValueError: + console.print("\n[red]Input non numerico ricevuto dalla tabella.") + return None \ No newline at end of file diff --git a/StreamingCommunity/Lib/DASH/cdm_helpher.py b/StreamingCommunity/Lib/DASH/cdm_helpher.py new file mode 100644 index 000000000..ac132a72c --- /dev/null +++ b/StreamingCommunity/Lib/DASH/cdm_helpher.py @@ -0,0 +1,174 @@ +# 25.07.25 + +import sys +import base64 +from urllib.parse import urlencode + + +# External libraries +from curl_cffi import requests +from rich.console import Console +from pywidevine.cdm import Cdm +from pywidevine.device import Device +from pywidevine.pssh import PSSH + + +# Variable +console = Console() + + +def get_widevine_keys(pssh: str, license_url: str, cdm_device_path: str, headers: dict = None, query_params: dict =None, key: str=None): + """ + Extract Widevine CONTENT keys (KID/KEY) from a license using pywidevine. + + Args: + - pssh (str): PSSH base64. + - license_url (str): Widevine license URL. + - cdm_device_path (str): Path to CDM file (device.wvd). + - headers (dict): Optional HTTP headers for the license request (from fetch). + - query_params (dict): Optional query parameters to append to the URL. + - key (str): Optional raw license data to bypass HTTP request. + + Returns: + list: List of dicts {'kid': ..., 'key': ...} (only CONTENT keys) or None if error. + """ + if not cdm_device_path: + console.print("[red]Invalid CDM device path.") + return None + + device = Device.load(cdm_device_path) + cdm = Cdm.from_device(device) + session_id = cdm.open() + + try: + console.log(f"[cyan]PSSH: [green]{pssh}") + challenge = cdm.get_license_challenge(session_id, PSSH(pssh)) + + # With request license + if key is None: + + # Build request URL with query params + request_url = license_url + if query_params: + request_url = f"{license_url}?{urlencode(query_params)}" + + # Prepare headers (use original headers from fetch) + req_headers = headers.copy() if headers else {} + request_kwargs = {} + request_kwargs['data'] = challenge + + # Keep original Content-Type or default to octet-stream + if 'Content-Type' not in req_headers: + req_headers['Content-Type'] = 'application/octet-stream' + + # Send license request + if request_url is None: + console.print("[red]License URL is None.") + sys.exit(0) + response = requests.post(request_url, headers=req_headers, impersonate="chrome124", **request_kwargs) + + if response.status_code != 200: + console.print(f"[red]License error: {response.status_code}, {response.text}") + return None + + # Parse license response + license_bytes = response.content + content_type = response.headers.get("Content-Type", "") + + # Handle JSON response + if "application/json" in content_type: + try: + data = response.json() + if "license" in data: + license_bytes = base64.b64decode(data["license"]) + else: + console.print(f"[red]'license' field not found in JSON response: {data}.") + return None + except Exception as e: + console.print(f"[red]Error parsing JSON license: {e}") + return None + + if not license_bytes: + console.print("[red]License data is empty.") + return None + + # Parse license + try: + cdm.parse_license(session_id, license_bytes) + except Exception as e: + console.print(f"[red]Error parsing license: {e}") + return None + + # Extract CONTENT keys + content_keys = [] + for key in cdm.get_keys(session_id): + if key.type == "CONTENT": + kid = key.kid.hex() if isinstance(key.kid, bytes) else str(key.kid) + key_val = key.key.hex() if isinstance(key.key, bytes) else str(key.key) + + content_keys.append({ + 'kid': kid.replace('-', '').strip(), + 'key': key_val.replace('-', '').strip() + }) + + if not content_keys: + console.print("[yellow]⚠️ No CONTENT keys found in license.") + return None + + console.log(f"[cyan]KID: [green]{content_keys[0]['kid']} [white]| [cyan]KEY: [green]{content_keys[0]['key']}") + return content_keys + + else: + content_keys = [] + raw_kid = key.split(":")[0] + raw_key = key.split(":")[1] + content_keys.append({ + 'kid': raw_kid.replace('-', '').strip(), + 'key': raw_key.replace('-', '').strip() + }) + + # Return keys + console.log(f"[cyan]KID: [green]{content_keys[0]['kid']} [white]| [cyan]KEY: [green]{content_keys[0]['key']}") + return content_keys + + finally: + cdm.close(session_id) + + +def get_info_wvd(cdm_device_path): + """ + Extract device information from a Widevine CDM device file (.wvd). + + Args: + cdm_device_path (str): Path to CDM file (device.wvd). + """ + device = Device.load(cdm_device_path) + + # Extract client info + info = {ci.name: ci.value for ci in device.client_id.client_info} + caps = device.client_id.client_capabilities + + company = info.get("company_name", "N/A") + model = info.get("model_name", "N/A") + + device_name = info.get("device_name", "").lower() + build_info = info.get("build_info", "").lower() + + # Extract device type + is_emulator = any(x in device_name for x in [ + "generic", "sdk", "emulator", "x86" + ]) or "test-keys" in build_info or "userdebug" in build_info + + if "tv" in model.lower(): + dev_type = "Android TV" + elif is_emulator: + dev_type = "Android Emulator" + else: + dev_type = "Android Phone" + + console.print( + f"[cyan]Load WVD: " + f"[red]L{device.security_level} [cyan]| [red]{dev_type} [cyan]| " + f"[red]{company} {model} [cyan]| API [red]{caps.oem_crypto_api_version} [cyan]| " + f"[cyan]SysID: [red]{device.system_id}" + ) \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/DASH/decrypt.py b/StreamingCommunity/Lib/DASH/decrypt.py similarity index 83% rename from StreamingCommunity/Lib/Downloader/DASH/decrypt.py rename to StreamingCommunity/Lib/DASH/decrypt.py index 924b15611..23a4301d0 100644 --- a/StreamingCommunity/Lib/Downloader/DASH/decrypt.py +++ b/StreamingCommunity/Lib/DASH/decrypt.py @@ -1,10 +1,10 @@ # 25.07.25 import os +import time import subprocess import logging import threading -import time # External libraries @@ -13,19 +13,17 @@ # Internal utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.os import get_mp4decrypt_path -from StreamingCommunity.Util.color import Colors +from StreamingCommunity.Util import config_manager, Colors # Variable console = Console() extension_output = config_manager.get("M3U8_CONVERSION", "extension") CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder') -SHOW_DECRYPT_PROGRESS = False +SHOW_DECRYPT_PROGRESS = True -def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cleanup=True): +def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None): """ Decrypt an mp4/m4s file using mp4decrypt. @@ -40,10 +38,11 @@ def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cl Returns: str: Path to decrypted file, or None if error. """ + from StreamingCommunity.Util.os import get_mp4decrypt_path # Check if input file exists if not os.path.isfile(encrypted_path): - console.print(f"[bold red] Encrypted file not found: {encrypted_path}[/bold red]") + console.print(f"[red] Encrypted file not found: {encrypted_path}") return None # Check if kid and key are valid hex @@ -51,7 +50,7 @@ def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cl bytes.fromhex(kid) bytes.fromhex(key) except Exception: - console.print("[bold red] Invalid KID or KEY (not hex).[/bold red]") + console.print("[red] Invalid KID or KEY (not hex).") return None if not output_path: @@ -62,7 +61,7 @@ def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cl key_format = f"{kid.lower()}:{key.lower()}" cmd = [get_mp4decrypt_path(), "--key", key_format, encrypted_path, output_path] - logging.info(f"Running command: {' '.join(cmd)}") + logging.info(f"Running mp4decrypt command: {' '.join(cmd)}") progress_bar = None monitor_thread = None @@ -95,7 +94,6 @@ def monitor_output_file(): progress_bar.refresh() if current_size == last_size and current_size > 0: - # File stopped growing, likely finished break last_size = current_size @@ -111,7 +109,7 @@ def monitor_output_file(): except Exception as e: if progress_bar: progress_bar.close() - console.print(f"[bold red] mp4decrypt execution failed: {e}[/bold red]") + console.print(f"[red] mp4decrypt execution failed: {e}") return None if progress_bar: @@ -122,7 +120,7 @@ def monitor_output_file(): if result.returncode == 0 and os.path.exists(output_path): # Cleanup temporary files - if cleanup and CLEANUP_TMP: + if CLEANUP_TMP: if os.path.exists(encrypted_path): os.remove(encrypted_path) @@ -134,11 +132,11 @@ def monitor_output_file(): # Check if output file is not empty if os.path.getsize(output_path) == 0: - console.print(f"[bold red] Decrypted file is empty: {output_path}[/bold red]") + console.print(f"[red] Decrypted file is empty: {output_path}") return None return output_path else: - console.print(f"[bold red] mp4decrypt failed:[/bold red] {result.stderr}") + console.print(f"[red] mp4decrypt failed: {result.stderr}") return None \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/DASH/downloader.py b/StreamingCommunity/Lib/DASH/downloader.py similarity index 94% rename from StreamingCommunity/Lib/Downloader/DASH/downloader.py rename to StreamingCommunity/Lib/DASH/downloader.py index 163bb7d10..601aa7b0e 100644 --- a/StreamingCommunity/Lib/Downloader/DASH/downloader.py +++ b/StreamingCommunity/Lib/DASH/downloader.py @@ -12,21 +12,21 @@ # Internal utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.os import os_manager, internet_manager, get_wvd_path -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.headers import get_userAgent +from StreamingCommunity.Util import config_manager, os_manager, internet_manager +from StreamingCommunity.Util.os import get_wvd_path +from StreamingCommunity.Util.http_client import create_client, get_userAgent # Logic class -from .parser import MPDParser +from .parser import MPD_Parser from .segments import MPD_Segments from .decrypt import decrypt_with_mp4decrypt from .cdm_helpher import get_widevine_keys # FFmpeg functions -from ...FFmpeg import print_duration_table, join_audios, join_video, join_subtitle +from StreamingCommunity.Lib.FFmpeg.util import print_duration_table +from StreamingCommunity.Lib.FFmpeg.merge import join_audios, join_video, join_subtitle # Config @@ -56,8 +56,8 @@ def __init__(self, license_url, mpd_url, mpd_sub_list: list = None, output_path: - output_path (str): Path to save the final output file. """ self.cdm_device = get_wvd_path() - self.license_url = license_url - self.mpd_url = mpd_url + self.license_url = str(license_url).strip() if license_url else None + self.mpd_url = str(mpd_url).strip() self.mpd_sub_list = mpd_sub_list or [] # Sanitize the output path to remove invalid characters @@ -108,7 +108,7 @@ def parse_manifest(self, custom_headers): if self.file_already_exists: return - self.parser = MPDParser(self.mpd_url) + self.parser = MPD_Parser(self.mpd_url) self.parser.parse(custom_headers) def calculate_column_widths(): @@ -174,7 +174,7 @@ def calculate_column_widths(): data_rows, column_widths = calculate_column_widths() # Create table with dynamic widths - table = Table(show_header=True, header_style="bold cyan", border_style="blue") + table = Table(show_header=True, header_style="cyan", border_style="blue") table.add_column("Type", style="cyan bold", width=column_widths[0]) table.add_column("Available", style="green", width=column_widths[1]) table.add_column("Set", style="red", width=column_widths[2]) @@ -184,7 +184,6 @@ def calculate_column_widths(): for row in data_rows: table.add_row(*row) - console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]") console.print(table) console.print("") @@ -222,22 +221,23 @@ def download_subtitles(self) -> bool: f.write(response.content) except Exception as e: - console.print(f"[red]Error downloading subtitle {language}: {e}[/red]") + console.print(f"[red]Error downloading subtitle {language}: {e}") return False return True - def download_and_decrypt(self, custom_headers=None, query_params=None): + def download_and_decrypt(self, custom_headers=None, query_params=None, key=None) -> bool: """ Download and decrypt video/audio streams. Skips download if file already exists. - + Args: - custom_headers (dict): Optional HTTP headers for the license request. - query_params (dict): Optional query parameters to append to the license URL. - license_data (str/bytes): Optional raw license data to bypass HTTP request. + - custom_headers (dict): Optional HTTP headers for the license request. + - query_params (dict): Optional query parameters to append to the license URL. + - license_data (str/bytes): Optional raw license data to bypass HTTP request. + - key (str): Optional raw license data to bypass HTTP request. """ if self.file_already_exists: - console.print(f"[red]File already exists: {self.original_output_path}[/red]") + console.print(f"[red]File already exists: {self.original_output_path}") self.output_file = self.original_output_path return True @@ -256,16 +256,16 @@ def download_and_decrypt(self, custom_headers=None, query_params=None): cdm_device_path=self.cdm_device, headers=custom_headers, query_params=query_params, + key=key ) if not keys: - console.print("[red]No keys found, cannot proceed with download.[/red]") + console.print("[red]No keys found, cannot proceed with download.") return False # Extract the first key for decryption - key = keys[0] - KID = key['kid'] - KEY = key['key'] + KID = keys[0]['kid'] + KEY = keys[0]['key'] # Download subtitles self.download_subtitles() @@ -319,12 +319,10 @@ def download_and_decrypt(self, custom_headers=None, query_params=None): if not result_path: self.error = "Decryption of video failed" - print(self.error) return False else: self.error = "No video found" - print(self.error) return False # Now download audio with segment limiting @@ -376,12 +374,10 @@ def download_and_decrypt(self, custom_headers=None, query_params=None): if not result_path: self.error = "Decryption of audio failed" - print(self.error) return False else: self.error = "No audio found" - print(self.error) return False return True @@ -394,7 +390,7 @@ def download_segments(self, clear=False): clear (bool): If True, content is not encrypted and doesn't need decryption """ if not clear: - console.print("[yellow]Warning: download_segments called with clear=False[/yellow]") + console.print("[yellow]Warning: download_segments called with clear=False") return False video_segments_count = 0 @@ -437,7 +433,7 @@ def download_segments(self, clear=False): except Exception as ex: self.error = str(ex) - console.print(f"[red]Error downloading video: {ex}[/red]") + console.print(f"[red]Error downloading video: {ex}") return False finally: @@ -451,7 +447,7 @@ def download_segments(self, clear=False): else: self.error = "No video found" - console.print(f"[red]{self.error}[/red]") + console.print(f"[red]{self.error}") return False # Download audio with segment limiting @@ -489,7 +485,7 @@ def download_segments(self, clear=False): except Exception as ex: self.error = str(ex) - console.print(f"[red]Error downloading audio: {ex}[/red]") + console.print(f"[red]Error downloading audio: {ex}") return False finally: @@ -503,7 +499,7 @@ def download_segments(self, clear=False): else: self.error = "No audio found" - console.print(f"[red]{self.error}[/red]") + console.print(f"[red]{self.error}") return False return True @@ -537,12 +533,12 @@ def finalize_output(self): merged_file = join_video(video_file, output_file, codec=None) else: - console.print("[red]Video file missing, cannot export[/red]") + console.print("[red]Video file missing, cannot export") self.error = "Video file missing, cannot export" return None except Exception as e: - console.print(f"[red]Error during merge: {e}[/red]") + console.print(f"[red]Error during merge: {e}") self.error = f"Merge failed: {e}" return None @@ -582,7 +578,7 @@ def finalize_output(self): merged_file = output_file except Exception as e: - console.print(f"[yellow]Warning: Failed to merge subtitles: {e}[/yellow]") + console.print(f"[yellow]Warning: Failed to merge subtitles: {e}") # Handle failed sync case if use_shortest: diff --git a/StreamingCommunity/Lib/Downloader/DASH/parser.py b/StreamingCommunity/Lib/DASH/parser.py similarity index 96% rename from StreamingCommunity/Lib/Downloader/DASH/parser.py rename to StreamingCommunity/Lib/DASH/parser.py index c6783b1ba..ea6d0009a 100644 --- a/StreamingCommunity/Lib/Downloader/DASH/parser.py +++ b/StreamingCommunity/Lib/DASH/parser.py @@ -354,7 +354,7 @@ def _build_media_urls(self, media_template: str, base_url: str, rep_id: str, ban return media_urls -class MPDParser: +class MPD_Parser: @staticmethod def _is_ad_period(period_id: str, base_url: str) -> bool: """ @@ -437,19 +437,6 @@ def _deduplicate_audios(representations: List[Dict[str, Any]]) -> List[Dict[str, return list(audio_map.values()) - @staticmethod - def get_best(representations): - """ - Returns the video representation with the highest resolution/bandwidth, or audio with highest bandwidth. - """ - videos = [r for r in representations if r['type'] == 'video'] - audios = [r for r in representations if r['type'] == 'audio'] - if videos: - return max(videos, key=lambda r: (r['height'], r['width'], r['bandwidth'])) - elif audios: - return max(audios, key=lambda r: r['bandwidth']) - return None - @staticmethod def get_worst(representations): """ @@ -565,14 +552,12 @@ def _parse_representations(self) -> None: if rep_id not in rep_aggregator: rep_aggregator[rep_id] = rep - #print(f" ✨ New Rep {rep_id} ({rep['type']}): {len(rep['segment_urls'])} segments") else: existing = rep_aggregator[rep_id] # Concatenate segment URLs if rep['segment_urls']: existing['segment_urls'].extend(rep['segment_urls']) - # Update init_url only if it wasn't set before if not existing['init_url'] and rep['init_url']: existing['init_url'] = rep['init_url'] @@ -652,7 +637,7 @@ def select_video(self, force_resolution="Best"): filter_custom_resolution = "Best" elif force_resolution_l == "worst": - selected_video = MPDParser.get_worst(video_reps) + selected_video = MPD_Parser.get_worst(video_reps) filter_custom_resolution = "Worst" else: diff --git a/StreamingCommunity/Lib/Downloader/DASH/segments.py b/StreamingCommunity/Lib/DASH/segments.py similarity index 98% rename from StreamingCommunity/Lib/Downloader/DASH/segments.py rename to StreamingCommunity/Lib/DASH/segments.py index f0b8118a1..ac7d9600f 100644 --- a/StreamingCommunity/Lib/Downloader/DASH/segments.py +++ b/StreamingCommunity/Lib/DASH/segments.py @@ -13,10 +13,9 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Lib.M3U8.estimator import M3U8_Ts_Estimator -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.color import Colors +from StreamingCommunity.Util.http_client import get_userAgent +from StreamingCommunity.Lib.HLS.estimator import M3U8_Ts_Estimator +from StreamingCommunity.Util import config_manager, Colors # Config @@ -145,6 +144,7 @@ async def download_segments(self, output_dir: str = None, concurrent_downloads: worker_type = 'video' if 'Video' in description else 'audio' concurrent_downloads = self._get_worker_count(worker_type) + print("") progress_bar = tqdm( total=len(segment_urls) + 1, desc=f"Downloading {rep_id}", diff --git a/StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py b/StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py deleted file mode 100644 index 87d48dfd1..000000000 --- a/StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py +++ /dev/null @@ -1,129 +0,0 @@ -# 25.07.25 - -import base64 -from urllib.parse import urlencode - - -# External libraries -from curl_cffi import requests -from rich.console import Console -from pywidevine.cdm import Cdm -from pywidevine.device import Device -from pywidevine.pssh import PSSH - - -# Variable -console = Console() - - -def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, query_params=None): - """ - Extract Widevine CONTENT keys (KID/KEY) from a license using pywidevine. - - Args: - pssh (str): PSSH base64. - license_url (str): Widevine license URL. - cdm_device_path (str): Path to CDM file (device.wvd). - headers (dict): Optional HTTP headers for the license request (from fetch). - query_params (dict): Optional query parameters to append to the URL. - - Returns: - list: List of dicts {'kid': ..., 'key': ...} (only CONTENT keys) or None if error. - """ - if not cdm_device_path: - console.print("[bold red]Invalid CDM device path.[/bold red]") - return None - - try: - device = Device.load(cdm_device_path) - cdm = Cdm.from_device(device) - session_id = cdm.open() - - try: - challenge = cdm.get_license_challenge(session_id, PSSH(pssh)) - - # Build request URL with query params - request_url = license_url - if query_params: - request_url = f"{license_url}?{urlencode(query_params)}" - - # Prepare headers (use original headers from fetch) - req_headers = headers.copy() if headers else {} - request_kwargs = {} - request_kwargs['data'] = challenge - - # Keep original Content-Type or default to octet-stream - if 'Content-Type' not in req_headers: - req_headers['Content-Type'] = 'application/octet-stream' - - # Send license request - try: - # response = httpx.post(license_url, data=challenge, headers=req_headers, content=payload) - response = requests.post(request_url, headers=req_headers, impersonate="chrome124", **request_kwargs) - - except Exception as e: - console.print(f"[bold red]Request error:[/bold red] {e}") - return None - - if response.status_code != 200: - console.print(f"[bold red]License error:[/bold red] {response.status_code}, {response.text}") - console.print({ - "url": license_url, - "headers": req_headers, - "session_id": session_id.hex(), - "pssh": pssh - }) - return None - - # Parse license response - license_bytes = response.content - content_type = response.headers.get("Content-Type", "") - - # Handle JSON response - if "application/json" in content_type: - try: - data = response.json() - if "license" in data: - license_bytes = base64.b64decode(data["license"]) - else: - console.print(f"[bold red]'license' field not found in JSON response: {data}.[/bold red]") - return None - except Exception as e: - console.print(f"[bold red]Error parsing JSON license:[/bold red] {e}") - return None - - if not license_bytes: - console.print("[bold red]License data is empty.[/bold red]") - return None - - # Parse license - try: - cdm.parse_license(session_id, license_bytes) - except Exception as e: - console.print(f"[bold red]Error parsing license:[/bold red] {e}") - return None - - # Extract CONTENT keys - content_keys = [] - for key in cdm.get_keys(session_id): - if key.type == "CONTENT": - kid = key.kid.hex() if isinstance(key.kid, bytes) else str(key.kid) - key_val = key.key.hex() if isinstance(key.key, bytes) else str(key.key) - - content_keys.append({ - 'kid': kid.replace('-', '').strip(), - 'key': key_val.replace('-', '').strip() - }) - - if not content_keys: - console.print("[bold yellow]⚠️ No CONTENT keys found in license.[/bold yellow]") - return None - - return content_keys - - finally: - cdm.close(session_id) - - except Exception as e: - console.print(f"[bold red]CDM error:[/bold red] {e}") - return None \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/MEGA/crypto.py b/StreamingCommunity/Lib/Downloader/MEGA/crypto.py deleted file mode 100644 index e308f3c18..000000000 --- a/StreamingCommunity/Lib/Downloader/MEGA/crypto.py +++ /dev/null @@ -1,118 +0,0 @@ -# 25-06-2020 By @rodwyer "https://pypi.org/project/mega.py/" - - -import json -import base64 -import struct -import binascii -import random -import codecs - - -# External libraries -from Crypto.Cipher import AES - - -def makebyte(x): - return codecs.latin_1_encode(x)[0] - -def makestring(x): - return codecs.latin_1_decode(x)[0] - -def aes_cbc_encrypt(data, key): - aes_cipher = AES.new(key, AES.MODE_CBC, makebyte('\0' * 16)) - return aes_cipher.encrypt(data) - -def aes_cbc_decrypt(data, key): - aes_cipher = AES.new(key, AES.MODE_CBC, makebyte('\0' * 16)) - return aes_cipher.decrypt(data) - -def aes_cbc_encrypt_a32(data, key): - return str_to_a32(aes_cbc_encrypt(a32_to_str(data), a32_to_str(key))) - -def aes_cbc_decrypt_a32(data, key): - return str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key))) - -def encrypt_key(a, key): - return sum((aes_cbc_encrypt_a32(a[i:i + 4], key) - for i in range(0, len(a), 4)), ()) - -def decrypt_key(a, key): - return sum((aes_cbc_decrypt_a32(a[i:i + 4], key) - for i in range(0, len(a), 4)), ()) - -def decrypt_attr(attr, key): - attr = aes_cbc_decrypt(attr, a32_to_str(key)) - attr = makestring(attr) - attr = attr.rstrip('\0') - - if attr[:6] == 'MEGA{"': - json_start = attr.index('{') - json_end = attr.rfind('}') + 1 - return json.loads(attr[json_start:json_end]) - -def a32_to_str(a): - return struct.pack('>%dI' % len(a), *a) - -def str_to_a32(b): - if isinstance(b, str): - b = makebyte(b) - if len(b) % 4: - b += b'\0' * (4 - len(b) % 4) - return struct.unpack('>%dI' % (len(b) / 4), b) - - -def mpi_to_int(s): - return int(binascii.hexlify(s[2:]), 16) - -def extended_gcd(a, b): - if a == 0: - return (b, 0, 1) - else: - g, y, x = extended_gcd(b % a, a) - return (g, x - (b // a) * y, y) - -def modular_inverse(a, m): - g, x, y = extended_gcd(a, m) - if g != 1: - raise Exception('modular inverse does not exist') - else: - return x % m - -def base64_url_decode(data): - data += '=='[(2 - len(data) * 3) % 4:] - for search, replace in (('-', '+'), ('_', '/'), (',', '')): - data = data.replace(search, replace) - return base64.b64decode(data) - -def base64_to_a32(s): - return str_to_a32(base64_url_decode(s)) - -def base64_url_encode(data): - data = base64.b64encode(data) - data = makestring(data) - for search, replace in (('+', '-'), ('/', '_'), ('=', '')): - data = data.replace(search, replace) - - return data - -def a32_to_base64(a): - return base64_url_encode(a32_to_str(a)) - -def get_chunks(size): - p = 0 - s = 0x20000 - while p + s < size: - yield (p, s) - p += s - if s < 0x100000: - s += 0x20000 - - yield (p, size - p) - -def make_id(length): - text = '' - possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - for i in range(length): - text += random.choice(possible) - return text \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/MEGA/errors.py b/StreamingCommunity/Lib/Downloader/MEGA/errors.py deleted file mode 100644 index db56f401c..000000000 --- a/StreamingCommunity/Lib/Downloader/MEGA/errors.py +++ /dev/null @@ -1,58 +0,0 @@ -# 25-06-2020 By @rodwyer "https://pypi.org/project/mega.py/" - - -_CODE_TO_DESCRIPTIONS = { - -1: ('EINTERNAL', - ('An internal error has occurred. Please submit a bug report, ' - 'detailing the exact circumstances in which this error occurred')), - -2: ('EARGS', 'You have passed invalid arguments to this command'), - -3: ('EAGAIN', - ('(always at the request level) A temporary congestion or server ' - 'malfunction prevented your request from being processed. ' - 'No data was altered. Retry. Retries must be spaced with ' - 'exponential backoff')), - -4: ('ERATELIMIT', - ('You have exceeded your command weight per time quota. Please ' - 'wait a few seconds, then try again (this should never happen ' - 'in sane real-life applications)')), - -5: ('EFAILED', 'The upload failed. Please restart it from scratch'), - -6: - ('ETOOMANY', - 'Too many concurrent IP addresses are accessing this upload target URL'), - -7: - ('ERANGE', ('The upload file packet is out of range or not starting and ' - 'ending on a chunk boundary')), - -8: ('EEXPIRED', - ('The upload target URL you are trying to access has expired. ' - 'Please request a fresh one')), - -9: ('ENOENT', 'Object (typically, node or user) not found'), - -10: ('ECIRCULAR', 'Circular linkage attempted'), - -11: ('EACCESS', - 'Access violation (e.g., trying to write to a read-only share)'), - -12: ('EEXIST', 'Trying to create an object that already exists'), - -13: ('EINCOMPLETE', 'Trying to access an incomplete resource'), - -14: ('EKEY', 'A decryption operation failed (never returned by the API)'), - -15: ('ESID', 'Invalid or expired user session, please relogin'), - -16: ('EBLOCKED', 'User blocked'), - -17: ('EOVERQUOTA', 'Request over quota'), - -18: ('ETEMPUNAVAIL', - 'Resource temporarily not available, please try again later'), - -19: ('ETOOMANYCONNECTIONS', 'many connections on this resource'), - -20: ('EWRITE', 'Write failed'), - -21: ('EREAD', 'Read failed'), - -22: ('EAPPKEY', 'Invalid application key; request not processed'), -} - - -class RequestError(Exception): - """ - Error in API request - """ - def __init__(self, message): - code = message - self.code = code - code_desc, long_desc = _CODE_TO_DESCRIPTIONS[code] - self.message = f'{code_desc}, {long_desc}' - - def __str__(self): - return self.message \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/MEGA/mega.py b/StreamingCommunity/Lib/Downloader/MEGA/mega.py deleted file mode 100644 index 22fa1e5e1..000000000 --- a/StreamingCommunity/Lib/Downloader/MEGA/mega.py +++ /dev/null @@ -1,321 +0,0 @@ -# 25-06-2020 By @rodwyer "https://pypi.org/project/mega.py/" - -import os -import math -import re -import random -import binascii -import sys -import time -from pathlib import Path - - -# External libraries -import httpx -from tqdm import tqdm -from Crypto.Cipher import AES -from Crypto.PublicKey import RSA -from Crypto.Util import Counter -from rich.console import Console - - -# Internal utilities -from .errors import RequestError -from .crypto import ( - a32_to_base64, encrypt_key, base64_url_encode, - base64_to_a32, base64_url_decode, - decrypt_attr, a32_to_str, get_chunks, str_to_a32, - decrypt_key, mpi_to_int, make_id, - modular_inverse -) - -from StreamingCommunity.Util.color import Colors -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.os import internet_manager, os_manager -from StreamingCommunity.Util.headers import get_userAgent -from ...FFmpeg import print_duration_table - - -# Config -EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension") - - -# Variable -console = Console() - - -class Mega_Downloader: - def __init__(self, options=None): - self.schema = 'https' - self.domain = 'mega.co.nz' - self.timeout = 160 - self.sid = None - self.sequence_num = random.randint(0, 0xFFFFFFFF) - self.request_id = make_id(10) - self._trash_folder_node_id = None - self.options = options or {} - - def login(self): - self.login_anonymous() - self._trash_folder_node_id = self.get_node_by_type(4)[0] - return self - - def login_anonymous(self): - master_key = [random.randint(0, 0xFFFFFFFF)] * 4 - password_key = [random.randint(0, 0xFFFFFFFF)] * 4 - session_self_challenge = [random.randint(0, 0xFFFFFFFF)] * 4 - - user = self._api_request({ - 'a': 'up', - 'k': a32_to_base64(encrypt_key(master_key, password_key)), - 'ts': base64_url_encode( - a32_to_str(session_self_challenge) + - a32_to_str(encrypt_key(session_self_challenge, master_key)) - ) - }) - - resp = self._api_request({'a': 'us', 'user': user}) - if isinstance(resp, int): - raise RequestError(resp) - self._login_process(resp, password_key) - - def _login_process(self, resp, password): - encrypted_master_key = base64_to_a32(resp['k']) - self.master_key = decrypt_key(encrypted_master_key, password) - - if 'tsid' in resp: - tsid = base64_url_decode(resp['tsid']) - key_encrypted = a32_to_str( - encrypt_key(str_to_a32(tsid[:16]), self.master_key) - ) - - if key_encrypted == tsid[-16:]: - self.sid = resp['tsid'] - - elif 'csid' in resp: - encrypted_rsa_private_key = base64_to_a32(resp['privk']) - rsa_private_key = decrypt_key(encrypted_rsa_private_key, self.master_key) - - private_key = a32_to_str(rsa_private_key) - rsa_private_key = [0, 0, 0, 0] - - for i in range(4): - bitlength = (private_key[0] * 256) + private_key[1] - bytelength = math.ceil(bitlength / 8) + 2 - rsa_private_key[i] = mpi_to_int(private_key[:bytelength]) - private_key = private_key[bytelength:] - - first_factor_p = rsa_private_key[0] - second_factor_q = rsa_private_key[1] - private_exponent_d = rsa_private_key[2] - rsa_modulus_n = first_factor_p * second_factor_q - phi = (first_factor_p - 1) * (second_factor_q - 1) - public_exponent_e = modular_inverse(private_exponent_d, phi) - - rsa_components = ( - rsa_modulus_n, - public_exponent_e, - private_exponent_d, - first_factor_p, - second_factor_q, - ) - rsa_decrypter = RSA.construct(rsa_components) - encrypted_sid = mpi_to_int(base64_url_decode(resp['csid'])) - sid = '%x' % rsa_decrypter._decrypt(encrypted_sid) - sid = binascii.unhexlify('0' + sid if len(sid) % 2 else sid) - self.sid = base64_url_encode(sid[:43]) - - def _api_request(self, data): - params = {'id': self.sequence_num} - self.sequence_num += 1 - - if self.sid: - params['sid'] = self.sid - - if not isinstance(data, list): - data = [data] - - url = f'{self.schema}://g.api.{self.domain}/cs' - - with httpx.Client(timeout=self.timeout) as client: - response = client.post(url, params=params, json=data) - json_resp = response.json() - - int_resp = None - try: - if isinstance(json_resp, list): - int_resp = json_resp[0] if isinstance(json_resp[0], int) else None - elif isinstance(json_resp, int): - int_resp = json_resp - except IndexError: - pass - - if int_resp is not None: - if int_resp == 0: - return int_resp - if int_resp == -3: - raise RuntimeError('Request failed, retrying') - raise RequestError(int_resp) - - return json_resp[0] - - def _parse_url(self, url): - """Parse file id and key from url.""" - if '/file/' in url: - url = url.replace(' ', '') - file_id = re.findall(r'\W\w{8}\W', url)[0][1:-1] - id_index = re.search(file_id, url).end() - key = url[id_index + 1:] - return f'{file_id}!{key}' - - elif '!' in url: - match = re.findall(r'/#!(.*)', url) - return match[0] - - else: - raise RequestError('Url key missing') - - def get_node_by_type(self, node_type): - """Get node by type (2=root, 3=inbox, 4=trash)""" - files = self._api_request({'a': 'f', 'c': 1, 'r': 1}) - for file in files['f']: - if file['t'] == node_type: - return (file['h'], file) - - return None - - def download_url(self, url, dest_path=None): - """Download a file by its public url""" - path_obj = Path(dest_path) - folder = str(path_obj.parent) - name = path_obj.name.replace(EXTENSION_OUTPUT, f".{EXTENSION_OUTPUT}") - os_manager.create_path(folder) - - path = self._parse_url(url).split('!') - file_id = path[0] - file_key = path[1] - - return self._download_file( - file_handle=file_id, - file_key=file_key, - dest_path=os.path.join(folder, name) - ) - - def _download_file(self, file_handle, file_key, dest_path=None): - file_key = base64_to_a32(file_key) - file_data = self._api_request({ - 'a': 'g', - 'g': 1, - 'p': file_handle - }) - - k = (file_key[0] ^ file_key[4], file_key[1] ^ file_key[5], - file_key[2] ^ file_key[6], file_key[3] ^ file_key[7]) - iv = file_key[4:6] + (0, 0) - meta_mac = file_key[6:8] - - if 'g' not in file_data: - raise RequestError('File not accessible anymore') - - file_url = file_data['g'] - file_size = file_data['s'] - attribs = base64_url_decode(file_data['at']) - attribs = decrypt_attr(attribs, k) - - file_name = os_manager.get_sanitize_file(attribs['n']) - output_path = Path(dest_path) if dest_path else Path(file_name) - os_manager.create_path(output_path.parent) - - k_str = a32_to_str(k) - counter = Counter.new( - 128, - initial_value=((iv[0] << 32) + iv[1]) << 64 - ) - aes = AES.new(k_str, AES.MODE_CTR, counter=counter) - - mac_str = '\0' * 16 - mac_encryptor = AES.new(k_str, AES.MODE_CBC, mac_str.encode("utf8")) - iv_str = a32_to_str([iv[0], iv[1], iv[0], iv[1]]) - - start_time = time.time() - downloaded = 0 - - console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]") - with open(output_path, 'wb') as output_file: - with httpx.Client(timeout=None, headers={'User-Agent': get_userAgent()}) as client: - with client.stream('GET', file_url, headers={'User-Agent': get_userAgent()}) as response: - response.raise_for_status() - - progress_bar = tqdm( - total=file_size, - ascii='β–‘β–’β–ˆ', - bar_format=f"{Colors.YELLOW}MEGA{Colors.CYAN} Downloading{Colors.WHITE}: " - f"{Colors.MAGENTA}{{bar:40}} " - f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}}" - f" {Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}]" - f"{Colors.WHITE}{{postfix}} ", - unit='B', - unit_scale=True, - unit_divisor=1024, - mininterval=0.05, - file=sys.stdout - ) - - with progress_bar: - chunks_data = list(get_chunks(file_size)) - stream_iter = response.iter_bytes(chunk_size=8192) - - for chunk_start, chunk_size in chunks_data: - chunk = b'' - remaining = chunk_size - - while remaining > 0: - try: - data = next(stream_iter) - to_read = min(len(data), remaining) - chunk += data[:to_read] - remaining -= to_read - except StopIteration: - break - - chunk = aes.decrypt(chunk) - output_file.write(chunk) - - downloaded += len(chunk) - progress_bar.update(len(chunk)) - - # Update postfix with speed - elapsed = time.time() - start_time - if elapsed > 0: - speed = downloaded / elapsed - speed_str = internet_manager.format_transfer_speed(speed) - postfix_str = f"{Colors.LIGHT_MAGENTA}@ {Colors.LIGHT_CYAN}{speed_str}" - progress_bar.set_postfix_str(postfix_str) - - encryptor = AES.new(k_str, AES.MODE_CBC, iv_str) - for i in range(0, len(chunk) - 16, 16): - block = chunk[i:i + 16] - encryptor.encrypt(block) - - if file_size > 16: - i += 16 - else: - i = 0 - - block = chunk[i:i + 16] - if len(block) % 16: - block += b'\0' * (16 - (len(block) % 16)) - mac_str = mac_encryptor.encrypt(encryptor.encrypt(block)) - - file_mac = str_to_a32(mac_str) - if (file_mac[0] ^ file_mac[1], file_mac[2] ^ file_mac[3]) != meta_mac: - if output_path.exists(): - output_path.unlink() - raise ValueError('Mismatched mac') - - # Display file information - file_size = internet_manager.format_file_size(os.path.getsize(output_path)) - duration = print_duration_table(output_path, description=False, return_string=True) - console.print(f"[yellow]Output[white]: [red]{os.path.abspath(output_path)} \n" - f" [cyan]with size[white]: [red]{file_size} \n" - f" [cyan]and duration[white]: [red]{duration}") \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/TOR/downloader.py b/StreamingCommunity/Lib/Downloader/TOR/downloader.py deleted file mode 100644 index 4ba00a0b2..000000000 --- a/StreamingCommunity/Lib/Downloader/TOR/downloader.py +++ /dev/null @@ -1,467 +0,0 @@ -# 23.06.24 - -import os -import re -import sys -import time -import psutil -import logging -from pathlib import Path - - -# External libraries -from rich.console import Console -from tqdm import tqdm -import qbittorrentapi - - -# Internal utilities -from StreamingCommunity.Util.color import Colors -from StreamingCommunity.Util.os import internet_manager -from StreamingCommunity.Util.config_json import config_manager - - -# Configuration -HOST = config_manager.get('QBIT_CONFIG', 'host') -PORT = config_manager.get('QBIT_CONFIG', 'port') -USERNAME = config_manager.get('QBIT_CONFIG', 'user') -PASSWORD = config_manager.get('QBIT_CONFIG', 'pass') - -REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') -console = Console() - - -class TOR_downloader: - def __init__(self): - """ - Initializes the TorrentDownloader instance and connects to qBittorrent. - """ - self.console = Console() - self.latest_torrent_hash = None - self.output_file = None - self.file_name = None - self.save_path = None - self.torrent_name = None - - self._connect_to_client() - - def _connect_to_client(self): - """ - Establishes connection to qBittorrent client using configuration parameters. - """ - self.console.print(f"[cyan]Connecting to qBittorrent: [green]{HOST}:{PORT}") - - try: - # Create client with connection settings and timeouts - self.qb = qbittorrentapi.Client( - host=HOST, - port=PORT, - username=USERNAME, - password=PASSWORD, - VERIFY_WEBUI_CERTIFICATE=False, - REQUESTS_ARGS={'timeout': REQUEST_TIMEOUT} - ) - - # Test connection and login - self.qb.auth_log_in() - qb_version = self.qb.app.version - self.console.print(f"[green]Successfully connected to qBittorrent v{qb_version}") - - except Exception as e: - logging.error(f"Unexpected error: {str(e)}") - self.console.print(f"[bold red]Error initializing qBittorrent client: {str(e)}[/bold red]") - sys.exit(1) - - def add_magnet_link(self, magnet_link, save_path=None): - """ - Adds a magnet link to qBittorrent and retrieves torrent information. - - Args: - magnet_link (str): Magnet link to add to qBittorrent - save_path (str, optional): Directory where to save the downloaded files - - Returns: - TorrentDictionary: Information about the added torrent - - Raises: - ValueError: If magnet link is invalid or torrent can't be added - """ - # Extract hash from magnet link - magnet_hash_match = re.search(r'urn:btih:([0-9a-fA-F]+)', magnet_link) - if not magnet_hash_match: - raise ValueError("Invalid magnet link: hash not found") - - magnet_hash = magnet_hash_match.group(1).lower() - - # Extract torrent name from magnet link if available - name_match = re.search(r'dn=([^&]+)', magnet_link) - torrent_name = name_match.group(1).replace('+', ' ') if name_match else "Unknown" - - # Record timestamp before adding torrent for identification - before_add_time = time.time() - - self.console.print(f"[cyan]Adding magnet link for: [yellow]{torrent_name}") - - # Prepare save path - if save_path: - self.console.print(f"[cyan]Setting save location to: [green]{save_path}") - - # Ensure save path exists - os.makedirs(save_path, exist_ok=True) - - # Add the torrent with save options - add_options = { - "urls": magnet_link, - "use_auto_torrent_management": False, # Don't use automatic management - "is_paused": False, # Start download immediately - "tags": ["StreamingCommunity"] # Add tag for easy identification - } - - # If save_path is provided, add it to options - if save_path: - add_options["save_path"] = save_path - - add_result = self.qb.torrents_add(**add_options) - - if not add_result == "Ok.": - raise ValueError(f"Failed to add torrent: {add_result}") - - # Wait for torrent to be recognized by the client - time.sleep(1.5) - - # Find the newly added torrent - matching_torrents = self._find_torrent(magnet_hash, before_add_time) - - if not matching_torrents: - raise ValueError("Torrent was added but couldn't be found in client") - - torrent_info = matching_torrents[0] - - # Store relevant information - self.latest_torrent_hash = torrent_info.hash - self.output_file = torrent_info.content_path - self.file_name = torrent_info.name - self.save_path = torrent_info.save_path - - # Display torrent information - self._display_torrent_info(torrent_info) - - # Check download viability after a short delay - time.sleep(3) - self._check_torrent_viability() - - return torrent_info - - def _find_torrent(self, magnet_hash=None, timestamp=None): - """ - Find a torrent by hash or added timestamp. - - Args: - magnet_hash (str, optional): Hash of the torrent to find - timestamp (float, optional): Timestamp to compare against torrent added_on time - - Returns: - list: List of matching torrent objects - """ - # Get list of all torrents with detailed information - torrents = self.qb.torrents_info() - - if magnet_hash: - # First try to find by hash (most reliable) - hash_matches = [t for t in torrents if t.hash.lower() == magnet_hash] - if hash_matches: - return hash_matches - - if timestamp: - # Fallback to finding by timestamp (least recently added torrent after timestamp) - time_matches = [t for t in torrents if getattr(t, 'added_on', 0) > timestamp] - if time_matches: - # Sort by added_on to get the most recently added - return sorted(time_matches, key=lambda t: getattr(t, 'added_on', 0), reverse=True) - - # If we're just looking for the latest torrent - if not magnet_hash and not timestamp: - if torrents: - return [sorted(torrents, key=lambda t: getattr(t, 'added_on', 0), reverse=True)[0]] - - return [] - - def _display_torrent_info(self, torrent_info): - """ - Display detailed information about a torrent. - - Args: - torrent_info: Torrent object from qBittorrent API - """ - self.console.print("\n[bold green]Torrent Details:[/bold green]") - self.console.print(f"[yellow]Name:[/yellow] {torrent_info.name}") - self.console.print(f"[yellow]Hash:[/yellow] {torrent_info.hash}") - #self.console.print(f"[yellow]Size:[/yellow] {internet_manager.format_file_size(torrent_info.size)}") - self.console.print(f"[yellow]Save Path:[/yellow] {torrent_info.save_path}") - - # Show additional metadata if available - if hasattr(torrent_info, 'category') and torrent_info.category: - self.console.print(f"[yellow]Category:[/yellow] {torrent_info.category}") - - if hasattr(torrent_info, 'tags') and torrent_info.tags: - self.console.print(f"[yellow]Tags:[/yellow] {torrent_info.tags}") - - # Show connection info - self.console.print(f"[yellow]Seeds:[/yellow] {torrent_info.num_seeds} complete, {torrent_info.num_complete} connected") - self.console.print(f"[yellow]Peers:[/yellow] {torrent_info.num_leechs} incomplete, {torrent_info.num_incomplete} connected") - print() - - def _check_torrent_viability(self): - """ - Check if the torrent is viable for downloading (has seeds/peers). - Removes the torrent if it doesn't appear to be downloadable. - """ - if not self.latest_torrent_hash: - return - - try: - # Get updated torrent info - torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] - - # Check if torrent has no activity and no source (seeders or peers) - if (torrent_info.dlspeed == 0 and - torrent_info.num_leechs == 0 and - torrent_info.num_seeds == 0 and - torrent_info.state in ('stalledDL', 'missingFiles', 'error')): - - self.console.print("[bold red]Torrent not downloadable. No seeds or peers available. Removing...[/bold red]") - self._remove_torrent(self.latest_torrent_hash) - self.latest_torrent_hash = None - return False - - return True - - except Exception as e: - logging.error(f"Error checking torrent viability: {str(e)}") - return False - - def _remove_torrent(self, torrent_hash, delete_files=True): - """ - Remove a torrent from qBittorrent. - - Args: - torrent_hash (str): Hash of the torrent to remove - delete_files (bool): Whether to delete associated files - """ - try: - self.qb.torrents_delete(delete_files=delete_files, torrent_hashes=torrent_hash) - self.console.print("[yellow]Torrent removed from client[/yellow]") - except Exception as e: - logging.error(f"Error removing torrent: {str(e)}") - - def move_completed_torrent(self, destination): - """ - Move a completed torrent to a new destination using qBittorrent's API - - Args: - destination (str): New destination path - - Returns: - bool: True if successful, False otherwise - """ - if not self.latest_torrent_hash: - self.console.print("[yellow]No active torrent to move[/yellow]") - return False - - try: - # Make sure destination exists - os.makedirs(destination, exist_ok=True) - - # Get current state of the torrent - torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] - - if torrent_info.progress < 1.0: - self.console.print("[yellow]Torrent not yet completed. Cannot move.[/yellow]") - return False - - self.console.print(f"[cyan]Moving torrent to: [green]{destination}") - - # Use qBittorrent API to set location - self.qb.torrents_set_location(location=destination, torrent_hashes=self.latest_torrent_hash) - - # Wait a bit for the move operation to complete - time.sleep(2) - - # Verify move was successful - updated_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] - - if Path(updated_info.save_path) == Path(destination): - self.console.print(f"[bold green]Successfully moved torrent to {destination}[/bold green]") - self.save_path = updated_info.save_path - self.output_file = updated_info.content_path - return True - else: - self.console.print(f"[bold red]Failed to move torrent. Current path: {updated_info.save_path}[/bold red]") - return False - - except Exception as e: - logging.error(f"Error moving torrent: {str(e)}") - self.console.print(f"[bold red]Error moving torrent: {str(e)}[/bold red]") - return False - - def start_download(self): - """ - Start downloading the torrent and monitor its progress with a progress bar. - """ - if not self.latest_torrent_hash: - self.console.print("[yellow]No active torrent to download[/yellow]") - return False - - try: - # Ensure the torrent is started - self.qb.torrents_resume(torrent_hashes=self.latest_torrent_hash) - - # Configure progress bar display format - bar_format = ( - f"{Colors.YELLOW}[TOR] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): " - f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ " - f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]" - ) - - # Initialize progress bar - with tqdm( - total=100, - ascii='β–‘β–’β–ˆ', - bar_format=bar_format, - unit_scale=True, - unit_divisor=1024, - mininterval=0.1 - ) as pbar: - - was_downloading = True - stalled_count = 0 - - while True: - - # Get updated torrent information - try: - torrent_info = self.qb.torrents_info(torrent_hashes=self.latest_torrent_hash)[0] - except (IndexError, qbittorrentapi.exceptions.NotFound404Error): - self.console.print("[bold red]Torrent no longer exists in client[/bold red]") - return False - - # Store the latest path and name - self.save_path = torrent_info.save_path - self.torrent_name = torrent_info.name - self.output_file = torrent_info.content_path - - # Update progress - progress = torrent_info.progress * 100 - pbar.n = progress - - # Get download statistics - download_speed = torrent_info.dlspeed - total_size = torrent_info.size - downloaded_size = torrent_info.downloaded - - # Format sizes and speeds using the existing functions without modification - downloaded_size_str = internet_manager.format_file_size(downloaded_size) - total_size_str = internet_manager.format_file_size(total_size) - download_speed_str = internet_manager.format_transfer_speed(download_speed) - - # Parse the formatted strings to extract numbers and units - # The format is "X.XX Unit" from the format_file_size and format_transfer_speed functions - dl_parts = downloaded_size_str.split(' ') - dl_size_num = dl_parts[0] if len(dl_parts) > 0 else "0" - dl_size_unit = dl_parts[1] if len(dl_parts) > 1 else "B" - - total_parts = total_size_str.split(' ') - total_size_num = total_parts[0] if len(total_parts) > 0 else "0" - total_size_unit = total_parts[1] if len(total_parts) > 1 else "B" - - speed_parts = download_speed_str.split(' ') - speed_num = speed_parts[0] if len(speed_parts) > 0 else "0" - speed_unit = ' '.join(speed_parts[1:]) if len(speed_parts) > 1 else "B/s" - - # Check if download is active - currently_downloading = download_speed > 0 - - # Handle stalled downloads - if was_downloading and not currently_downloading and progress < 100: - stalled_count += 1 - if stalled_count >= 15: # 3 seconds (15 * 0.2) - pbar.set_description(f"{Colors.RED}Stalled") - else: - stalled_count = 0 - pbar.set_description(f"{Colors.GREEN}Active") - - was_downloading = currently_downloading - - # Update progress bar display with formatted statistics - pbar.set_postfix_str( - f"{Colors.GREEN}{dl_size_num} {Colors.RED}{dl_size_unit} {Colors.WHITE}< " - f"{Colors.GREEN}{total_size_num} {Colors.RED}{total_size_unit}{Colors.WHITE}, " - f"{Colors.CYAN}{speed_num} {Colors.RED}{speed_unit}" - ) - pbar.refresh() - - # Check for completion - if int(progress) == 100: - pbar.n = 100 - pbar.refresh() - break - - # Check torrent state for errors - if torrent_info.state in ('error', 'missingFiles', 'unknown'): - self.console.print(f"[bold red]Error in torrent: {torrent_info.state}[/bold red]") - return False - - time.sleep(0.3) - - self.console.print(f"[bold green]Download complete: {self.torrent_name}[/bold green]") - return True - - except KeyboardInterrupt: - self.console.print("[yellow]Download process interrupted[/yellow]") - return False - - except Exception as e: - logging.error(f"Error monitoring download: {str(e)}") - self.console.print(f"[bold red]Error monitoring download: {str(e)}[/bold red]") - return False - - def is_file_in_use(self, file_path): - """ - Check if a file is currently being used by any process. - - Args: - file_path (str): Path to the file to check - - Returns: - bool: True if file is in use, False otherwise - """ - # Convert to absolute path for consistency - file_path = str(Path(file_path).resolve()) - - try: - for proc in psutil.process_iter(['open_files', 'name']): - try: - proc_info = proc.info - if 'open_files' in proc_info and proc_info['open_files']: - for file_info in proc_info['open_files']: - if file_path == file_info.path: - return True - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - return False - - except Exception as e: - logging.error(f"Error checking if file is in use: {str(e)}") - return False - - def cleanup(self): - """ - Clean up resources and perform final operations before shutting down. - """ - if self.latest_torrent_hash: - self._remove_torrent(self.latest_torrent_hash) - - try: - self.qb.auth_log_out() - except Exception: - pass \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/__init__.py b/StreamingCommunity/Lib/Downloader/__init__.py deleted file mode 100644 index 779fbc2eb..000000000 --- a/StreamingCommunity/Lib/Downloader/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# 23.06.24 - -from .HLS.downloader import HLS_Downloader -from .MP4.downloader import MP4_downloader -from .TOR.downloader import TOR_downloader -from .DASH.downloader import DASH_Downloader -from .MEGA.mega import Mega_Downloader - -__all__ = [ - "HLS_Downloader", - "MP4_downloader", - "TOR_downloader", - "DASH_Downloader", - "Mega_Downloader" -] \ No newline at end of file diff --git a/StreamingCommunity/Lib/FFmpeg/__init__.py b/StreamingCommunity/Lib/FFmpeg/__init__.py index 15e13bd7d..95883ea54 100644 --- a/StreamingCommunity/Lib/FFmpeg/__init__.py +++ b/StreamingCommunity/Lib/FFmpeg/__init__.py @@ -1,9 +1,8 @@ # 18.04.24 -from .command import join_video, join_audios, join_subtitle +from .merge import join_video, join_audios, join_subtitle from .util import print_duration_table, get_video_duration - __all__ = [ "join_video", "join_audios", diff --git a/StreamingCommunity/Lib/FFmpeg/command.py b/StreamingCommunity/Lib/FFmpeg/merge.py similarity index 96% rename from StreamingCommunity/Lib/FFmpeg/command.py rename to StreamingCommunity/Lib/FFmpeg/merge.py index d803228f0..0b7f3ce29 100644 --- a/StreamingCommunity/Lib/FFmpeg/command.py +++ b/StreamingCommunity/Lib/FFmpeg/merge.py @@ -17,7 +17,7 @@ # Logic class from .util import need_to_force_to_ts, check_duration_v_a from .capture import capture_ffmpeg_real_time -from ..M3U8 import M3U8_Codec +from ..HLS.parser import M3U8_Codec # Config @@ -75,6 +75,7 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None): # Output file and overwrite ffmpeg_cmd.extend([out_path, '-y']) + logging.info(f"FFMPEG Command: {' '.join(ffmpeg_cmd)} \n") # Run join if DEBUG_MODE: @@ -122,7 +123,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s if use_shortest: for track in duration_diffs: color = "red" if track['has_error'] else "green" - console.print(f"[{color}]Audio {track['language']}: Video duration: {track['video_duration']:.2f}s, Audio duration: {track['audio_duration']:.2f}s, Difference: {track['difference']:.2f}s[/{color}]") + console.print(f"[{color}]Audio {track['language']}: Video duration: {track['video_duration']:.2f}s, Audio duration: {track['audio_duration']:.2f}s, Difference: {track['difference']:.2f}s[/{color}] \n") # Start command with locate ffmpeg ffmpeg_cmd = [get_ffmpeg_path()] @@ -153,6 +154,7 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s # Output file and overwrite ffmpeg_cmd.extend([out_path, '-y']) + logging.info(f"FFMPEG Command: {' '.join(ffmpeg_cmd)} \n") # Run join if DEBUG_MODE: @@ -197,7 +199,7 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat # Overwrite ffmpeg_cmd += [out_path, "-y"] - logging.info(f"FFmpeg command: {ffmpeg_cmd}") + logging.info(f"FFMPEG Command: {' '.join(ffmpeg_cmd)} \n") # Run join if DEBUG_MODE: diff --git a/StreamingCommunity/Lib/FFmpeg/util.py b/StreamingCommunity/Lib/FFmpeg/util.py index 914cba018..d782abf54 100644 --- a/StreamingCommunity/Lib/FFmpeg/util.py +++ b/StreamingCommunity/Lib/FFmpeg/util.py @@ -20,33 +20,6 @@ console = Console() -def has_audio_stream(video_path: str) -> bool: - """ - Check if the input video has an audio stream. - - Parameters: - - video_path (str): Path to the input video file. - - Returns: - has_audio (bool): True if the input video has an audio stream, False otherwise. - """ - try: - ffprobe_cmd = [get_ffprobe_path(), '-v', 'error', '-print_format', 'json', '-select_streams', 'a', '-show_streams', video_path] - logging.info(f"FFmpeg command: {ffprobe_cmd}") - - with subprocess.Popen(ffprobe_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as proc: - stdout, stderr = proc.communicate() - if stderr: - logging.error(f"Error: {stderr}") - else: - probe_result = json.loads(stdout) - return bool(probe_result.get('streams', [])) - - except Exception as e: - logging.error(f"Error: {e}") - return False - - def get_video_duration(file_path: str, file_type: str = "file") -> float: """ Get the duration of a media file (video or audio). @@ -233,15 +206,15 @@ def check_duration_v_a(video_path, audio_path, tolerance=1.0): # Check if either duration is None and specify which one is None if video_duration is None and audio_duration is None: - console.print("[yellow]Warning: Both video and audio durations are None. Returning 0 as duration difference.[/yellow]") + console.print("[yellow]Warning: Both video and audio durations are None. Returning 0 as duration difference.") return False, 0.0, 0.0, 0.0 elif video_duration is None: - console.print("[yellow]Warning: Video duration is None. Using audio duration for calculation.[/yellow]") + console.print("[yellow]Warning: Video duration is None. Using audio duration for calculation.") return False, 0.0, 0.0, audio_duration elif audio_duration is None: - console.print("[yellow]Warning: Audio duration is None. Using video duration for calculation.[/yellow]") + console.print("[yellow]Warning: Audio duration is None. Using video duration for calculation.") return False, 0.0, video_duration, 0.0 # Calculate the duration difference diff --git a/StreamingCommunity/Lib/HLS/__init__.py b/StreamingCommunity/Lib/HLS/__init__.py new file mode 100644 index 000000000..2a111a9ae --- /dev/null +++ b/StreamingCommunity/Lib/HLS/__init__.py @@ -0,0 +1,18 @@ +# 17.12.25 + +from .downloader import HLS_Downloader +from .decrypt import M3U8_Decryption +from .estimator import M3U8_Ts_Estimator +from .parser import M3U8_Parser +from .segments import M3U8_Segments +from .url_fixer import M3U8_UrlFix + + +__all__ = [ + "HLS_Downloader", + "M3U8_Decryption", + "M3U8_Ts_Estimator", + "M3U8_Parser", + "M3U8_Segments", + "M3U8_UrlFix", +] \ No newline at end of file diff --git a/StreamingCommunity/Lib/M3U8/decryptor.py b/StreamingCommunity/Lib/HLS/decrypt.py similarity index 82% rename from StreamingCommunity/Lib/M3U8/decryptor.py rename to StreamingCommunity/Lib/HLS/decrypt.py index 160b7afd6..9f898ae44 100644 --- a/StreamingCommunity/Lib/M3U8/decryptor.py +++ b/StreamingCommunity/Lib/HLS/decrypt.py @@ -19,21 +19,15 @@ crypto_spec = importlib.util.find_spec("Cryptodome") crypto_installed = crypto_spec is not None - if not crypto_installed: console.log("[red]pycryptodomex non Γ¨ installato. Per favore installalo. Leggi readme.md [Requirement].") sys.exit(0) - - logging.info("[cyan]Decryption use: Cryptodomex") class M3U8_Decryption: - """ - Class for decrypting M3U8 playlist content using AES with pycryptodomex. - """ - def __init__(self, key: bytes, iv: bytes, method: str) -> None: + def __init__(self, key: bytes, iv: bytes, method: str, pssh: bytes = None) -> None: """ Initialize the M3U8_Decryption object. @@ -47,6 +41,7 @@ def __init__(self, key: bytes, iv: bytes, method: str) -> None: if "0x" in str(iv): self.iv = bytes.fromhex(iv.replace("0x", "")) self.method = method + self.pssh = pssh # Pre-create the cipher based on the encryption method if self.method == "AES": @@ -55,8 +50,15 @@ def __init__(self, key: bytes, iv: bytes, method: str) -> None: self.cipher = AES.new(self.key[:16], AES.MODE_CBC, iv=self.iv) elif self.method == "AES-128-CTR": self.cipher = AES.new(self.key[:16], AES.MODE_CTR, nonce=self.iv) - else: - raise ValueError("Invalid or unsupported method") + + message = None + if self.method is not None: + message = f"Method: [green]{self.method}" + if self.key is not None: + message += f" | Key: [green]{self.key.hex()}" + if self.iv is not None: + message += f" | IV: [green]{self.iv.hex()}" + console.log(f"[cyan]Decryption {message}") def decrypt(self, ciphertext: bytes) -> bytes: """ @@ -68,8 +70,6 @@ def decrypt(self, ciphertext: bytes) -> bytes: Returns: bytes: The decrypted content. """ - #start = time.perf_counter_ns() - if self.method in {"AES", "AES-128"}: decrypted_data = self.cipher.decrypt(ciphertext) decrypted_content = unpad(decrypted_data, AES.block_size) diff --git a/StreamingCommunity/Lib/Downloader/HLS/downloader.py b/StreamingCommunity/Lib/HLS/downloader.py similarity index 82% rename from StreamingCommunity/Lib/Downloader/HLS/downloader.py rename to StreamingCommunity/Lib/HLS/downloader.py index b5c04d93d..4a81baeb7 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/downloader.py +++ b/StreamingCommunity/Lib/HLS/downloader.py @@ -1,6 +1,7 @@ # 17.10.24 import os +import sys import logging import shutil from typing import Any, Dict, List, Optional, Union @@ -12,24 +13,20 @@ # Internal utilities -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import fetch -from StreamingCommunity.Util.os import os_manager, internet_manager +from StreamingCommunity.Util import config_manager, os_manager, internet_manager +from StreamingCommunity.Util.http_client import fetch, get_userAgent # Logic class -from ...FFmpeg import ( - print_duration_table, - join_video, - join_audios, - join_subtitle -) -from ...M3U8 import M3U8_Parser, M3U8_UrlFix +from StreamingCommunity.Lib.FFmpeg import print_duration_table, join_video, join_audios, join_subtitle +from .parser import M3U8_Parser +from .url_fixer import M3U8_UrlFix from .segments import M3U8_Segments + # Config +console = Console() DOWNLOAD_SPECIFIC_AUDIO = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_audio') DOWNLOAD_SPECIFIC_SUBTITLE = config_manager.get_list('M3U8_DOWNLOAD', 'specific_list_subtitles') MERGE_SUBTITLE = config_manager.get_bool('M3U8_DOWNLOAD', 'merge_subs') @@ -38,8 +35,6 @@ FILTER_CUSTOM_RESOLUTION = str(config_manager.get('M3U8_CONVERSION', 'force_resolution')).strip().lower() EXTENSION_OUTPUT = config_manager.get("M3U8_CONVERSION", "extension") -console = Console() - class HLSClient: """Client for making HTTP requests to HLS endpoints with retry mechanism.""" @@ -66,6 +61,7 @@ def request(self, url: str, return_content: bool = False) -> Optional[Union[str, url, method="GET", headers=self.headers, + timeout=15, return_content=return_content ) @@ -144,6 +140,7 @@ def parse(self) -> bool: content = self.client.request(self.m3u8_url) if not content: logging.error(f"Failed to fetch M3U8 content from {self.m3u8_url}") + sys.exit(0) return False self.parser.parse_data(uri=self.m3u8_url, raw_content=content) @@ -198,7 +195,7 @@ def select_streams(self): # 3. Filters are configured (not empty) if not self.audio_streams and all_audio and DOWNLOAD_SPECIFIC_AUDIO: first_audio_lang = all_audio[0].get('language', 'unknown') - console.print(f"\n[yellow]Auto-selecting first available audio track: {first_audio_lang}[/yellow]") + console.print(f"[yellow]Auto-selecting first available audio track: {first_audio_lang}") self.audio_streams = [all_audio[0]] # Subtitle selection @@ -233,7 +230,7 @@ def calculate_column_widths(): available_subtitles = self.parser._subtitle.get_all_uris_and_names() or [] if available_subtitles: available_sub_languages = [sub.get('language') for sub in available_subtitles] - available_subs = ', '.join(available_sub_languages) + available_subs = ', '.join(available_sub_languages) if available_sub_languages else "Nothing" downloadable_sub_languages = [sub.get('language') for sub in self.sub_streams] downloadable_subs = ', '.join(downloadable_sub_languages) if downloadable_sub_languages else "Nothing" @@ -264,7 +261,7 @@ def calculate_column_widths(): data_rows, column_widths = calculate_column_widths() - table = Table(show_header=True, header_style="bold cyan", border_style="blue") + table = Table(show_header=True, header_style="cyan", border_style="blue") table.add_column("Type", style="cyan bold", width=column_widths[0]) table.add_column("Available", style="green", width=column_widths[1]) table.add_column("Set", style="red", width=column_widths[2]) @@ -279,7 +276,7 @@ def calculate_column_widths(): class DownloadManager: """Manages downloading of video, audio, and subtitle streams.""" - def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix, custom_headers: Optional[Dict[str, str]] = None): + def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix, license_url: Optional[str] = None, custom_headers: Optional[Dict[str, str]] = None): """ Args: temp_dir: Directory for storing temporary files @@ -290,10 +287,13 @@ def __init__(self, temp_dir: str, client: HLSClient, url_fixer: M3U8_UrlFix, cus self.temp_dir = temp_dir self.client = client self.url_fixer = url_fixer + self.license_url = license_url self.custom_headers = custom_headers self.missing_segments = [] self.stopped = False self.video_segments_count = 0 + self.video_output_path = None + self.audio_output_paths = {} # For progress tracking self.current_downloader: Optional[M3U8_Segments] = None @@ -306,42 +306,40 @@ def download_video(self, video_url: str) -> bool: Returns: bool: True if download was successful, False otherwise """ - try: - video_full_url = self.url_fixer.generate_full_url(video_url) - video_tmp_dir = os.path.join(self.temp_dir, 'video') - - # Create downloader without segment limit for video - downloader = M3U8_Segments( - url=video_full_url, - tmp_folder=video_tmp_dir, - custom_headers=self.custom_headers - ) + video_full_url = self.url_fixer.generate_full_url(video_url) + video_tmp_dir = os.path.join(self.temp_dir, 'video') + + # Create downloader without segment limit for video + downloader = M3U8_Segments( + url=video_full_url, + tmp_folder=video_tmp_dir, + license_url=self.license_url, + custom_headers=self.custom_headers + ) - # Set current downloader for progress tracking - self.current_downloader = downloader - self.current_download_type = 'video' - - # Download video and get segment count - result = downloader.download_streams("Video", "video") - self.video_segments_count = downloader.get_segments_count() - self.missing_segments.append(result) + # Set current downloader for progress tracking + self.current_downloader = downloader + self.current_download_type = 'video' + + # Download video and get segment count + result = downloader.download_streams("Video", "video") + self.video_segments_count = downloader.get_segments_count() + self.missing_segments.append(result) + + # Store the actual output path from the result + if 'output_path' in result and result['output_path']: + self.video_output_path = result['output_path'] - # Reset current downloader after completion - self.current_downloader = None - self.current_download_type = None + # Reset current downloader after completion + self.current_downloader = None + self.current_download_type = None - if result.get('stopped', False): - self.stopped = True - return False - - return True - - except Exception as e: - logging.error(f"Error downloading video from {video_url}: {str(e)}") - self.current_downloader = None - self.current_download_type = None + if result.get('stopped', False): + self.stopped = True return False + return True + def download_audio(self, audio: Dict) -> bool: """ Downloads audio segments for a specific language track. @@ -358,6 +356,7 @@ def download_audio(self, audio: Dict) -> bool: downloader = M3U8_Segments( url=audio_full_url, tmp_folder=audio_tmp_dir, + license_url=self.license_url, limit_segments=self.video_segments_count if self.video_segments_count > 0 else None, custom_headers=self.custom_headers ) @@ -369,6 +368,10 @@ def download_audio(self, audio: Dict) -> bool: # Download audio result = downloader.download_streams(f"Audio {audio['language']}", "audio") self.missing_segments.append(result) + + # Store the actual output path from the result + if 'output_path' in result and result['output_path']: + self.audio_output_paths[audio['language']] = result['output_path'] # Reset current downloader after completion self.current_downloader = None @@ -423,42 +426,36 @@ def download_all(self, video_url: str, audio_streams: List[Dict], sub_streams: L bool: True if any critical download failed and should stop processing """ critical_failure = False - video_file = os.path.join(self.temp_dir, 'video', '0.ts') # Download video (this is critical) - if not os.path.exists(video_file): - if not self.download_video(video_url): - logging.error("Critical failure: Video download failed") - critical_failure = True + if not self.download_video(video_url): + logging.error("Critical failure: Video download failed") + critical_failure = True # Download audio streams (continue even if some fail) for audio in audio_streams: if self.stopped: break - audio_file = os.path.join(self.temp_dir, 'audio', audio['language'], '0.ts') - if not os.path.exists(audio_file): - success = self.download_audio(audio) - if not success: - logging.warning(f"Audio download failed for language {audio.get('language', 'unknown')}, continuing...") + success = self.download_audio(audio) + if not success: + logging.warning(f"Audio download failed for language {audio.get('language', 'unknown')}, continuing...") # Download subtitle streams (continue even if some fail) for sub in sub_streams: if self.stopped: break - sub_file = os.path.join(self.temp_dir, 'subs', f"{sub['language']}.vtt") - if not os.path.exists(sub_file): - success = self.download_subtitle(sub) - if not success: - logging.warning(f"Subtitle download failed for language {sub.get('language', 'unknown')}, continuing...") + success = self.download_subtitle(sub) + if not success: + logging.warning(f"Subtitle download failed for language {sub.get('language', 'unknown')}, continuing...") return critical_failure or self.stopped class MergeManager: """Handles merging of video, audio, and subtitle streams.""" - def __init__(self, temp_dir: str, parser: M3U8_Parser, audio_streams: List[Dict], sub_streams: List[Dict]): + def __init__(self, temp_dir: str, parser: M3U8_Parser, audio_streams: List[Dict], sub_streams: List[Dict], video_output_path: Optional[str] = None, audio_output_paths: Optional[Dict[str, str]] = None): """ Args: temp_dir: Directory containing temporary files @@ -470,6 +467,44 @@ def __init__(self, temp_dir: str, parser: M3U8_Parser, audio_streams: List[Dict] self.parser = parser self.audio_streams = audio_streams self.sub_streams = sub_streams + self.video_output_path = video_output_path + self.audio_output_paths = audio_output_paths or {} + + def _get_video_file(self) -> str: + """Get the actual video file path from the provided output path.""" + if self.video_output_path and os.path.exists(self.video_output_path): + return self.video_output_path + + # Fallback to check common locations if output_path wasn't set + default_ts = os.path.join(self.temp_dir, 'video', '0.ts') + if os.path.exists(default_ts): + return default_ts + + default_mp4 = os.path.join(self.temp_dir, 'video', 'video_decrypted.mp4') + if os.path.exists(default_mp4): + return default_mp4 + + raise FileNotFoundError(f"Video file not found. Expected at: {self.video_output_path}") + + def _get_audio_file(self, language: str) -> Optional[str]: + """Get the actual audio file path for a given language from the stored output path.""" + if language in self.audio_output_paths: + audio_path = self.audio_output_paths[language] + if os.path.exists(audio_path): + return audio_path + else: + logging.warning(f"Audio file not found at stored path: {audio_path}") + + # Fallback to check common locations if output_path wasn't stored + default_ts = os.path.join(self.temp_dir, 'audio', language, '0.ts') + if os.path.exists(default_ts): + return default_ts + + default_mp4 = os.path.join(self.temp_dir, 'audio', language, 'audio_decrypted.mp4') + if os.path.exists(default_mp4): + return default_mp4 + + return None def merge(self) -> tuple[str, bool]: """ @@ -481,7 +516,7 @@ def merge(self) -> tuple[str, bool]: 2. If audio exists, merge with video 3. If subtitles exist, add them to the video """ - video_file = os.path.join(self.temp_dir, 'video', '0.ts') + video_file = self._get_video_file() merged_file = video_file use_shortest = False @@ -498,8 +533,8 @@ def merge(self) -> tuple[str, bool]: # Only include audio tracks that actually exist existing_audio_tracks = [] for a in self.audio_streams: - audio_path = os.path.join(self.temp_dir, 'audio', a['language'], '0.ts') - if os.path.exists(audio_path): + audio_path = self._get_audio_file(a['language']) + if audio_path and os.path.exists(audio_path): existing_audio_tracks.append({ 'path': audio_path, 'name': a['language'] @@ -539,11 +574,12 @@ def merge(self) -> tuple[str, bool]: class HLS_Downloader: """Main class for HLS video download and processing.""" - def __init__(self, m3u8_url: str, output_path: Optional[str] = None, headers: Optional[Dict[str, str]] = None): + def __init__(self, m3u8_url: str, license_url: Optional[str] = None, output_path: Optional[str] = None, headers: Optional[Dict[str, str]] = None): """ Initializes the HLS_Downloader with parameters. """ - self.m3u8_url = m3u8_url + self.m3u8_url = str(m3u8_url).strip() + self.license_url = str(license_url).strip() if license_url else None self.path_manager = PathManager(m3u8_url, output_path) self.custom_headers = headers self.client = HLSClient(custom_headers=self.custom_headers) @@ -567,7 +603,7 @@ def start(self) -> Dict[str, Any]: """ if GET_ONLY_LINK: - console.print(f"URL: [bold red]{self.m3u8_url}[/bold red]") + console.print(f"URL: [red]{self.m3u8_url}") return { 'path': None, 'url': self.m3u8_url, @@ -577,11 +613,9 @@ def start(self) -> Dict[str, Any]: 'stopped': True } - console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]") - try: if os.path.exists(self.path_manager.output_path): - console.print(f"[red]Output file {self.path_manager.output_path} already exists![/red]") + console.print(f"[red]Output file {self.path_manager.output_path} already exists!") response = { 'path': self.path_manager.output_path, 'url': self.m3u8_url, @@ -606,6 +640,7 @@ def start(self) -> Dict[str, Any]: temp_dir=self.path_manager.temp_dir, client=self.client, url_fixer=self.m3u8_manager.url_fixer, + license_url=self.license_url, custom_headers=self.custom_headers ) @@ -618,7 +653,7 @@ def start(self) -> Dict[str, Any]: if download_failed: error_msg = "Critical download failure occurred" - console.print(f"[red]Download failed: {error_msg}[/red]") + console.print(f"[red]Download failed: {error_msg}") self.path_manager.cleanup() return { 'path': None, @@ -633,7 +668,9 @@ def start(self) -> Dict[str, Any]: temp_dir=self.path_manager.temp_dir, parser=self.m3u8_manager.parser, audio_streams=self.m3u8_manager.audio_streams, - sub_streams=self.m3u8_manager.sub_streams + sub_streams=self.m3u8_manager.sub_streams, + video_output_path=self.download_manager.video_output_path, + audio_output_paths=self.download_manager.audio_output_paths ) final_file, use_shortest = self.merge_manager.merge() @@ -651,7 +688,7 @@ def start(self) -> Dict[str, Any]: } except KeyboardInterrupt: - console.print("\n[yellow]Download interrupted by user[/yellow]") + console.print("\n[yellow]Download interrupted by user") self.path_manager.cleanup() return { 'path': None, @@ -664,7 +701,7 @@ def start(self) -> Dict[str, Any]: except Exception as e: error_msg = str(e) - console.print(f"[red]Download failed: {error_msg}[/red]") + console.print(f"[red]Download failed: {error_msg}") logging.error(f"Download error for {self.m3u8_url}", exc_info=True) # Cleanup on error @@ -687,7 +724,7 @@ def _print_summary(self, use_shortest: bool): 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]" + missing_info += f"[red]TS Failed: {item['nFailed']} {item['type']} tracks" 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/M3U8/estimator.py b/StreamingCommunity/Lib/HLS/estimator.py similarity index 96% rename from StreamingCommunity/Lib/M3U8/estimator.py rename to StreamingCommunity/Lib/HLS/estimator.py index d8f1edeaa..f4940876f 100644 --- a/StreamingCommunity/Lib/M3U8/estimator.py +++ b/StreamingCommunity/Lib/HLS/estimator.py @@ -13,8 +13,7 @@ # Internal utilities -from StreamingCommunity.Util.color import Colors -from StreamingCommunity.Util.os import internet_manager +from StreamingCommunity.Util import internet_manager, Colors class M3U8_Ts_Estimator: @@ -167,11 +166,6 @@ def stop(self): self._running = False if self.speed_thread.is_alive(): self.speed_thread.join(timeout=5.0) - - def get_speed_data(self) -> Dict[str, str]: - """Returns current speed data thread-safe.""" - with self.lock: - return self.speed.copy() def get_average_segment_size(self) -> int: """Returns average segment size in bytes.""" diff --git a/StreamingCommunity/Lib/M3U8/parser.py b/StreamingCommunity/Lib/HLS/parser.py similarity index 89% rename from StreamingCommunity/Lib/M3U8/parser.py rename to StreamingCommunity/Lib/HLS/parser.py index 9ab65e72b..50162cb3b 100644 --- a/StreamingCommunity/Lib/M3U8/parser.py +++ b/StreamingCommunity/Lib/HLS/parser.py @@ -6,7 +6,6 @@ # Internal utilities from m3u8 import loads -from StreamingCommunity.Util.os import internet_manager # Costant @@ -98,7 +97,6 @@ def convert_video_codec(self, video_codec_identifier) -> str: str: Codec name corresponding to the identifier. """ if not video_codec_identifier: - logging.warning("No video codec identifier provided. Using default codec libx264.") return "libx264" # Default # Extract codec type from the identifier @@ -111,7 +109,6 @@ def convert_video_codec(self, video_codec_identifier) -> str: if codec_name: return codec_name else: - logging.warning(f"No corresponding video codec found for {video_codec_identifier}. Using default codec libx264.") return "libx264" # Default def convert_audio_codec(self, audio_codec_identifier) -> str: @@ -237,24 +234,6 @@ def get_list_resolution(self): """ return [video['resolution'] for video in self.video_playlist] - def get_list_resolution_and_size(self, duration): - """ - Retrieve a list of resolutions and size from the video playlist. - - Parameters: - - duration (int): Total duration of the video in 's'. - - Returns: - list: A list of resolutions extracted from the video playlist. - """ - result = [] - - for video in self.video_playlist: - video_size = internet_manager.format_file_size((video['bandwidth'] * duration) / 8) - result.append((video_size)) - - return result - class M3U8_Audio: def __init__(self, audio_playlist) -> None: @@ -527,21 +506,29 @@ def __parse_encryption_keys__(self, obj) -> None: if hasattr(obj, 'key') and obj.key is not None: key_info = { 'method': obj.key.method, + 'uri': obj.key.uri, + 'base_uri': obj.key.base_uri, 'iv': obj.key.iv, - 'uri': obj.key.uri + 'key_format': obj.key.keyformat, + 'key_format_versions': obj.key.keyformatversions, + 'drm': None } if self.keys is None: self.keys = key_info - """ - elif obj.key.uri not in self.keys: - if isinstance(self.keys, dict): - self.keys[obj.key.uri] = key_info - else: - old_key = self.keys - self.keys = {'default': old_key, obj.key.uri: key_info} - """ + # Determine DRM type based on method + if "SAMPLE-AES-CTR" in key_info['method']: + self.keys['drm'] = 'widevine' + self.keys['pssh'] = key_info['uri'].split('base64,')[1] + + elif "PLAYREADY" in key_info['method']: + self.keys['drm'] = 'playready' + elif "SAMPLE-AES" in key_info['method']: + self.keys['drm'] = 'fairplay' + else: + self.keys['drm'] = None + self.keys['pssh'] = None except Exception as e: logging.error(f"Error parsing encryption keys: {e}") @@ -618,28 +605,4 @@ def __create_variable__(self): """ self._video = M3U8_Video(self.video_playlist) self._audio = M3U8_Audio(self.audio_playlist) - self._subtitle = M3U8_Subtitle(self.subtitle_playlist) - - def get_duration(self, return_string:bool = True): - """ - Convert duration from seconds to hours, minutes, and remaining seconds. - - Parameters: - - return_string (bool): If True, returns the formatted duration string. - If False, returns a dictionary with hours, minutes, and seconds. - - Returns: - - formatted_duration (str): Formatted duration string with hours, minutes, and seconds if return_string is True. - - duration_dict (dict): Dictionary with keys 'h', 'm', 's' representing hours, minutes, and seconds respectively if return_string is False. - """ - - # Calculate hours, minutes, and remaining seconds - hours, remainder = divmod(self.duration, 3600) - minutes, seconds = divmod(remainder, 60) - - - # Format the duration string with colors - if return_string: - return f"[yellow]{int(hours)}[red]h [yellow]{int(minutes)}[red]m [yellow]{int(seconds)}[red]s" - else: - return {'h': int(hours), 'm': int(minutes), 's': int(seconds)} \ No newline at end of file + self._subtitle = M3U8_Subtitle(self.subtitle_playlist) \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Lib/HLS/segments.py similarity index 72% rename from StreamingCommunity/Lib/Downloader/HLS/segments.py rename to StreamingCommunity/Lib/HLS/segments.py index deb096b75..20a801d76 100644 --- a/StreamingCommunity/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Lib/HLS/segments.py @@ -16,19 +16,23 @@ # Internal utilities -from StreamingCommunity.Util.color import Colors -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client_curl -from StreamingCommunity.Util.config_json import config_manager +from StreamingCommunity.Util import config_manager, Colors +from StreamingCommunity.Util.http_client import create_client_curl, get_userAgent +from StreamingCommunity.Util.os import get_wvd_path + # Logic class -from ...M3U8 import ( - M3U8_Decryption, - M3U8_Ts_Estimator, - M3U8_Parser, - M3U8_UrlFix -) +from .decrypt import M3U8_Decryption +from .estimator import M3U8_Ts_Estimator +from .parser import M3U8_Parser +from .url_fixer import M3U8_UrlFix + + +# External +from ..MP4 import MP4_Downloader +from ..DASH.cdm_helpher import get_widevine_keys +from ..DASH.decrypt import decrypt_with_mp4decrypt # Config @@ -47,7 +51,7 @@ class M3U8_Segments: - def __init__(self, url: str, tmp_folder: str, is_index_url: bool = True, limit_segments: int = None, custom_headers: Optional[Dict[str, str]] = None): + def __init__(self, url: str, tmp_folder: str, license_url: Optional[str] = None, is_index_url: bool = True, limit_segments: int = None, custom_headers: Optional[Dict[str, str]] = None): """ Initializes the M3U8_Segments object. @@ -60,9 +64,11 @@ def __init__(self, url: str, tmp_folder: str, is_index_url: bool = True, limit_s """ self.url = url self.tmp_folder = tmp_folder + self.license_url = license_url self.is_index_url = is_index_url self.custom_headers = custom_headers if custom_headers else {'User-Agent': get_userAgent()} self.final_output_path = os.path.join(self.tmp_folder, "0.ts") + self.drm_method = None os.makedirs(self.tmp_folder, exist_ok=True) # Use LIMIT_SEGMENT from config if limit_segments not specified or is 0 @@ -93,20 +99,26 @@ def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes: """ Fetches the encryption key from the M3U8 playlist. """ - key_uri = urljoin(self.url, m3u8_parser.keys.get('uri')) - parsed_url = urlparse(key_uri) - self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/" - - try: - response = create_client_curl(headers=self.custom_headers).get(key_uri) - response.raise_for_status() + if m3u8_parser.keys.get('drm') is None: + key_uri = urljoin(self.url, m3u8_parser.keys.get('uri')) + parsed_url = urlparse(key_uri) + self.key_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/" - hex_content = binascii.hexlify(response.content).decode('utf-8') - return bytes.fromhex(hex_content) + try: + response = create_client_curl(headers=self.custom_headers).get(key_uri) + response.raise_for_status() + + hex_content = binascii.hexlify(response.content).decode('utf-8') + console.log(f"[cyan]Fetch key from URI: [green]{key_uri}") + return bytes.fromhex(hex_content) + + except Exception as e: + raise Exception(f"Failed to fetch key: {e}") - except Exception as e: - raise Exception(f"Failed to fetch key: {e}") - + else: + self.drm_method = m3u8_parser.keys.get('method') + logging.info("DRM key detected, method: " + str(m3u8_parser.keys.get('method'))) + def parse_data(self, m3u8_content: str) -> None: """Parses the M3U8 content and extracts necessary data.""" m3u8_parser = M3U8_Parser() @@ -117,8 +129,8 @@ def parse_data(self, m3u8_content: str) -> None: self.has_init_segment = self.segment_init_url is not None if m3u8_parser.keys: - key = self.__get_key__(m3u8_parser) - self.decryption = M3U8_Decryption(key, m3u8_parser.keys.get('iv'), m3u8_parser.keys.get('method')) + key = self.__get_key__(m3u8_parser) + self.decryption = M3U8_Decryption(key, m3u8_parser.keys.get('iv'), m3u8_parser.keys.get('method'), m3u8_parser.keys.get('pssh')) segments = [ self.class_url_fixer.generate_full_url(seg) if "http" not in seg else seg @@ -131,30 +143,42 @@ def parse_data(self, m3u8_content: str) -> None: segments = segments[:self.limit_segments] self.segments = segments + self.stream_type = self.get_type_stream(self.segments) self.class_ts_estimator.total_segments = len(self.segments) + console.log(f"[cyan]Detected stream type: [green]{self.stream_type}") def get_segments_count(self) -> int: """ Returns the total number of segments. """ return len(self.segments) if hasattr(self, 'segments') else 0 + + def get_type_stream(self, segments) -> str: + self.is_stream_ts = (".ts" in self.segments[len(self.segments) // 2]) if self.segments else False + self.is_stream_mp4 = (".mp4" in self.segments[len(self.segments) // 2]) if self.segments else False + self.is_stream_aac = (".aac" in self.segments[len(self.segments) // 2]) if self.segments else False + + if self.is_stream_ts: + return "ts" + elif self.is_stream_mp4: + return "mp4" + elif self.is_stream_aac: + return "aac" + else: + return None def get_info(self) -> None: """ Retrieves M3U8 playlist information from the given URL. """ if self.is_index_url: - try: - response = create_client_curl(headers=self.custom_headers).get(self.url) - response.raise_for_status() - - self.parse_data(response.text) - with open(os.path.join(self.tmp_folder, "playlist.m3u8"), "w") as f: - f.write(response.text) + response = create_client_curl(headers=self.custom_headers).get(self.url) + response.raise_for_status() + + self.parse_data(response.text) + with open(os.path.join(self.tmp_folder, "playlist.m3u8"), "w") as f: + f.write(response.text) - except Exception as e: - raise RuntimeError(f"M3U8 info retrieval failed: {e}") - def _throttled_progress_update(self, content_size: int, progress_bar: tqdm): """ Throttled progress update to reduce CPU usage. @@ -211,8 +235,7 @@ async def _download_init_segment(self, client: httpx.AsyncClient, output_path: s pass return False - async def _download_single_segment(self, client: httpx.AsyncClient, ts_url: str, index: int, temp_dir: str, - semaphore: asyncio.Semaphore, max_retry: int) -> tuple: + async def _download_single_segment(self, client: httpx.AsyncClient, ts_url: str, index: int, temp_dir: str, semaphore: asyncio.Semaphore, max_retry: int) -> tuple: """ Downloads a single TS segment and saves to temp file IMMEDIATELY. @@ -296,8 +319,7 @@ async def _download_all_segments(self, client: httpx.AsyncClient, temp_dir: str, if self.enable_retry and not self.download_interrupted: await self._retry_failed_segments(client, temp_dir, semaphore, progress_bar) - async def _retry_failed_segments(self, client: httpx.AsyncClient, temp_dir: str, semaphore: asyncio.Semaphore, - progress_bar: tqdm): + async def _retry_failed_segments(self, client: httpx.AsyncClient, temp_dir: str, semaphore: asyncio.Semaphore, progress_bar: tqdm): """ Retry failed segments up to 3 times. """ @@ -368,55 +390,92 @@ async def download_segments_async(self, description: str, type: str): temp_dir = os.path.join(self.tmp_folder, "segments_temp") os.makedirs(temp_dir, exist_ok=True) - # Initialize progress bar - total_segments = len(self.segments) + (1 if self.has_init_segment else 0) - progress_bar = tqdm( - total=total_segments, - bar_format=self._get_bar_format(description) - ) + if self.stream_type in ["ts", "aac"]: - # Reset stats - self.downloaded_segments = set() - self.info_nFailed = 0 - self.info_nRetry = 0 - self.info_maxRetry = 0 - self.download_interrupted = False + # Initialize progress bar + total_segments = len(self.segments) + (1 if self.has_init_segment else 0) + progress_bar = tqdm( + total=total_segments, + bar_format=self._get_bar_format(description) + ) - try: - # Configure HTTP client - timeout_config = httpx.Timeout(SEGMENT_MAX_TIMEOUT, connect=10.0) - limits = httpx.Limits(max_keepalive_connections=20, max_connections=100) - - async with httpx.AsyncClient(timeout=timeout_config, limits=limits, verify=REQUEST_VERIFY) as client: - - # Download init segment first (writes to 0.ts) - await self._download_init_segment(client, self.final_output_path, progress_bar) - - # Determine worker count based on type - max_workers = self._get_worker_count(type) - semaphore = asyncio.Semaphore(max_workers) - - # Update estimator - self.class_ts_estimator.total_segments = len(self.segments) + # Reset stats + self.downloaded_segments = set() + self.info_nFailed = 0 + self.info_nRetry = 0 + self.info_maxRetry = 0 + self.download_interrupted = False + + try: + # Configure HTTP client + timeout_config = httpx.Timeout(SEGMENT_MAX_TIMEOUT, connect=10.0) + limits = httpx.Limits(max_keepalive_connections=20, max_connections=100) - # Download all segments to temp files - await self._download_all_segments(client, temp_dir, semaphore, progress_bar) + async with httpx.AsyncClient(timeout=timeout_config, limits=limits, verify=REQUEST_VERIFY) as client: + + # Download init segment first (writes to 0.ts) + await self._download_init_segment(client, self.final_output_path, progress_bar) + + # Determine worker count based on type + max_workers = self._get_worker_count(type) + semaphore = asyncio.Semaphore(max_workers) + + # Update estimator + self.class_ts_estimator.total_segments = len(self.segments) + + # Download all segments to temp files + await self._download_all_segments(client, temp_dir, semaphore, progress_bar) + + # Concatenate all segments to 0.ts + if not self.download_interrupted: + await self._concatenate_segments(self.final_output_path, temp_dir) + + except KeyboardInterrupt: + self.download_interrupted = True + console.print("\n[red]Download interrupted by user (Ctrl+C).") - # Concatenate all segments to 0.ts - if not self.download_interrupted: - await self._concatenate_segments(self.final_output_path, temp_dir) + finally: + self._cleanup_resources(temp_dir, progress_bar) - except KeyboardInterrupt: - self.download_interrupted = True - console.print("\n[red]Download interrupted by user (Ctrl+C).") - - finally: - self._cleanup_resources(temp_dir, progress_bar) + if not self.download_interrupted: + self._verify_download_completion() - if not self.download_interrupted: - self._verify_download_completion() + return self._generate_results(type) + + else: + # DRM + if self.decryption is not None: + + # Get Widevine keys + content_keys = get_widevine_keys(self.decryption.pssh, self.license_url, get_wvd_path()) + + # Download encrypted MP4 file + encrypted_file, kill = MP4_Downloader( + url = self.segments[0], + path=os.path.join(self.tmp_folder, "encrypted.mp4"), + headers_=self.custom_headers, + show_final_info=False + ) + + # Decrypt MP4 file + KID = content_keys[0]['kid'] + KEY = content_keys[0]['key'] + decrypted_file = os.path.join(self.tmp_folder, f"{type}_decrypted.mp4") + mp4_output = decrypt_with_mp4decrypt("MP4", encrypted_file, KID, KEY, decrypted_file) + + return self._generate_results(type, mp4_output) + + # NOT DRM + else: + #print(self.segment_init_url, self.segments[0]) + decrypted_file, kill = MP4_Downloader( + url = self.segments[0], + path=os.path.join(self.tmp_folder, f"{type}_decrypted.mp4"), + headers_=self.custom_headers, + show_final_info=False + ) + return self._generate_results(type, decrypted_file) - return self._generate_results(type) def download_streams(self, description: str, type: str): """ @@ -451,12 +510,15 @@ def _get_worker_count(self, stream_type: str) -> int: 'audio': DEFAULT_AUDIO_WORKERS }.get(stream_type.lower(), 1) - def _generate_results(self, stream_type: str) -> Dict: + def _generate_results(self, stream_type: str, output_path: str = None) -> Dict: """Package final download results.""" return { 'type': stream_type, 'nFailed': self.info_nFailed, - 'stopped': self.download_interrupted + 'stopped': self.download_interrupted, + 'stream': self.stream_type, + 'drm': self.drm_method, + 'output_path': output_path if output_path else self.final_output_path } def _verify_download_completion(self) -> None: diff --git a/StreamingCommunity/Lib/M3U8/url_fixer.py b/StreamingCommunity/Lib/HLS/url_fixer.py similarity index 89% rename from StreamingCommunity/Lib/M3U8/url_fixer.py rename to StreamingCommunity/Lib/HLS/url_fixer.py index 2b48ace75..6bf21ebca 100644 --- a/StreamingCommunity/Lib/M3U8/url_fixer.py +++ b/StreamingCommunity/Lib/HLS/url_fixer.py @@ -46,10 +46,4 @@ def generate_full_url(self, url_resource: str) -> str: # Join the base URL with the relative resource URL to get the full URL full_url = urljoin(base_url, url_resource) - return full_url - - def reset_playlist(self) -> None: - """ - Reset the M3U8 playlist URL to its default state (None). - """ - self.url_playlist = None \ No newline at end of file + return full_url \ No newline at end of file diff --git a/StreamingCommunity/Lib/M3U8/__init__.py b/StreamingCommunity/Lib/M3U8/__init__.py deleted file mode 100644 index 31867e17c..000000000 --- a/StreamingCommunity/Lib/M3U8/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# 02.04.24 - -from .decryptor import M3U8_Decryption -from .estimator import M3U8_Ts_Estimator -from .parser import M3U8_Parser, M3U8_Codec -from .url_fixer import M3U8_UrlFix - -__all__ = [ - "M3U8_Decryption", - "M3U8_Ts_Estimator", - "M3U8_Parser", - "M3U8_Codec", - "M3U8_UrlFix" -] \ No newline at end of file diff --git a/StreamingCommunity/Lib/MEGA/__init__.py b/StreamingCommunity/Lib/MEGA/__init__.py new file mode 100644 index 000000000..b80174bda --- /dev/null +++ b/StreamingCommunity/Lib/MEGA/__init__.py @@ -0,0 +1,5 @@ +# 17.12.25 + +from .downloader import MEGA_Downloader + +__all__ = ['MEGA_Downloader'] \ No newline at end of file diff --git a/StreamingCommunity/Lib/MEGA/downloader.py b/StreamingCommunity/Lib/MEGA/downloader.py new file mode 100644 index 000000000..b5bc30f42 --- /dev/null +++ b/StreamingCommunity/Lib/MEGA/downloader.py @@ -0,0 +1,221 @@ +# 16.12.25 + +import re +import subprocess +import shutil +from pathlib import Path + + +# External libraries +from rich.console import Console + + +# Internal utilities +from StreamingCommunity.Util.os import get_megatools_path +from StreamingCommunity.Util.os import os_manager + + +# Variable +console = Console() + + +class MEGA_Downloader: + + # Episode patterns for series organization + EP_PATTERNS = [ + re.compile(r'[Ss](\d{1,2})[Ee](\d{1,2})'), + re.compile(r'[_\s-]+(\d{1,2})x(\d{1,2})', re.IGNORECASE) + ] + + LANG_PRIORITY = [ + re.compile(r'\bita\b', re.IGNORECASE), + re.compile(r'italian', re.IGNORECASE) + ] + + VIDEO_EXTENSIONS = {'.mkv', '.mp4', '.avi', '.m4v', '.mov', '.wmv'} + + def __init__(self, choose_files=False): + self.megatools_exe = get_megatools_path() + self.choose_files = choose_files + + if not self.megatools_exe: + raise RuntimeError("Megatools executable not found. Please ensure it is installed.") + + def download_url(self, url, dest_path=None): + """Download a file or folder by its public url""" + if "/folder/" in url: + return self._download_folder_megatools(str(url).strip(), dest_path) + else: + return self._download_movie_megatools(str(url).strip(), dest_path) + + def _download_movie_megatools(self, url, dest_path=None): + """Download a single movie file using megatools""" + output_dir = Path(dest_path).parent if dest_path else Path("./Movies") + output_dir.mkdir(parents=True, exist_ok=True) + + cmd = [str(self.megatools_exe), "dl", url, "--path", str(output_dir)] + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 + ) + + for line in process.stdout: + print(line, end='') + + process.wait() + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, process.args) + + console.print("[green]Download completato!") + return str(output_dir) + + def _download_folder_megatools(self, url, dest_path=None): + """Download and organize a series folder using megatools""" + # Sanitize dest_path if provided + if dest_path: + dest_path = os_manager.get_sanitize_path(dest_path) + base_dir = Path(dest_path).parent + else: + base_dir = Path("./") + + tv_dir = base_dir / "TV" + + # Use a shorter temp directory name to avoid path length issues + import uuid + tmp_dir_name = f"_tmp_{uuid.uuid4().hex[:8]}" # Shorter temp name + tmp_dir = base_dir / tmp_dir_name + + console.print("[cyan]Download Series ...") + if tmp_dir.exists(): + shutil.rmtree(tmp_dir) + tmp_dir.mkdir(parents=True, exist_ok=True) + + cmd = [str(self.megatools_exe), "dl", url, "--path", str(tmp_dir)] + if self.choose_files: + cmd.append("--choose-files") + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 + ) + + for line in process.stdout: + print(line, end='') + + if line.startswith('F ') and not self.choose_files: + filename = line.split('\\')[-1].strip() + parsed = self._parse_episode(filename) + + if parsed: + show, season, episode, ep_title = parsed + ep_display = f"S{season:02d}E{episode:02d}" + if ep_title: + ep_display += f" - {ep_title}" + + console.print(f"\n[cyan]Download: [yellow]{show} [magenta]{ep_display}[/magenta]\n") + + process.wait() + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, process.args) + + series_root = self._select_language_folder(tmp_dir) + result_path = self._organize_series(series_root, tv_dir) + + shutil.rmtree(tmp_dir) + return result_path + + def _select_language_folder(self, base): + """Select folder based on language priority""" + folders = [f for f in base.iterdir() if f.is_dir()] + if not folders: + return base + + for rx in self.LANG_PRIORITY: + for f in folders: + if rx.search(f.name): + console.print(f"[green]Select language: {f.name}") + return f + + return folders[0] + + def _organize_series(self, base, tv_dir): + """Organize series files into proper structure""" + tv_dir.mkdir(parents=True, exist_ok=True) + show_name = None + last_path = None + + for file in base.rglob("*"): + if not file.is_file(): + continue + + if file.suffix.lower() not in self.VIDEO_EXTENSIONS: + continue + + parsed = self._parse_episode(file.name) + if not parsed: + continue + + show, season, episode, ep_title = parsed + + if show_name is None: + show_name = show + + # Sanitize show name + clean_show = os_manager.get_sanitize_file(show) + season_dir = tv_dir / clean_show / f"Season {season:02d}" + season_dir.mkdir(parents=True, exist_ok=True) + + name = f"S{season:02d}E{episode:02d}" + if ep_title: + # Limit episode title length + max_title_len = 50 # Adjust as needed + if len(ep_title) > max_title_len: + ep_title = ep_title[:max_title_len] + name += f" - {ep_title}" + + # Sanitize the final filename + final_name = os_manager.get_sanitize_file(f"{name}{file.suffix}") + last_path = season_dir / final_name + + shutil.move(file, last_path) + + return str(last_path.parent) if last_path else str(tv_dir) + + def _parse_episode(self, filename): + """Parse episode information from filename""" + for rx in self.EP_PATTERNS: + m = rx.search(filename) + if m: + season = int(m.group(1)) + episode = int(m.group(2)) + + before = filename[:m.start()] + after = filename[m.end():] + + show = self._clean_show_name(before) + ep_title = self._clean_episode_title(after) + + return show, season, episode, ep_title + return None + + def _clean_show_name(self, text): + """Clean show name from extra characters""" + text = re.sub(r'[-._\s]+\d*$', '', text) + text = re.sub(r'[-._]+$', '', text) + text = text.replace('_', ' ') + text = re.sub(r'\s{2,}', ' ', text) + return text.strip().title() + + def _clean_episode_title(self, text): + """Clean episode title from quality tags""" + text = re.sub(r'\.(mkv|mp4|avi).*$', '', text, flags=re.I) + text = re.sub(r'[-_]', ' ', text) + text = re.sub(r'\b(web[- ]?dl|bluray|720p|1080p|ita|eng|subs?)\b', '', text, flags=re.I) + text = re.sub(r'\s{2,}', ' ', text).strip() + return text if len(text) > 3 else None \ No newline at end of file diff --git a/StreamingCommunity/Lib/MP4/__init__.py b/StreamingCommunity/Lib/MP4/__init__.py new file mode 100644 index 000000000..08f580132 --- /dev/null +++ b/StreamingCommunity/Lib/MP4/__init__.py @@ -0,0 +1,5 @@ +# 17.12.25 + +from .downloader import MP4_Downloader + +__all__ = ['MP4_Downloader'] \ No newline at end of file diff --git a/StreamingCommunity/Lib/Downloader/MP4/downloader.py b/StreamingCommunity/Lib/MP4/downloader.py similarity index 70% rename from StreamingCommunity/Lib/Downloader/MP4/downloader.py rename to StreamingCommunity/Lib/MP4/downloader.py index d045b20cb..d44c51257 100644 --- a/StreamingCommunity/Lib/Downloader/MP4/downloader.py +++ b/StreamingCommunity/Lib/MP4/downloader.py @@ -1,7 +1,6 @@ # 09.06.24 import os -import re import sys import time import signal @@ -17,22 +16,17 @@ # Internal utilities -from StreamingCommunity.Util.headers import get_userAgent -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.color import Colors -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.os import internet_manager, os_manager -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance +from StreamingCommunity.Util.http_client import create_client, get_userAgent +from StreamingCommunity.Util import config_manager, os_manager, internet_manager, Colors # Logic class -from ...FFmpeg import print_duration_table +from ..FFmpeg.util import print_duration_table # Config REQUEST_VERIFY = config_manager.get_bool('REQUESTS', 'verify') REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') -TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') # Variable @@ -62,35 +56,29 @@ def signal_handler(signum, frame, interrupt_handler, original_handler): if interrupt_handler.interrupt_count == 1: interrupt_handler.kill_download = True - console.print("\n[bold yellow]First interrupt received. Download will complete and save. Press Ctrl+C three times quickly to force quit.[/bold yellow]") + console.print("\n[yellow]First interrupt received. Download will complete and save. Press Ctrl+C three times quickly to force quit.") elif interrupt_handler.interrupt_count >= 3: interrupt_handler.force_quit = True - console.print("\n[bold red]Force quit activated. Saving partial download...[/bold red]") + console.print("\n[red]Force quit activated. Saving partial download...") signal.signal(signum, original_handler) -def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = None): +def MP4_Downloader(url: str, path: str, referer: str = None, headers_: dict = None, show_final_info: bool = True): """ Downloads an MP4 video with enhanced interrupt handling. - Single Ctrl+C: Completes download gracefully - Triple Ctrl+C: Saves partial download and exits """ - url = url.strip() - if TELEGRAM_BOT: - bot = get_bot_instance() - console.log("####") - + url = str(url).strip() path = os_manager.get_sanitize_path(path) if os.path.exists(path): console.log("[red]Output file already exists.") - if TELEGRAM_BOT: - bot.send_message("Contenuto giΓ  scaricato!", None) return None, False if not (url.lower().startswith('http://') or url.lower().startswith('https://')): logging.error(f"Invalid URL: {url}") - console.print(f"[bold red]Invalid URL: {url}[/bold red]") + console.print(f"[red]Invalid URL: {url}") return None, False # Set headers @@ -107,6 +95,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No temp_path = f"{path}.temp" interrupt_handler = InterruptHandler() original_handler = None + try: if threading.current_thread() is threading.main_thread(): original_handler = signal.signal( @@ -117,6 +106,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No original_handler=signal.getsignal(signal.SIGINT), ), ) + except Exception: original_handler = None @@ -130,12 +120,11 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No total = int(response.headers.get('content-length', 0)) if total == 0: - console.print("[bold red]No video stream found.[/bold red]") + console.print("[red]No video stream found.") return None, False # Create progress bar with percentage instead of n_fmt/total_fmt - console.print("[cyan]You can safely stop the download with [bold]Ctrl+c[bold] [cyan]") - + print("") progress_bar = tqdm( total=total, ascii='β–‘β–’β–ˆ', @@ -146,8 +135,7 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No f"{Colors.WHITE}{{postfix}} ", unit='B', unit_scale=True, - unit_divisor=1024, - mininterval=0.05, + mininterval=0.1, file=sys.stdout ) @@ -155,9 +143,9 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No downloaded = 0 with open(temp_path, 'wb') as file, progress_bar as bar: try: - for chunk in response.iter_bytes(chunk_size=1024): + for chunk in response.iter_bytes(chunk_size=65536): if interrupt_handler.force_quit: - console.print("\n[bold red]Force quitting... Saving partial download.[/bold red]") + console.print("\n[red]Force quitting... Saving partial download.") break if chunk: @@ -181,26 +169,22 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = No os.rename(temp_path, path) if os.path.exists(path): - file_size = internet_manager.format_file_size(os.path.getsize(path)) - duration = print_duration_table(path, description=False, return_string=True) - console.print(f"[yellow]Output[white]: [red]{os.path.abspath(path)} \n" - f" [cyan]with size[white]: [red]{file_size} \n" - f" [cyan]and duration[white]: [red]{duration}") - - if TELEGRAM_BOT: - message = f"Download completato{'(Parziale)' if interrupt_handler.force_quit else ''}\nDimensione: {internet_manager.format_file_size(os.path.getsize(path))}\nDurata: {print_duration_table(path, description=False, return_string=True)}\nTitolo: {os.path.basename(path.replace(f'.{extension_output}', ''))}" - clean_message = re.sub(r'\[[a-zA-Z]+\]', '', message) - bot.send_message(clean_message, None) + if show_final_info: + file_size = internet_manager.format_file_size(os.path.getsize(path)) + duration = print_duration_table(path, description=False, return_string=True) + console.print(f"[yellow]Output[white]: [red]{os.path.abspath(path)} \n" + f" [cyan]with size[white]: [red]{file_size} \n" + f" [cyan]and duration[white]: [red]{duration}") return path, interrupt_handler.kill_download else: - console.print("[bold red]Download failed or file is empty.[/bold red]") + console.print("[red]Download failed or file is empty.") return None, interrupt_handler.kill_download except Exception as e: logging.error(f"Unexpected error: {e}") - console.print(f"[bold red]Unexpected Error: {e}[/bold red]") + console.print(f"[red]Unexpected Error: {e}") if os.path.exists(temp_path): os.remove(temp_path) return None, interrupt_handler.kill_download diff --git a/StreamingCommunity/Lib/TMBD/__init__.py b/StreamingCommunity/Lib/TMBD/__init__.py deleted file mode 100644 index f778fff15..000000000 --- a/StreamingCommunity/Lib/TMBD/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# 17.09.24 - -from .tmdb import tmdb -from .obj_tmbd import Json_film - -__all__ = [ - "tmdb", - "Json_film" -] \ No newline at end of file diff --git a/StreamingCommunity/Lib/TMBD/obj_tmbd.py b/StreamingCommunity/Lib/TMBD/obj_tmbd.py deleted file mode 100644 index 14a9d0f30..000000000 --- a/StreamingCommunity/Lib/TMBD/obj_tmbd.py +++ /dev/null @@ -1,25 +0,0 @@ -# 17.09.24 - -from typing import Dict - - -class Json_film: - def __init__(self, data: Dict): - self.id = data.get('id', 0) - self.imdb_id = data.get('imdb_id') - self.origin_country = data.get('origin_country', []) - self.original_language = data.get('original_language') - self.original_title = data.get('original_title') - self.popularity = data.get('popularity', 0.0) - self.poster_path = data.get('poster_path') - self.release_date = data.get('release_date') - self.status = data.get('status') - self.title = data.get('title') - self.vote_average = data.get('vote_average', 0.0) - self.vote_count = data.get('vote_count', 0) - - def __repr__(self): - return (f"Json_film(id={self.id}, imdb_id='{self.imdb_id}', origin_country={self.origin_country}, " - f"original_language='{self.original_language}', original_title='{self.original_title}', " - f"popularity={self.popularity}, poster_path='{self.poster_path}', release_date='{self.release_date}', " - f"status='{self.status}', title='{self.title}', vote_average={self.vote_average}, vote_count={self.vote_count})") \ No newline at end of file diff --git a/StreamingCommunity/Lib/TMBD/tmdb.py b/StreamingCommunity/Lib/TMBD/tmdb.py deleted file mode 100644 index e294a9c8f..000000000 --- a/StreamingCommunity/Lib/TMBD/tmdb.py +++ /dev/null @@ -1,297 +0,0 @@ -# 24.08.24 - -import os -import sys - - -# External libraries -from rich.console import Console -from dotenv import load_dotenv - - -# Internal utilities -from .obj_tmbd import Json_film -from StreamingCommunity.Util.http_client import create_client -from StreamingCommunity.Util.table import TVShowManager - - -# Variable -load_dotenv() -console = Console() -table_show_manager = TVShowManager() -api_key = os.environ.get("TMDB_API_KEY") - - - -def get_select_title(table_show_manager, generic_obj): - """ - Display a selection of titles and prompt the user to choose one. - - Returns: - dict: The selected media item. - """ - if not generic_obj: - console.print("\n[red]No media items available.") - return None - - # Example of available colors for columns - available_colors = ['red', 'magenta', 'yellow', 'cyan', 'green', 'blue', 'white'] - - # Retrieve the keys of the first item as column headers - first_item = generic_obj[0] - column_info = {"Index": {'color': available_colors[0]}} # Always include Index with a fixed color - - # Assign colors to the remaining keys dynamically - color_index = 1 - for key in first_item.keys(): - if key in ('name', 'date', 'number'): # Custom prioritization of colors - if key == 'name': - column_info["Name"] = {'color': 'magenta'} - elif key == 'date': - column_info["Date"] = {'color': 'cyan'} - elif key == 'number': - column_info["Number"] = {'color': 'yellow'} - - else: - column_info[key.capitalize()] = {'color': available_colors[color_index % len(available_colors)]} - color_index += 1 - - table_show_manager.add_column(column_info) - - # Populate the table with title information - for i, item in enumerate(generic_obj): - item_dict = {'Index': str(i)} - - for key in item.keys(): - # Ensure all values are strings for rich add table - item_dict[key.capitalize()] = str(item[key]) - - table_show_manager.add_tv_show(item_dict) - - # Run the table and handle user input - last_command = table_show_manager.run(force_int_input=True, max_int_input=len(generic_obj)) - table_show_manager.clear() - - # Handle user's quit command - if last_command == "q" or last_command == "quit": - console.print("\n[red]Quit ...") - sys.exit(0) - - # Check if the selected index is within range - if 0 <= int(last_command) < len(generic_obj): - return generic_obj[int(last_command)] - - else: - console.print("\n[red]Wrong index") - sys.exit(0) - - -class TheMovieDB: - def __init__(self, api_key): - """ - Initialize the class with the API key. - - Parameters: - - api_key (str): The API key for authenticating requests to TheMovieDB. - """ - self.api_key = api_key - self.base_url = "https://api.themoviedb.org/3" - self._cached_trending_tv = None - self._cached_trending_movies = None - - def _make_request(self, endpoint, params=None): - """ - Make a request to the given API endpoint with optional parameters. - - Parameters: - - endpoint (str): The API endpoint to hit. - - params (dict): Additional parameters for the request. - - Returns: - dict: JSON response as a dictionary. - """ - if params is None: - params = {} - - params['api_key'] = self.api_key - url = f"{self.base_url}/{endpoint}" - response = create_client().get(url, params=params) - response.raise_for_status() - - return response.json() - - def _display_top_5(self, category: str, data, name_key='title'): - """ - Display top 5 most popular items in a single line with colors. - - Parameters: - - category (str): Category label (e.g., "Trending films", "Trending TV shows") - - data (list): List of media items - - name_key (str): Key to use for the name ('title' for movies, 'name' for TV shows) - """ - # Colors for the titles - colors = ['cyan', 'magenta', 'yellow', 'green', 'blue'] - - # Sort by popularity and get top 5 - sorted_data = sorted(data, key=lambda x: x.get('popularity', 0), reverse=True)[:5] - - # Create list of colored titles - colored_items = [] - for item, color in zip(sorted_data, colors): - title = item.get(name_key, 'Unknown') - colored_items.append(f"[{color}]{title}[/]") - - # Join with colored arrows and print with proper category label - console.print( - f"[bold purple]{category}:[/] {' [red]->[/] '.join(colored_items)}" - ) - - def display_trending_tv_shows(self): - """ - Fetch and display the top 5 trending TV shows of the week. - Uses cached data if available, otherwise makes a new request. - """ - if self._cached_trending_tv is None: - self._cached_trending_tv = self._make_request("trending/tv/week").get("results", []) - - self._display_top_5("Trending TV shows", self._cached_trending_tv, name_key='name') - - def refresh_trending_tv_shows(self): - """ - Force a refresh of the trending TV shows cache. - """ - self._cached_trending_tv = self._make_request("trending/tv/week").get("results", []) - return self._cached_trending_tv - - def display_trending_films(self): - """ - Fetch and display the top 5 trending films of the week. - Uses cached data if available, otherwise makes a new request. - """ - if self._cached_trending_movies is None: - self._cached_trending_movies = self._make_request("trending/movie/week").get("results", []) - - self._display_top_5("Trending films", self._cached_trending_movies, name_key='title') - - def refresh_trending_films(self): - """ - Force a refresh of the trending films cache. - """ - self._cached_trending_movies = self._make_request("trending/movie/week").get("results", []) - return self._cached_trending_movies - - def search_movie(self, movie_name: str): - """ - Search for a movie by name and return its TMDB ID. - - Parameters: - - movie_name (str): The name of the movie to search for. - - Returns: - int: The TMDB ID of the selected movie. - """ - generic_obj = [] - data = self._make_request("search/movie", {"query": movie_name}).get("results", []) - if not data: - console.print("No movies found with that name.", style="red") - return None - - console.print("\nSelect a Movie:") - for i, movie in enumerate(data, start=1): - generic_obj.append({ - 'name': movie['title'], - 'date': movie.get('release_date', 'N/A'), - 'id': movie['id'] - }) - - choice = get_select_title(table_show_manager, generic_obj) - return choice["id"] - - def get_movie_details(self, tmdb_id: int) -> Json_film: - """ - Fetch and display details for a specific movie using its TMDB ID. - - Parameters: - - tmdb_id (int): The TMDB ID of the movie. - - Returns: - - Json_film: The movie details as a class. - """ - movie = self._make_request(f"movie/{tmdb_id}") - if not movie: - console.print("Movie not found.", style="red") - return None - - return Json_film(movie) - - def search_tv_show(self, tv_name: str): - """ - Search for a TV show by name and return its TMDB ID. - - Parameters: - - tv_name (str): The name of the TV show to search for. - - Returns: - int: The TMDB ID of the selected TV show. - """ - data = self._make_request("search/tv", {"query": tv_name}).get("results", []) - if not data: - console.print("No TV shows found with that name.", style="red") - return None - - console.print("\nSelect a TV Show:") - for i, show in enumerate(data, start=1): - console.print(f"{i}. {show['name']} (First Air Date: {show.get('first_air_date', 'N/A')})") - - choice = int(input("Enter the number of the show you want: ")) - 1 - selected_show = data[choice] - return selected_show["id"] # Return the TMDB ID of the selected TV show - - def get_seasons(self, tv_show_id: int): - """ - Get seasons for a given TV show. - - Parameters: - - tv_show_id (int): The TMDB ID of the TV show. - - Returns: - int: The season number selected by the user. - """ - data = self._make_request(f"tv/{tv_show_id}").get("seasons", []) - if not data: - console.print("No seasons found for this TV show.", style="red") - return None - - console.print("\nSelect a Season:") - for i, season in enumerate(data, start=1): - console.print(f"{i}. {season['name']} (Episodes: {season['episode_count']})") - - choice = int(input("Enter the number of the season you want: ")) - 1 - return data[choice]["season_number"] - - def get_episodes(self, tv_show_id: int, season_number: int): - """ - Get episodes for a given season of a TV show. - - Parameters: - - tv_show_id (int): The TMDB ID of the TV show. - - season_number (int): The season number. - - Returns: - dict: The details of the selected episode. - """ - data = self._make_request(f"tv/{tv_show_id}/season/{season_number}").get("episodes", []) - if not data: - console.print("No episodes found for this season.", style="red") - return None - - console.print("\nSelect an Episode:") - for i, episode in enumerate(data, start=1): - console.print(f"{i}. {episode['name']} (Air Date: {episode.get('air_date', 'N/A')})") - - choice = int(input("Enter the number of the episode you want: ")) - 1 - return data[choice] - - -# Output -tmdb = TheMovieDB(api_key) \ No newline at end of file diff --git a/StreamingCommunity/TelegramHelp/__init__.py b/StreamingCommunity/TelegramHelp/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/StreamingCommunity/TelegramHelp/telegram_bot.py b/StreamingCommunity/TelegramHelp/telegram_bot.py deleted file mode 100644 index 49aded52c..000000000 --- a/StreamingCommunity/TelegramHelp/telegram_bot.py +++ /dev/null @@ -1,718 +0,0 @@ -# 04.02.26 -# Made by: @GiuPic - -import os -import re -import sys -import time -import uuid -import json -import threading -import subprocess -from typing import Optional - -# External libraries -import telebot - -session_data = {} - -class TelegramSession: - - def set_session(value): - session_data['script_id'] = value - - def get_session(): - return session_data.get('script_id', 'unknown') - - def updateScriptId(screen_id, titolo): - json_file = "../../scripts.json" - try: - with open(json_file, 'r') as f: - scripts_data = json.load(f) - except FileNotFoundError: - scripts_data = [] - - # cerco lo script con lo screen_id - for script in scripts_data: - if script["screen_id"] == screen_id: - # se trovo il match, aggiorno il titolo - script["titolo"] = titolo - - # aggiorno il file json - with open(json_file, 'w') as f: - json.dump(scripts_data, f, indent=4) - - return - - print(f"Screen_id {screen_id} non trovato.") - - def deleteScriptId(screen_id): - json_file = "../../scripts.json" - try: - with open(json_file, 'r') as f: - scripts_data = json.load(f) - except FileNotFoundError: - scripts_data = [] - - for script in scripts_data: - if script["screen_id"] == screen_id: - # se trovo il match, elimino lo script - scripts_data.remove(script) - - # aggiorno il file json - with open(json_file, 'w') as f: - json.dump(scripts_data, f, indent=4) - - print(f"Script eliminato per screen_id {screen_id}") - return - - print(f"Screen_id {screen_id} non trovato.") - -class TelegramRequestManager: - _instance = None - - def __new__(cls, *args, **kwargs): - if not cls._instance: - cls._instance = super().__new__(cls) - return cls._instance - - def __init__(self, json_file: str = "active_requests.json"): - if not hasattr(self, 'initialized'): - self.json_file = json_file - self.initialized = True - self.on_response_callback = None - - def create_request(self, type: str) -> str: - request_data = { - "type": type, - "response": None, - "timestamp": time.time() - } - - with open(self.json_file, "w") as f: - json.dump(request_data, f) - - return "Ok" - - def save_response(self, message_text: str) -> bool: - try: - # Carica il file JSON - with open(self.json_file, "r") as f: - data = json.load(f) - - # Controlla se esiste la chiave 'type' e se la risposta Γ¨ presente - if "type" in data and "response" in data: - data["response"] = message_text # Aggiorna la risposta - - with open(self.json_file, "w") as f: - json.dump(data, f, indent=4) - - return True - else: - return False - - except (FileNotFoundError, json.JSONDecodeError) as e: - print(f" save_response - errore: {e}") - return False - - def get_response(self) -> Optional[str]: - try: - with open(self.json_file, "r") as f: - data = json.load(f) - - # Verifica se esiste la chiave "response" - if "response" in data: - response = data["response"] # Ottieni la risposta direttamente - - if response is not None and self.on_response_callback: - self.on_response_callback(response) - - return response - - except (FileNotFoundError, json.JSONDecodeError) as e: - print(f"get_response - errore: {e}") - return None - - def clear_file(self) -> bool: - try: - with open(self.json_file, "w") as f: - json.dump({}, f) - print(f"File {self.json_file} Γ¨ stato svuotato con successo.") - return True - - except Exception as e: - print(f" clear_file - errore: {e}") - return False - -# Funzione per caricare variabili da un file .env -def load_env(file_path="../../.env"): - if os.path.exists(file_path): - with open(file_path) as f: - for line in f: - if line.strip() and not line.startswith("#"): - key, value = line.strip().split("=", 1) - os.environ[key] = value - -# Carica le variabili -load_env() - - -class TelegramBot: - _instance = None - _config_file = "../../bot_config.json" - - @classmethod - def get_instance(cls): - if cls._instance is None: - # Prova a caricare la configurazione e inizializzare il bot - if os.path.exists(cls._config_file): - with open(cls._config_file, "r") as f: - config = json.load(f) - - # Assicura che authorized_user_id venga trattato come una lista - authorized_users = config.get('authorized_user_id', []) - if isinstance(authorized_users, str): - authorized_users = [int(uid) for uid in authorized_users.split(",") if uid.strip().isdigit()] - - cls._instance = cls.init_bot(config['token'], authorized_users) - #cls._instance = cls.init_bot(config['token'], config['authorized_user_id']) - - else: - raise Exception("Bot non ancora inizializzato. Chiamare prima init_bot() con token e authorized_user_id") - return cls._instance - - @classmethod - def init_bot(cls, token, authorized_user_id): - if cls._instance is None: - cls._instance = cls(token, authorized_user_id) - # Salva la configurazione - config = {"token": token, "authorized_user_id": authorized_user_id} - with open(cls._config_file, "w") as f: - json.dump(config, f) - return cls._instance - - def __init__(self, token, authorized_users): - - def monitor_scripts(): - while True: - try: - with open("../../scripts.json", "r") as f: - scripts_data = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - scripts_data = [] - - current_time = time.time() - - # Crea una nuova lista senza gli script che sono scaduti o le screen che non esistono - scripts_data_to_save = [] - - for script in scripts_data: - screen_exists = False - try: - existing_screens = subprocess.check_output( - ["screen", "-list"] - ).decode("utf-8") - if script["screen_id"] in existing_screens: - screen_exists = True - except subprocess.CalledProcessError: - pass # Se il comando fallisce, significa che non ci sono screen attivi. - - if screen_exists: - if ( - "titolo" not in script - and script["status"] == "running" - and (current_time - script["start_time"]) > 600 - ): - # Prova a terminare la sessione screen - try: - subprocess.check_output( - ["screen", "-S", script["screen_id"], "-X", "quit"] - ) - print( - f" La sessione screen con ID {script['screen_id']} Γ¨ stata fermata automaticamente." - ) - except subprocess.CalledProcessError: - print( - f" Impossibile fermare la sessione screen con ID {script['screen_id']}." - ) - print( - f" Lo script con ID {script['screen_id']} ha superato i 10 minuti e verrΓ  rimosso." - ) - else: - scripts_data_to_save.append(script) - else: - print( - f" La sessione screen con ID {script['screen_id']} non esiste piΓΉ e verrΓ  rimossa." - ) - - # Salva la lista aggiornata, senza gli script scaduti o le screen non esistenti - with open("../../scripts.json", "w") as f: - json.dump(scripts_data_to_save, f, indent=4) - - time.sleep(60) # Controlla ogni minuto - - # Avvia il thread di monitoraggio - monitor_thread = threading.Thread(target=monitor_scripts, daemon=True) - monitor_thread.start() - - if TelegramBot._instance is not None: - raise Exception( - "Questa classe Γ¨ un singleton! Usa get_instance() per ottenere l'istanza." - ) - - self.token = token - self.authorized_users = authorized_users - self.chat_id = authorized_users - self.bot = telebot.TeleBot(token) - self.request_manager = TelegramRequestManager() - - # Registra gli handler - self.register_handlers() - - def register_handlers(self): - - """@self.bot.message_handler(commands=['start']) - def start(message): - self.handle_start(message)""" - - @self.bot.message_handler(commands=["get_id"]) - def get_id(message): - self.handle_get_id(message) - - @self.bot.message_handler(commands=["start"]) - def start_script(message): - self.handle_start_script(message) - - @self.bot.message_handler(commands=["list"]) - def list_scripts(message): - self.handle_list_scripts(message) - - @self.bot.message_handler(commands=["stop"]) - def stop_script(message): - self.handle_stop_script(message) - - @self.bot.message_handler(commands=["screen"]) - def screen_status(message): - self.handle_screen_status(message) - - @self.bot.message_handler(func=lambda message: True) - def handle_all_messages(message): - self.handle_response(message) - - def is_authorized(self, user_id): - return user_id in self.authorized_users - - def handle_get_id(self, message): - if not self.is_authorized(message.from_user.id): - print(" Non sei autorizzato.") - self.bot.send_message(message.chat.id, " Non sei autorizzato.") - return - - print(f"Il tuo ID utente Γ¨: `{message.from_user.id}`") - self.bot.send_message( - message.chat.id, - f"Il tuo ID utente Γ¨: `{message.from_user.id}`", - parse_mode="Markdown", - ) - - def handle_start_script(self, message): - if not self.is_authorized(message.from_user.id): - print(f" Non sei autorizzato. {message.from_user.id}") - self.bot.send_message(message.chat.id, " Non sei autorizzato.") - return - - screen_id = str(uuid.uuid4())[:8] - debug_mode = os.getenv("DEBUG") - - if debug_mode == "True": - subprocess.Popen(["python3", "../../test_run.py", screen_id]) - else: - # Verifica se lo screen con il nome esiste giΓ  - try: - subprocess.check_output(["screen", "-list"]) - existing_screens = subprocess.check_output(["screen", "-list"]).decode( - "utf-8" - ) - if screen_id in existing_screens: - print(f" Lo script con ID {screen_id} Γ¨ giΓ  in esecuzione.") - self.bot.send_message( - message.chat.id, - f" Lo script con ID {screen_id} Γ¨ giΓ  in esecuzione.", - ) - return - except subprocess.CalledProcessError: - pass # Se il comando fallisce, significa che non ci sono screen attivi. - - # Crea la sessione screen e avvia lo script al suo interno - command = [ - "screen", - "-dmS", - screen_id, - "python3", - "../../test_run.py", - screen_id, - ] - - # Avvia il comando tramite subprocess - subprocess.Popen(command) - - # Creazione oggetto script info - script_info = { - "screen_id": screen_id, - "start_time": time.time(), - "status": "running", - "user_id": message.from_user.id, - } - - # Salvataggio nel file JSON - json_file = "../../scripts.json" - - # Carica i dati esistenti o crea una nuova lista - try: - with open(json_file, "r") as f: - scripts_data = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - scripts_data = [] - - # Aggiungi il nuovo script - scripts_data.append(script_info) - - # Scrivi il file aggiornato - with open(json_file, "w") as f: - json.dump(scripts_data, f, indent=4) - - def handle_list_scripts(self, message): - if not self.is_authorized(message.from_user.id): - print(" Non sei autorizzato.") - self.bot.send_message(message.chat.id, " Non sei autorizzato.") - return - - try: - with open("../../scripts.json", "r") as f: - scripts_data = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - scripts_data = [] - - if not scripts_data: - print(" Nessuno script registrato.") - self.bot.send_message(message.chat.id, " Nessuno script registrato.") - return - - current_time = time.time() - msg = [" **Script Registrati:**\n"] - - for script in scripts_data: - # Calcola la durata - duration = current_time - script["start_time"] - if "end_time" in script: - duration = script["end_time"] - script["start_time"] - - # Formatta la durata - hours, rem = divmod(duration, 3600) - minutes, seconds = divmod(rem, 60) - duration_str = f"{int(hours)}h {int(minutes)}m {int(seconds)}s" - - # Icona stato - status_icons = {"running": "", "stopped": "", "completed": ""} - - # Costruisci riga - line = ( - f"β€’ ID: `{script['screen_id']}`\n" - f"β€’ Stato: {status_icons.get(script['status'], '')}\n" - f"β€’ Stop: `/stop {script['screen_id']}`\n" - f"β€’ Screen: `/screen {script['screen_id']}`\n" - f"β€’ Durata: {duration_str}\n" - f"β€’ Download:\n{script.get('titolo', 'N/A')}\n" - ) - msg.append(line) - - # Formatta la risposta finale - final_msg = "\n".join(msg) - if len(final_msg) > 4000: - final_msg = final_msg[:4000] + "\n[...] (messaggio troncato)" - - print(f"{final_msg}") - self.bot.send_message(message.chat.id, final_msg, parse_mode="Markdown") - - def handle_stop_script(self, message): - if not self.is_authorized(message.from_user.id): - print(" Non sei autorizzato.") - self.bot.send_message(message.chat.id, " Non sei autorizzato.") - return - - parts = message.text.split() - if len(parts) < 2: - try: - with open("../../scripts.json", "r") as f: - scripts_data = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - scripts_data = [] - - running_scripts = [s for s in scripts_data if s["status"] == "running"] - - if not running_scripts: - print(" Nessuno script attivo da fermare.") - self.bot.send_message( - message.chat.id, " Nessuno script attivo da fermare." - ) - return - - msg = " **Script Attivi:**\n" - for script in running_scripts: - msg += f" `/stop {script['screen_id']}` per fermarlo\n" - - print(f"{msg}") - self.bot.send_message(message.chat.id, msg, parse_mode="Markdown") - - elif len(parts) == 2: - screen_id = parts[1] - - try: - with open("../../scripts.json", "r") as f: - scripts_data = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - scripts_data = [] - - # Filtra la lista eliminando lo script con l'ID specificato - new_scripts_data = [ - script for script in scripts_data if script["screen_id"] != screen_id - ] - - if len(new_scripts_data) == len(scripts_data): - # Nessun elemento rimosso, quindi ID non trovato - print(f" Nessuno script attivo con ID `{screen_id}`.") - self.bot.send_message( - message.chat.id, - f" Nessuno script attivo con ID `{screen_id}`.", - parse_mode="Markdown", - ) - return - - # Terminare la sessione screen - try: - subprocess.check_output(["screen", "-S", screen_id, "-X", "quit"]) - print(f" La sessione screen con ID {screen_id} Γ¨ stata fermata.") - except subprocess.CalledProcessError: - print( - f" Impossibile fermare la sessione screen con ID `{screen_id}`." - ) - self.bot.send_message( - message.chat.id, - f" Impossibile fermare la sessione screen con ID `{screen_id}`.", - parse_mode="Markdown", - ) - return - - # Salva la lista aggiornata senza lo script eliminato - with open("../../scripts.json", "w") as f: - json.dump(new_scripts_data, f, indent=4) - - print(f" Script `{screen_id}` terminato con successo!") - self.bot.send_message( - message.chat.id, - f" Script `{screen_id}` terminato con successo!", - parse_mode="Markdown", - ) - - def handle_response(self, message): - text = message.text - if self.request_manager.save_response(text): - print(f" Risposta salvata correttamente per il tipo {text}") - else: - print(" Nessuna richiesta attiva.") - self.bot.reply_to(message, " Nessuna richiesta attiva.") - - def handle_screen_status(self, message): - command_parts = message.text.split() - if len(command_parts) < 2: - print(" ID mancante nel comando. Usa: /screen ") - self.bot.send_message( - message.chat.id, " ID mancante nel comando. Usa: /screen " - ) - return - - screen_id = command_parts[1] - temp_file = f"/tmp/screen_output_{screen_id}.txt" - - try: - # Verifica se lo screen con l'ID specificato esiste - existing_screens = subprocess.check_output(["screen", "-list"]).decode('utf-8') - if screen_id not in existing_screens: - print(f" La sessione screen con ID {screen_id} non esiste.") - self.bot.send_message(message.chat.id, f" La sessione screen con ID {screen_id} non esiste.") - return - - # Cattura l'output della screen - subprocess.run( - ["screen", "-X", "-S", screen_id, "hardcopy", "-h", temp_file], - check=True, - ) - except subprocess.CalledProcessError as e: - print(f" Errore durante la cattura dell'output della screen: {e}") - self.bot.send_message( - message.chat.id, - f" Errore durante la cattura dell'output della screen: {e}", - ) - return - - if not os.path.exists(temp_file): - print(" Impossibile catturare l'output della screen.") - self.bot.send_message( - message.chat.id, " Impossibile catturare l'output della screen." - ) - return - - try: - # Leggi il file con la codifica corretta - with open(temp_file, "r", encoding="latin-1") as file: - screen_output = file.read() - - # Pulisci l'output - cleaned_output = re.sub( - r"[\x00-\x1F\x7F]", "", screen_output - ) # Rimuovi caratteri di controllo - cleaned_output = cleaned_output.replace( - "\n\n", "\n" - ) # Rimuovi newline multipli - - # Inizializza le variabili - cleaned_output_0 = None # o "" - cleaned_output_1 = None # o "" - - # Dentro cleaned_output c'Γ¨ una stringa recupero quello che si trova tra ## ## - download_section = re.search(r"##(.*?)##", cleaned_output, re.DOTALL) - if download_section: - cleaned_output_0 = "Download: " + download_section.group(1).strip() - - # Recupero tutto quello che viene dopo con #### - download_section_bottom = re.search(r"####(.*)", cleaned_output, re.DOTALL) - if download_section_bottom: - cleaned_output_1 = download_section_bottom.group(1).strip() - - # Unico i due risultati se esistono - if cleaned_output_0 and cleaned_output_1: - cleaned_output = f"{cleaned_output_0}\n{cleaned_output_1}" - # Rimuovo 'segments.py:302' e 'downloader.py:385' se presente - cleaned_output = re.sub(r'downloader\.py:\d+', '', cleaned_output) - cleaned_output = re.sub(r'segments\.py:\d+', '', cleaned_output) - - # Invia l'output pulito - print(f" Output della screen {screen_id}:\n{cleaned_output}") - self._send_long_message( - message.chat.id, f" Output della screen {screen_id}:\n{cleaned_output}" - ) - - except Exception as e: - print( - f" Errore durante la lettura o l'invio dell'output della screen: {e}" - ) - self.bot.send_message( - message.chat.id, - f" Errore durante la lettura o l'invio dell'output della screen: {e}", - ) - - # Cancella il file temporaneo - os.remove(temp_file) - - def send_message(self, message, choices): - - formatted_message = message - if choices: - formatted_choices = "\n".join(choices) - formatted_message = f"{message}\n\n{formatted_choices}" - - for chat_id in self.authorized_users: - self.bot.send_message(chat_id, formatted_message) - - """ if choices is None: - if self.chat_id: - print(f"{message}") - self.bot.send_message(self.chat_id, message) - else: - formatted_choices = "\n".join(choices) - message = f"{message}\n\n{formatted_choices}" - if self.chat_id: - print(f"{message}") - self.bot.send_message(self.chat_id, message) """ - - def _send_long_message(self, chat_id, text, chunk_size=4096): - """Suddivide e invia un messaggio troppo lungo in piΓΉ parti.""" - for i in range(0, len(text), chunk_size): - print(f"{text[i:i+chunk_size]}") - self.bot.send_message(chat_id, text[i : i + chunk_size]) - - def ask(self, type, prompt_message, choices, timeout=60): - self.request_manager.create_request(type) - - if choices is None: - print(f"{prompt_message}") - """ self.bot.send_message( - self.chat_id, - f"{prompt_message}", - ) """ - for chat_id in self.authorized_users: # Manda a tutti gli ID autorizzati - self.bot.send_message(chat_id, f"{prompt_message}") - else: - print(f"{prompt_message}\n\nOpzioni: {', '.join(choices)}") - """ self.bot.send_message( - self.chat_id, - f"{prompt_message}\n\nOpzioni: {', '.join(choices)}", - ) """ - for chat_id in self.authorized_users: # Manda a tutti gli ID autorizzati - self.bot.send_message(chat_id, f"{prompt_message}\n\nOpzioni: {', '.join(choices)}") - - start_time = time.time() - while time.time() - start_time < timeout: - response = self.request_manager.get_response() - if response is not None: - return response - time.sleep(1) - - print(" Timeout: nessuna risposta ricevuta.") - for chat_id in self.authorized_users: # Manda a tutti gli ID autorizzati - self.bot.send_message(chat_id, " Timeout: nessuna risposta ricevuta.") - self.request_manager.clear_file() - return None - - def run(self): - print(" Avvio del bot...") - with open("../../scripts.json", "w") as f: - json.dump([], f) - self.bot.infinity_polling() - - -def get_bot_instance(): - return TelegramBot.get_instance() - -# Esempio di utilizzo -if __name__ == "__main__": - - # Usa le variabili - token = os.getenv("TOKEN_TELEGRAM") - authorized_users = os.getenv("AUTHORIZED_USER_ID") - - # Controlla se le variabili sono presenti - if not token: - print("Errore: TOKEN_TELEGRAM non Γ¨ definito nel file .env.") - sys.exit(1) - - if not authorized_users: - print("Errore: AUTHORIZED_USER_ID non Γ¨ definito nel file .env.") - sys.exit(1) - - try: - TOKEN = token # Inserisci il token del tuo bot Telegram sul file .env - AUTHORIZED_USER_ID = list(map(int, authorized_users.split(","))) # Inserisci il tuo ID utente Telegram sul file .env - except ValueError as e: - print(f"Errore nella conversione degli ID autorizzati: {e}. Controlla il file .env e assicurati che gli ID siano numeri interi separati da virgole.") - sys.exit(1) - - # Inizializza il bot - bot = TelegramBot.init_bot(TOKEN, AUTHORIZED_USER_ID) - bot.run() - -""" -start - Avvia lo script -list - Lista script attivi -get - Mostra ID utente Telegram -""" diff --git a/StreamingCommunity/Upload/update.py b/StreamingCommunity/Upload/update.py index 1682c9481..7b36df930 100644 --- a/StreamingCommunity/Upload/update.py +++ b/StreamingCommunity/Upload/update.py @@ -15,7 +15,7 @@ # Internal utilities from .version import __version__ as source_code_version, __author__, __title__ from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.headers import get_userAgent +from StreamingCommunity.Util.http_client import get_userAgent # Variable @@ -87,22 +87,19 @@ def update(): # Get commit details latest_commit = response_commits[0] if response_commits else None - if latest_commit: - latest_commit_message = latest_commit.get('commit', {}).get('message', 'No commit message') - else: - latest_commit_message = 'No commit history available' - - if str(current_version).replace('v', '') != str(last_version).replace('v', ''): - console.print(f"\n[cyan]New version available: [yellow]{last_version}") + latest_commit_message = latest_commit.get('commit', {}).get('message', 'No commit message') console.print( - f"\n[red]{__title__} has been downloaded [yellow]{total_download_count}" + f"\n[red]{__title__} has been downloaded: [yellow]{total_download_count}" f"\n[yellow]{get_execution_mode()} - [green]Current installed version: [yellow]{current_version} " f"[green]last commit: [white]'[yellow]{latest_commit_message.splitlines()[0]}[white]'\n" - f" [cyan]Help the repository grow today by leaving a [yellow]star [cyan]and [yellow]sharing " + f" [cyan]Help the repository grow today by leaving a [yellow]star [cyan]and [yellow]sharing " f"[cyan]it with others online!\n" - f" [magenta]If you'd like to support development and keep the program updated, consider leaving a " + f" [magenta]If you'd like to support development and keep the program updated, consider leaving a " f"[yellow]donation[magenta]. Thank you!" ) + + if str(current_version).replace('v', '') != str(last_version).replace('v', ''): + console.print(f"\n[cyan]New version available: [yellow]{last_version}") time.sleep(1) \ No newline at end of file diff --git a/StreamingCommunity/Util/__init__.py b/StreamingCommunity/Util/__init__.py new file mode 100644 index 000000000..413cddc9c --- /dev/null +++ b/StreamingCommunity/Util/__init__.py @@ -0,0 +1,17 @@ +# 18.12.25 + +from .color import Colors +from .config_json import config_manager +from .message import start_message +from .os import os_manager, os_summary, internet_manager +from .logger import Logger + +__all__ = [ + "config_manager", + "Colors", + "os_manager", + "os_summary", + "start_message", + "internet_manager", + "Logger" +] \ No newline at end of file diff --git a/StreamingCommunity/Util/config_json.py b/StreamingCommunity/Util/config_json.py index 2051e635f..83d1e5a20 100644 --- a/StreamingCommunity/Util/config_json.py +++ b/StreamingCommunity/Util/config_json.py @@ -12,10 +12,6 @@ from rich.console import Console -# Internal utilities -from StreamingCommunity.Util.headers import get_userAgent - - # Variable console = Console() @@ -42,7 +38,7 @@ def __init__(self, file_name: str = 'config.json') -> None: self.domains_path = os.path.join(base_path, 'domains.json') # Display the actual file path for debugging - console.print(f"[bold cyan]Configuration file path:[/bold cyan] [green]{self.file_path}[/green]") + console.print(f"[cyan]Config path: [green]{self.file_path}") # Reference repository URL self.reference_config_url = 'https://raw.githubusercontent.com/Arrowar/StreamingCommunity/refs/heads/main/config.json' @@ -52,25 +48,21 @@ def __init__(self, file_name: str = 'config.json') -> None: self.configSite = {} self.cache = {} - self.fetch_domain_online = True - - console.print(f"[bold cyan]Initializing ConfigManager:[/bold cyan] [green]{self.file_path}[/green]") - # Load the configuration + self.fetch_domain_online = True self.load_config() def load_config(self) -> None: """Load the configuration and initialize all settings.""" if not os.path.exists(self.file_path): - console.print(f"[bold red]WARNING: Configuration file not found:[/bold red] {self.file_path}") - console.print("[bold yellow]Downloading from repository...[/bold yellow]") + console.print(f"[red]WARNING: Configuration file not found: {self.file_path}") + console.print("[yellow]Downloading from repository...") self._download_reference_config() # Load the configuration file try: with open(self.file_path, 'r') as f: self.config = json.load(f) - console.print(f"[bold green]Configuration loaded:[/bold green] {len(self.config)} keys") # Update settings from the configuration self._update_settings_from_config() @@ -79,16 +71,16 @@ def load_config(self) -> None: self._load_site_data() except json.JSONDecodeError as e: - console.print(f"[bold red]Error parsing JSON:[/bold red] {str(e)}") + console.print(f"[red]Error parsing JSON: {str(e)}") self._handle_config_error() except Exception as e: - console.print(f"[bold red]Error loading configuration:[/bold red] {str(e)}") + console.print(f"[red]Error loading configuration: {str(e)}") self._handle_config_error() def _handle_config_error(self) -> None: """Handle configuration errors by downloading the reference version.""" - console.print("[bold yellow]Attempting to retrieve reference configuration...[/bold yellow]") + console.print("[yellow]Attempting to retrieve reference configuration...") self._download_reference_config() # Reload the configuration @@ -96,10 +88,10 @@ def _handle_config_error(self) -> None: with open(self.file_path, 'r') as f: self.config = json.load(f) self._update_settings_from_config() - console.print("[bold green]Reference configuration loaded successfully[/bold green]") + console.print("[green]Reference configuration loaded successfully") except Exception as e: - console.print(f"[bold red]Critical configuration error:[/bold red] {str(e)}") - console.print("[bold red]Unable to proceed. The application will terminate.[/bold red]") + console.print(f"[red]Critical configuration error: {str(e)}") + console.print("[red]Unable to proceed. The application will terminate.") sys.exit(1) def _update_settings_from_config(self) -> None: @@ -109,27 +101,25 @@ def _update_settings_from_config(self) -> None: # Get fetch_domain_online setting (True by default) self.fetch_domain_online = default_section.get('fetch_domain_online', True) - console.print(f"[bold cyan]Fetch domains online:[/bold cyan] [{'green' if self.fetch_domain_online else 'yellow'}]{self.fetch_domain_online}[/{'green' if self.fetch_domain_online else 'yellow'}]") - def _download_reference_config(self) -> None: """Download the reference configuration from GitHub.""" - console.print(f"[bold cyan]Downloading configuration:[/bold cyan] [green]{self.reference_config_url}[/green]") + console.print(f"[cyan]Downloading configuration: [green]{self.reference_config_url}") try: - response = requests.get(self.reference_config_url, timeout=8, headers={'User-Agent': get_userAgent()}) + response = requests.get(self.reference_config_url, timeout=8, headers={'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}) if response.status_code == 200: with open(self.file_path, 'wb') as f: f.write(response.content) file_size = len(response.content) / 1024 - console.print(f"[bold green]Download complete:[/bold green] {os.path.basename(self.file_path)} ({file_size:.2f} KB)") + console.print(f"[green]Download complete: {os.path.basename(self.file_path)} ({file_size:.2f} KB)") else: error_msg = f"HTTP Error: {response.status_code}, Response: {response.text[:100]}" - console.print(f"[bold red]Download failed:[/bold red] {error_msg}") + console.print(f"[red]Download failed: {error_msg}") raise Exception(error_msg) except Exception as e: - console.print(f"[bold red]Download error:[/bold red] {str(e)}") + console.print(f"[red]Download error: {str(e)}") raise def _load_site_data(self) -> None: @@ -143,7 +133,7 @@ def _load_site_data_online(self) -> None: """Load site data from GitHub and update local domains.json file.""" domains_github_url = "https://raw.githubusercontent.com/Arrowar/StreamingCommunity/refs/heads/main/.github/.domain/domains.json" headers = { - "User-Agent": get_userAgent() + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" } try: @@ -156,15 +146,15 @@ def _load_site_data_online(self) -> None: self._save_domains_to_appropriate_location() else: - console.print(f"[bold red]GitHub request failed:[/bold red] HTTP {response.status_code}, {response.text[:100]}") + console.print(f"[red]GitHub request failed: HTTP {response.status_code}, {response.text[:100]}") self._handle_site_data_fallback() except json.JSONDecodeError as e: - console.print(f"[bold red]Error parsing JSON from GitHub:[/bold red] {str(e)}") + console.print(f"[red]Error parsing JSON from GitHub: {str(e)}") self._handle_site_data_fallback() except Exception as e: - console.print(f"[bold red]GitHub connection error:[/bold red] {str(e)}") + console.print(f"[red]GitHub connection error: {str(e)}") self._handle_site_data_fallback() def _save_domains_to_appropriate_location(self) -> None: @@ -178,6 +168,7 @@ def _save_domains_to_appropriate_location(self) -> None: # Check for GitHub structure first github_domains_path = os.path.join(base_path, '.github', '.domain', 'domains.json') + console.print(f"[cyan]Domain path: [green]{github_domains_path}") try: if os.path.exists(github_domains_path): @@ -190,24 +181,24 @@ def _save_domains_to_appropriate_location(self) -> None: # Save to root only if it doesn't exist and GitHub structure doesn't exist with open(self.domains_path, 'w', encoding='utf-8') as f: json.dump(self.configSite, f, indent=4, ensure_ascii=False) - console.print(f"[bold green]Domains saved to:[/bold green] {self.domains_path}") + console.print(f"[green]Domains saved to: {self.domains_path}") else: # Root file exists, don't overwrite it - console.print(f"[bold yellow]Local domains.json already exists, not overwriting:[/bold yellow] {self.domains_path}") - console.print("[bold yellow]Tip: Delete the file if you want to recreate it from GitHub[/bold yellow]") + console.print(f"[yellow]Local domains.json already exists, not overwriting: {self.domains_path}") + console.print("[yellow]Tip: Delete the file if you want to recreate it from GitHub") except Exception as save_error: - console.print(f"[bold yellow]Warning: Could not save domains to file:[/bold yellow] {str(save_error)}") + console.print(f"[yellow]Warning: Could not save domains to file: {str(save_error)}") # Try to save to root as fallback only if it doesn't exist if not os.path.exists(self.domains_path): try: with open(self.domains_path, 'w', encoding='utf-8') as f: json.dump(self.configSite, f, indent=4, ensure_ascii=False) - console.print(f"[bold green]Domains saved to fallback location:[/bold green] {self.domains_path}") + console.print(f"[green]Domains saved to fallback location: {self.domains_path}") except Exception as fallback_error: - console.print(f"[bold red]Failed to save to fallback location:[/bold red] {str(fallback_error)}") + console.print(f"[red]Failed to save to fallback location: {str(fallback_error)}") def _load_site_data_from_file(self) -> None: """Load site data from local domains.json file.""" @@ -226,29 +217,28 @@ def _load_site_data_from_file(self) -> None: github_domains_path = os.path.join(base_path, '.github', '.domain', 'domains.json') if os.path.exists(github_domains_path): - console.print(f"[bold cyan]Reading domains from GitHub structure:[/bold cyan] {github_domains_path}") + console.print(f"[cyan]Domain path: [green]{github_domains_path}") with open(github_domains_path, 'r', encoding='utf-8') as f: self.configSite = json.load(f) site_count = len(self.configSite) if isinstance(self.configSite, dict) else 0 - console.print(f"[bold green]Domains loaded from GitHub structure:[/bold green] {site_count} streaming services") elif os.path.exists(self.domains_path): - console.print(f"[bold cyan]Reading domains from root:[/bold cyan] {self.domains_path}") + console.print(f"[cyan]Reading domains from root: {self.domains_path}") with open(self.domains_path, 'r', encoding='utf-8') as f: self.configSite = json.load(f) site_count = len(self.configSite) if isinstance(self.configSite, dict) else 0 - console.print(f"[bold green]Domains loaded from root file:[/bold green] {site_count} streaming services") + console.print(f"[green]Domains loaded from root file: {site_count} streaming services") else: error_msg = f"domains.json not found in GitHub structure ({github_domains_path}) or root ({self.domains_path}) and fetch_domain_online is disabled" - console.print(f"[bold red]Configuration error:[/bold red] {error_msg}") - console.print("[bold yellow]Tip: Set 'fetch_domain_online' to true to download domains from GitHub[/bold yellow]") + console.print(f"[red]Configuration error: {error_msg}") + console.print("[yellow]Tip: Set 'fetch_domain_online' to true to download domains from GitHub") self.configSite = {} except Exception as e: - console.print(f"[bold red]Local domain file error:[/bold red] {str(e)}") + console.print(f"[red]Local domain file error: {str(e)}") self.configSite = {} def _handle_site_data_fallback(self) -> None: @@ -266,58 +256,28 @@ def _handle_site_data_fallback(self) -> None: github_domains_path = os.path.join(base_path, '.github', '.domain', 'domains.json') if os.path.exists(github_domains_path): - console.print("[bold yellow]Attempting fallback to GitHub structure domains.json file...[/bold yellow]") + console.print("[yellow]Attempting fallback to GitHub structure domains.json file...") try: with open(github_domains_path, 'r', encoding='utf-8') as f: self.configSite = json.load(f) - console.print("[bold green]Fallback to GitHub structure successful[/bold green]") + console.print("[green]Fallback to GitHub structure successful") return except Exception as fallback_error: - console.print(f"[bold red]GitHub structure fallback failed:[/bold red] {str(fallback_error)}") + console.print(f"[red]GitHub structure fallback failed: {str(fallback_error)}") if os.path.exists(self.domains_path): - console.print("[bold yellow]Attempting fallback to root domains.json file...[/bold yellow]") + console.print("[yellow]Attempting fallback to root domains.json file...") try: with open(self.domains_path, 'r', encoding='utf-8') as f: self.configSite = json.load(f) - console.print("[bold green]Fallback to root domains successful[/bold green]") + console.print("[green]Fallback to root domains successful") return except Exception as fallback_error: - console.print(f"[bold red]Root domains fallback failed:[/bold red] {str(fallback_error)}") + console.print(f"[red]Root domains fallback failed: {str(fallback_error)}") - console.print("[bold red]No local domains.json file available for fallback[/bold red]") + console.print("[red]No local domains.json file available for fallback") self.configSite = {} - def download_file(self, url: str, filename: str) -> None: - """ - Download a file from the specified URL. - - Args: - url (str): URL to download from - filename (str): Local filename to save to - """ - try: - logging.info(f"Downloading {filename} from {url}...") - console.print(f"[bold cyan]File download:[/bold cyan] {os.path.basename(filename)}") - response = requests.get(url, timeout=8, headers={'User-Agent': get_userAgent()}, verify=self.get_bool('REQUESTS', 'verify')) - - if response.status_code == 200: - with open(filename, 'wb') as f: - f.write(response.content) - file_size = len(response.content) / 1024 - console.print(f"[bold green]Download complete:[/bold green] {os.path.basename(filename)} ({file_size:.2f} KB)") - - else: - error_msg = f"HTTP Status: {response.status_code}, Response: {response.text[:100]}" - console.print(f"[bold red]Download failed:[/bold red] {error_msg}") - logging.error(f"Download of {filename} failed. {error_msg}") - raise Exception(error_msg) - - except Exception as e: - console.print(f"[bold red]Download error:[/bold red] {str(e)}") - logging.error(f"Download of {filename} failed: {e}") - raise - def get(self, section: str, key: str, data_type: type = str, from_site: bool = False, default: Any = None) -> Any: """ Read a value from the configuration. @@ -402,14 +362,10 @@ def _convert_to_data_type(self, value: Any, data_type: type) -> Any: else: return value except Exception as e: - logging.error(f"Error converting to {data_type.__name__}: {e}") - raise ValueError(f"Cannot convert '{value}' to {data_type.__name__}: {str(e)}") + logging.error(f"Error converting: {data_type.__name__} to value '{value}' with error: {e}") + raise ValueError(f"Error converting: {data_type.__name__} to value '{value}' with error: {e}") # Getters for main configuration - def get_string(self, section: str, key: str, default: str = None) -> str: - """Read a string from the main configuration.""" - return self.get(section, key, str, default=default) - def get_int(self, section: str, key: str, default: int = None) -> int: """Read an integer from the main configuration.""" return self.get(section, key, int, default=default) @@ -435,30 +391,6 @@ def get_site(self, section: str, key: str) -> Any: """Read a value from the site configuration.""" return self.get(section, key, str, True) - def get_site_string(self, section: str, key: str) -> str: - """Read a string from the site configuration.""" - return self.get(section, key, str, True) - - def get_site_int(self, section: str, key: str) -> int: - """Read an integer from the site configuration.""" - return self.get(section, key, int, True) - - def get_site_float(self, section: str, key: str) -> float: - """Read a float from the site configuration.""" - return self.get(section, key, float, True) - - def get_site_bool(self, section: str, key: str) -> bool: - """Read a boolean from the site configuration.""" - return self.get(section, key, bool, True) - - def get_site_list(self, section: str, key: str) -> List[str]: - """Read a list from the site configuration.""" - return self.get(section, key, list, True) - - def get_site_dict(self, section: str, key: str) -> dict: - """Read a dictionary from the site configuration.""" - return self.get(section, key, dict, True) - def set_key(self, section: str, key: str, value: Any, to_site: bool = False) -> None: """ Set a key in the configuration. @@ -486,7 +418,7 @@ def set_key(self, section: str, key: str, value: Any, to_site: bool = False) -> except Exception as e: error_msg = f"Error setting key '{key}' in section '{section}' of {'site' if to_site else 'main'} configuration: {e}" logging.error(error_msg) - console.print(f"[bold red]{error_msg}[/bold red]") + console.print(f"[red]{error_msg}") def save_config(self) -> None: """Save the main configuration to file.""" @@ -498,31 +430,8 @@ def save_config(self) -> None: except Exception as e: error_msg = f"Error saving configuration: {e}" - console.print(f"[bold red]{error_msg}[/bold red]") + console.print(f"[red]{error_msg}") logging.error(error_msg) - - def get_all_sites(self) -> List[str]: - """ - Get the list of all available sites. - - Returns: - List[str]: List of site names - """ - return list(self.configSite.keys()) - - def has_section(self, section: str, in_site: bool = False) -> bool: - """ - Check if a section exists in the configuration. - - Args: - section (str): Section name - in_site (bool, optional): Whether to check in the site configuration. Default: False - - Returns: - bool: True if the section exists, False otherwise - """ - config_source = self.configSite if in_site else self.config - return section in config_source # Initialize the ConfigManager when the module is imported diff --git a/StreamingCommunity/Util/headers.py b/StreamingCommunity/Util/headers.py deleted file mode 100644 index 4d9886066..000000000 --- a/StreamingCommunity/Util/headers.py +++ /dev/null @@ -1,17 +0,0 @@ -# 4.04.24 - -# External library -import ua_generator - - -# Variable -ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge')) - - -def get_userAgent() -> str: - user_agent = ua_generator.generate().text - return user_agent - - -def get_headers() -> dict: - return ua.headers.get() \ No newline at end of file diff --git a/StreamingCommunity/Util/http_client.py b/StreamingCommunity/Util/http_client.py index ac324ae88..bd8407609 100644 --- a/StreamingCommunity/Util/http_client.py +++ b/StreamingCommunity/Util/http_client.py @@ -8,12 +8,16 @@ # External library import httpx +import ua_generator from curl_cffi import requests -# Logic class +# Internal utilities from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.headers import get_userAgent + + +# Variable +ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge')) # Defaults from config @@ -259,3 +263,12 @@ async def async_fetch( # Use same backoff logic for async by sleeping in thread (short duration) _sleep_with_backoff(attempt) return None + + +def get_userAgent() -> str: + user_agent = ua_generator.generate().text + return user_agent + + +def get_headers() -> dict: + return ua.headers.get() \ No newline at end of file diff --git a/StreamingCommunity/Util/installer/__init__.py b/StreamingCommunity/Util/installer/__init__.py index a97373041..94907c4e9 100644 --- a/StreamingCommunity/Util/installer/__init__.py +++ b/StreamingCommunity/Util/installer/__init__.py @@ -3,9 +3,11 @@ from .ffmpeg_install import check_ffmpeg from .bento4_install import check_mp4decrypt from .device_install import check_device_wvd_path +from .megatool_installer import check_megatools __all__ = [ "check_ffmpeg", "check_mp4decrypt", - "check_device_wvd_path" + "check_device_wvd_path", + "check_megatools" ] \ No newline at end of file diff --git a/StreamingCommunity/Util/installer/bento4_install.py b/StreamingCommunity/Util/installer/bento4_install.py index acd00dcfc..9f66e9b87 100644 --- a/StreamingCommunity/Util/installer/bento4_install.py +++ b/StreamingCommunity/Util/installer/bento4_install.py @@ -10,7 +10,6 @@ # External library import requests from rich.console import Console -from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn # Internal utilities @@ -61,21 +60,10 @@ def _download_file(self, url: str, destination: str) -> bool: try: response = requests.get(url, stream=True) response.raise_for_status() - total_size = int(response.headers.get('content-length', 0)) - with open(destination, 'wb') as file, \ - Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeRemainingColumn() - ) as progress: - - download_task = progress.add_task("[green]Downloading Bento4", total=total_size) + with open(destination, 'wb') as file: for chunk in response.iter_content(chunk_size=8192): - size = file.write(chunk) - progress.update(download_task, advance=size) + file.write(chunk) return True @@ -129,7 +117,7 @@ def download(self) -> list: ) zip_path = os.path.join(self.base_dir, "bento4.zip") - console.print(f"[bold blue]Downloading Bento4 from {download_url}[/]") + console.print(f"[blue]Downloading Bento4 from {download_url}") if self._download_file(download_url, zip_path): extracted_files = self._extract_executables(zip_path) @@ -142,7 +130,7 @@ def download(self) -> list: except Exception as e: logging.error(f"Error downloading Bento4: {e}") - console.print(f"[bold red]Error downloading Bento4: {str(e)}[/]") + console.print(f"[red]Error downloading Bento4: {str(e)}") return [] @@ -182,7 +170,7 @@ def check_mp4decrypt() -> Optional[str]: return mp4decrypt_path # STEP 3: Download if not found anywhere - console.print("[cyan]mp4decrypt not found. Downloading...[/]") + console.print("[cyan]mp4decrypt not found. Downloading...") downloader = Bento4Downloader() extracted_files = downloader.download() @@ -190,4 +178,4 @@ def check_mp4decrypt() -> Optional[str]: except Exception as e: logging.error(f"Error checking or downloading mp4decrypt: {e}") - return None + return None \ No newline at end of file diff --git a/StreamingCommunity/Util/installer/ffmpeg_install.py b/StreamingCommunity/Util/installer/ffmpeg_install.py index feca65229..120814322 100644 --- a/StreamingCommunity/Util/installer/ffmpeg_install.py +++ b/StreamingCommunity/Util/installer/ffmpeg_install.py @@ -12,7 +12,6 @@ # External library import requests from rich.console import Console -from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn # Internal utilities @@ -91,7 +90,7 @@ def _check_existing_binaries(self) -> Tuple[Optional[str], Optional[str], Option return ffmpeg_path, ffprobe_path, ffplay_path # STEP 2: Check in binary directory - console.print("[cyan]Checking for FFmpeg in binary directory...[/]") + console.print("[cyan]Checking for FFmpeg in binary directory...") config = FFMPEG_CONFIGURATION[self.os_name] executables = [exe.format(arch=self.arch) for exe in config['executables']] found_executables = [] @@ -136,29 +135,9 @@ def _check_existing_binaries(self) -> Tuple[Optional[str], Optional[str], Option logging.error(f"Error checking existing binaries: {e}") return (None, None, None) - def _get_latest_version(self, repo: str) -> Optional[str]: - """ - Get the latest FFmpeg version from the GitHub releases page. - - Returns: - Optional[str]: The latest version string, or None if retrieval fails. - """ - try: - # Use GitHub API to fetch the latest release - response = requests.get(f'https://api.github.com/repos/{repo}/releases/latest') - response.raise_for_status() - latest_release = response.json() - - # Extract the tag name or version from the release - return latest_release.get('tag_name') - - except Exception as e: - logging.error(f"Unable to get version from GitHub: {e}") - return None - def _download_file(self, url: str, destination: str) -> bool: """ - Download a file from URL with a Rich progress bar display. + Download a file from URL. Parameters: url (str): The URL to download the file from. Should be a direct download link. @@ -170,21 +149,10 @@ def _download_file(self, url: str, destination: str) -> bool: try: response = requests.get(url, stream=True) response.raise_for_status() - total_size = int(response.headers.get('content-length', 0)) - with open(destination, 'wb') as file, \ - Progress( - SpinnerColumn(), - TextColumn("[progress.description]{task.description}"), - BarColumn(), - TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), - TimeRemainingColumn() - ) as progress: - - download_task = progress.add_task("[green]Downloading FFmpeg", total=total_size) + with open(destination, 'wb') as file: for chunk in response.iter_content(chunk_size=8192): - size = file.write(chunk) - progress.update(download_task, advance=size) + file.write(chunk) return True except Exception as e: @@ -250,7 +218,7 @@ def download(self) -> Tuple[Optional[str], Optional[str], Optional[str]]: if self.os_name == 'linux': try: # Attempt to install FFmpeg using apt - console.print("[bold blue]Trying to install FFmpeg using 'sudo apt install ffmpeg'[/]") + console.print("[blue]Trying to install FFmpeg using 'sudo apt install ffmpeg'") result = subprocess.run( ['sudo', 'apt', 'install', '-y', 'ffmpeg'], stdout=subprocess.PIPE, @@ -264,11 +232,11 @@ def download(self) -> Tuple[Optional[str], Optional[str], Optional[str]]: if ffmpeg_path and ffprobe_path: return ffmpeg_path, ffprobe_path, None else: - console.print("[bold yellow]Failed to install FFmpeg via apt. Proceeding with static download.[/]") + console.print("[yellow]Failed to install FFmpeg via apt. Proceeding with static download.") except Exception as e: logging.error(f"Error during 'sudo apt install ffmpeg': {e}") - console.print("[bold red]Error during 'sudo apt install ffmpeg'. Proceeding with static download.[/]") + console.print("[red]Error during 'sudo apt install ffmpeg'. Proceeding with static download.") # Proceed with static download if apt installation fails or is not applicable config = FFMPEG_CONFIGURATION[self.os_name] @@ -283,22 +251,22 @@ def download(self) -> Tuple[Optional[str], Optional[str], Optional[str]]: # Log the current operation logging.info(f"Processing {executable}") - console.print(f"[bold blue]Downloading {executable} from GitHub[/]") + console.print(f"[blue]Downloading {executable} from GitHub") # Download the file if not self._download_file(download_url, download_path): - console.print(f"[bold red]Failed to download {executable}[/]") + console.print(f"[red]Failed to download {executable}") continue # Extract the file if self._extract_file(download_path, final_path): successful_extractions.append(final_path) else: - console.print(f"[bold red]Failed to extract {executable}[/]") + console.print(f"[red]Failed to extract {executable}") except Exception as e: logging.error(f"Error processing {executable}: {e}") - console.print(f"[bold red]Error processing {executable}: {str(e)}[/]") + console.print(f"[red]Error processing {executable}: {str(e)}") continue # Return the results based on successful extractions diff --git a/StreamingCommunity/Util/installer/megatool_installer.py b/StreamingCommunity/Util/installer/megatool_installer.py new file mode 100644 index 000000000..cb3c8ffed --- /dev/null +++ b/StreamingCommunity/Util/installer/megatool_installer.py @@ -0,0 +1,267 @@ +# 15.12.2025 + +import os +import shutil +import tarfile +import zipfile +import logging +from typing import Optional + + +# External library +import requests +from rich.console import Console + + +# Internal utilities +from .binary_paths import binary_paths + + +# Variable +console = Console() + + +MEGATOOLS_CONFIGURATION = { + 'windows': { + 'download_url': 'https://xff.cz/megatools/builds/builds/megatools-{version}-{platform}.zip', + 'versions': { + 'x64': 'win64', + 'x86': 'win32', + }, + 'executables': ['megatools.exe'] + }, + 'darwin': { + 'download_url': 'https://xff.cz/megatools/builds/builds/megatools-{version}-{platform}.tar.gz', + 'versions': { + 'x64': 'linux-x86_64', + 'arm64': 'linux-aarch64' + }, + 'executables': ['megatools'] + }, + 'linux': { + 'download_url': 'https://xff.cz/megatools/builds/builds/megatools-{version}-{platform}.tar.gz', + 'versions': { + 'x64': 'linux-x86_64', + 'x86': 'linux-i686', + 'arm64': 'linux-aarch64' + }, + 'executables': ['megatools'] + } +} + + +class MegatoolsDownloader: + def __init__(self): + self.os_name = binary_paths.system + self.arch = binary_paths.arch + self.home_dir = binary_paths.home_dir + self.base_dir = binary_paths.ensure_binary_directory() + self.version = "1.11.5.20250706" + + def _download_file(self, url: str, destination: str) -> bool: + try: + response = requests.get(url, stream=True) + response.raise_for_status() + + with open(destination, 'wb') as file: + for chunk in response.iter_content(chunk_size=8192): + file.write(chunk) + + return True + + except Exception as e: + logging.error(f"Download error: {e}") + return False + + def _extract_executables(self, archive_path: str) -> list: + try: + extracted_files = [] + config = MEGATOOLS_CONFIGURATION[self.os_name] + executables = config['executables'] + + # Determine if it's a zip or tar.gz + is_zip = archive_path.endswith('.zip') + + if is_zip: + with zipfile.ZipFile(archive_path, 'r') as archive: + + # Extract all contents to a temporary location + temp_extract_dir = os.path.join(self.base_dir, 'temp_megatools') + archive.extractall(temp_extract_dir) + + # Find executables in the extracted folder (search recursively) + for executable in executables: + found = False + for root, dirs, files in os.walk(temp_extract_dir): + if executable in files: + src_path = os.path.join(root, executable) + dst_path = os.path.join(self.base_dir, executable) + + shutil.copy2(src_path, dst_path) + extracted_files.append(dst_path) + found = True + break + + if not found: + logging.warning(f"Executable {executable} not found in archive") + + # Clean up temporary extraction directory + if os.path.exists(temp_extract_dir): + shutil.rmtree(temp_extract_dir) + + else: + with tarfile.open(archive_path, 'r:gz') as archive: + + # Extract all contents to a temporary location + temp_extract_dir = os.path.join(self.base_dir, 'temp_megatools') + archive.extractall(temp_extract_dir) + + # Find executables in the extracted folder (search recursively) + for executable in executables: + found = False + for root, dirs, files in os.walk(temp_extract_dir): + if executable in files: + src_path = os.path.join(root, executable) + dst_path = os.path.join(self.base_dir, executable) + + shutil.copy2(src_path, dst_path) + os.chmod(dst_path, 0o755) + extracted_files.append(dst_path) + found = True + break + + if not found: + logging.warning(f"Executable {executable} not found in archive") + + # Clean up temporary extraction directory + if os.path.exists(temp_extract_dir): + shutil.rmtree(temp_extract_dir) + + return extracted_files + + except Exception as e: + logging.error(f"Extraction error: {e}") + return [] + + def _verify_executable(self, executable_path: str) -> bool: + """Verify that the executable works by running --version.""" + try: + import subprocess + + result = subprocess.run( + [executable_path, '--version'], + capture_output=True, + text=True, + timeout=5 + ) + + # megatools returns exit code 1 when showing version/help, but still outputs correctly + if result.returncode in [0, 1] and ('megatools' in result.stdout.lower() or 'megatools' in result.stderr.lower()): + version_output = result.stdout or result.stderr + logging.info(f"Megatools executable verified: {version_output.splitlines()[0] if version_output else 'OK'}") + return True + + else: + logging.error(f"Executable verification failed with code: {result.returncode}") + return False + + except Exception as e: + logging.error(f"Failed to verify executable: {e}") + return False + + def download(self) -> list: + try: + config = MEGATOOLS_CONFIGURATION[self.os_name] + platform_str = config['versions'].get(self.arch) + + if not platform_str: + raise ValueError(f"Unsupported architecture: {self.arch}") + + download_url = config['download_url'].format( + version=self.version, + platform=platform_str + ) + + # Determine file extension + extension = '.zip' if self.os_name == 'windows' else '.tar.gz' + archive_path = os.path.join(self.base_dir, f"megatools{extension}") + + console.print(f"[blue]Downloading Megatools {self.version}") + + if self._download_file(download_url, archive_path): + extracted_files = self._extract_executables(archive_path) + + # Verify each extracted executable + if extracted_files: + verified_files = [] + + for exe_path in extracted_files: + if self._verify_executable(exe_path): + verified_files.append(exe_path) + + if verified_files: + console.print("[green]Successfully installed Megatools") + os.remove(archive_path) + return verified_files + else: + logging.error("No executables were verified successfully") + else: + logging.error("No executables were extracted") + + # Clean up archive + if os.path.exists(archive_path): + os.remove(archive_path) + + raise Exception("Failed to install Megatools") + + except Exception as e: + logging.error(f"Error downloading Megatools: {e}") + console.print(f"[red]Error downloading Megatools: {str(e)}") + return [] + + +def check_megatools() -> Optional[str]: + """ + Check for megatools in the system and download if not found. + Order: binary directory -> system PATH -> download + + Returns: + Optional[str]: Path to megatools executable or None if not found/downloaded + """ + try: + system_platform = binary_paths.system + megatools_name = "megatools.exe" if system_platform == "windows" else "megatools" + + # STEP 1: Check binary directory FIRST + binary_dir = binary_paths.get_binary_directory() + local_path = os.path.join(binary_dir, megatools_name) + + if os.path.isfile(local_path): + + # Only check execution permissions on Unix systems + if system_platform != 'windows' and not os.access(local_path, os.X_OK): + try: + os.chmod(local_path, 0o755) + except Exception: + pass + + logging.info("megatools found in binary directory") + return local_path + + # STEP 2: Check system PATH + megatools_path = shutil.which(megatools_name) + + if megatools_path: + logging.info("megatools found in system PATH") + return megatools_path + + # STEP 3: Download if not found anywhere + console.print("[cyan]megatools not found. Downloading...") + downloader = MegatoolsDownloader() + extracted_files = downloader.download() + + return extracted_files[0] if extracted_files else None + + except Exception as e: + logging.error(f"Error checking or downloading megatools: {e}") + return None \ No newline at end of file diff --git a/StreamingCommunity/Util/logger.py b/StreamingCommunity/Util/logger.py index 94f52d445..b2f59ded2 100644 --- a/StreamingCommunity/Util/logger.py +++ b/StreamingCommunity/Util/logger.py @@ -74,14 +74,4 @@ def _configure_console_log_file(self): self.logger.addHandler(console_file_handler) except Exception as e: - print(f"Error creating console.log: {e}") - - @staticmethod - def get_logger(name=None): - """ - Get a specific logger for a module/component. - If name is None, returns the root logger. - """ - # Ensure Logger instance is initialized - Logger() - return logging.getLogger(name) \ No newline at end of file + print(f"Error creating console.log: {e}") \ No newline at end of file diff --git a/StreamingCommunity/Util/message.py b/StreamingCommunity/Util/message.py index 0d760c8de..0d2d8ac08 100644 --- a/StreamingCommunity/Util/message.py +++ b/StreamingCommunity/Util/message.py @@ -18,18 +18,19 @@ SHOW = config_manager.get_bool('DEFAULT', 'show_message') -def start_message(): +def start_message(clean: bool=True): """Display a stylized start message in the console.""" - msg = r''' - ___ ______ _ - / _ | ___________ _ _____ _____ __ __ / __/ /________ ___ ___ _ (_)__ ___ _ - / __ |/ __/ __/ _ \ |/|/ / _ `/ __/ \ \ / _\ \/ __/ __/ -_) _ `/ ' \/ / _ \/ _ `/ - /_/ |_/_/ /_/ \___/__,__/\_,_/_/ /_\_\ /___/\__/_/ \__/\_,_/_/_/_/_/_//_/\_, / - /___/ +[red]+[cyan]=======================================================================================[red]+[purple] + ___ ______ _ + / _ | ___________ _ _____ _____[yellow] __ __[purple] / __/ /________ ___ ___ _ (_)__ ___ _ + / __ |/ __/ __/ _ \ |/|/ / _ `/ __/[yellow] \ \ /[purple] _\ \/ __/ __/ -_) _ `/ ' \/ / _ \/ _ `/ + /_/ |_/_/ /_/ \___/__,__/\_,_/_/ [yellow] /_\_\ [purple] /___/\__/_/ \__/\_,_/_/_/_/_/_//_/\_, / + /___/ +[red]+[cyan]=======================================================================================[red]+ '''.rstrip() - if CLEAN: + if CLEAN and clean: os.system("cls" if platform.system() == 'Windows' else "clear") if SHOW: diff --git a/StreamingCommunity/Util/os.py b/StreamingCommunity/Util/os.py index 86ec0072d..193663ad8 100644 --- a/StreamingCommunity/Util/os.py +++ b/StreamingCommunity/Util/os.py @@ -15,7 +15,8 @@ # Internal utilities -from .installer import check_ffmpeg, check_mp4decrypt, check_device_wvd_path +from .installer import check_ffmpeg, check_mp4decrypt, check_device_wvd_path, check_megatools +from StreamingCommunity.Lib.DASH.cdm_helpher import get_info_wvd # Variable @@ -39,37 +40,6 @@ def _get_max_length(self) -> int: """Get max filename length based on OS.""" return 255 if self.system == 'windows' else 4096 - def _normalize_windows_path(self, path: str) -> str: - """Normalize Windows paths.""" - if not path or self.system != 'windows': - return path - - # Preserve network paths (UNC and IP-based) - if path.startswith('\\\\') or path.startswith('//'): - return path.replace('/', '\\') - - # Handle drive letters - if len(path) >= 2 and path[1] == ':': - drive = path[0:2] - rest = path[2:].replace('/', '\\').lstrip('\\') - return f"{drive}\\{rest}" - - return path.replace('/', '\\') - - def _normalize_mac_path(self, path: str) -> str: - """Normalize macOS paths.""" - if not path or self.system != 'darwin': - return path - - # Convert Windows separators to Unix - normalized = path.replace('\\', '/') - - # Ensure absolute paths start with / - if normalized.startswith('/'): - return os.path.normpath(normalized) - - return normalized - def get_sanitize_file(self, filename: str, year: str = None) -> str: """Sanitize filename. Optionally append a year in format ' (YYYY)' if year is provided and valid.""" if not filename: @@ -196,55 +166,6 @@ def remove_folder(self, folder_path: str) -> bool: logging.error(f"Folder removal error: {e}") return False - def remove_files_except_one(self, folder_path: str, keep_file: str) -> None: - """ - Delete all files in a folder except for one specified file. - - Parameters: - - folder_path (str): The path to the folder containing the files. - - keep_file (str): The filename to keep in the folder. - """ - - try: - # First, try to make all files writable - for root, dirs, files in os.walk(self.temp_dir): - for dir_name in dirs: - dir_path = os.path.join(root, dir_name) - os.chmod(dir_path, 0o755) # rwxr-xr-x - for file_name in files: - file_path = os.path.join(root, file_name) - os.chmod(file_path, 0o644) # rw-r--r-- - - # Then remove the directory tree - shutil.rmtree(self.temp_dir, ignore_errors=True) - - # If directory still exists after rmtree, try force remove - if os.path.exists(self.temp_dir): - import subprocess - subprocess.run(['rm', '-rf', self.temp_dir], check=True) - - except Exception as e: - logging.error(f"Failed to cleanup temporary directory: {str(e)}") - pass - - def check_file(self, file_path: str) -> bool: - """ - Check if a file exists at the given file path. - - Parameters: - file_path (str): The path to the file. - - Returns: - bool: True if the file exists, False otherwise. - """ - try: - logging.info(f"Check if file exists: {file_path}") - return os.path.exists(file_path) - - except Exception as e: - logging.error(f"An error occurred while checking file existence: {e}") - return False - class InternetManager(): def format_file_size(self, size_bytes: float) -> str: @@ -294,6 +215,7 @@ def __init__(self): self.ffplay_path = None self.mp4decrypt_path = None self.wvd_path = None + self.megatools_path = None self.init() def init(self): @@ -302,6 +224,7 @@ def init(self): self.ffmpeg_path, self.ffprobe_path, _ = check_ffmpeg() self.mp4decrypt_path = check_mp4decrypt() self.wvd_path = check_device_wvd_path() + self.megatools_path = check_megatools() self._display_binary_paths() def _display_binary_paths(self): @@ -310,15 +233,17 @@ def _display_binary_paths(self): 'ffmpeg': self.ffmpeg_path, 'ffprobe': self.ffprobe_path, 'mp4decrypt': self.mp4decrypt_path, - 'wvd': self.wvd_path + 'wvd': self.wvd_path, + 'megatools': self.megatools_path } path_strings = [] for name, path in paths.items(): path_str = f"'{path}'" if path else "None" - path_strings.append(f"[red]{name} [bold yellow]{path_str}[/bold yellow]") + path_strings.append(f"[red]{name} [yellow]{path_str}") - console.print(f"[cyan]Path: {', [white]'.join(path_strings)}") + console.print(f"[cyan]Utilities: {', [white]'.join(path_strings)}") + get_info_wvd(self.wvd_path) # Initialize the os_summary, internet_manager, and os_manager when the module is imported @@ -364,4 +289,8 @@ def get_mp4decrypt_path(): def get_wvd_path(): """Returns the path of wvd.""" - return os_summary.wvd_path \ No newline at end of file + return os_summary.wvd_path + +def get_megatools_path(): + """Returns the path of megatools.""" + return os_summary.megatools_path \ No newline at end of file diff --git a/StreamingCommunity/Util/table.py b/StreamingCommunity/Util/table.py index bdecf704c..c9b05e69b 100644 --- a/StreamingCommunity/Util/table.py +++ b/StreamingCommunity/Util/table.py @@ -18,12 +18,7 @@ # Internal utilities from .os import get_call_stack from .message import start_message - - -# Telegram bot instance -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance -from StreamingCommunity.Util.config_json import config_manager -TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') +from StreamingCommunity.Api.Template.loader import folder_name as lazy_loader_folder @@ -51,26 +46,6 @@ def add_column(self, column_info: Dict[str, Dict[str, str]]) -> None: """ self.column_info = column_info - def set_table_title(self, title: str) -> None: - """ - Set the table title. - - Parameters: - - title (str): The title to display above the table. - """ - self.table_title = title - - def set_table_style(self, style: str = "blue", show_lines: bool = False) -> None: - """ - Set the table border style and row lines. - - Parameters: - - style (str): Border color (e.g., "blue", "green", "magenta", "cyan") - - show_lines (bool): Whether to show lines between rows - """ - self.table_style = style - self.show_lines = show_lines - def add_tv_show(self, tv_show: Dict[str, Any]) -> None: """ Add a TV show to the list of TV shows. @@ -101,7 +76,7 @@ def display_data(self, data_slice: List[Dict[str, Any]]) -> None: title=self.table_title, box=box.ROUNDED, show_header=True, - header_style="bold cyan", + header_style="cyan", border_style=self.table_style, show_lines=self.show_lines, padding=(0, 1) @@ -154,7 +129,7 @@ def run_back_command(research_func: dict) -> None: sys.path.insert(0, project_root) # Import using full absolute import - module_path = f'StreamingCommunity.Api.Site.{site_name}' + module_path = f'StreamingCommunity.Api.{lazy_loader_folder}.{site_name}' module = importlib.import_module(module_path) # Get and call the search function @@ -181,7 +156,7 @@ def run(self, force_int_input: bool = False, max_int_input: int = 0) -> str: """ if not self.tv_shows: logging.error("Error: No data available for display.") - return "" + sys.exit(0) if not self.column_info: logging.error("Error: Columns not configured.") @@ -189,8 +164,6 @@ def run(self, force_int_input: bool = False, max_int_input: int = 0) -> str: total_items = len(self.tv_shows) last_command = "" - is_telegram = config_manager.get_bool('DEFAULT', 'telegram_bot') - bot = get_bot_instance() if is_telegram else None while True: start_message() @@ -217,38 +190,26 @@ def run(self, force_int_input: bool = False, max_int_input: int = 0) -> str: self.console.print("\n[green]Press [red]Enter [green]for next page, [red]'q' [green]to quit, or [red]'back' [green]to search.") if not force_int_input: - prompt_msg = ("\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, " - "[yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end") - telegram_msg = "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" - - if is_telegram: - key = bot.ask("select_title_episode", telegram_msg, None) - else: - key = Prompt.ask(prompt_msg) + prompt_msg = ("\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, [yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end") + key = Prompt.ask(prompt_msg) + else: # 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" - - if is_telegram: - key = bot.ask("select_title", telegram_msg, None) - else: - key = Prompt.ask(prompt_msg, choices=choices, show_choices=False) + key = Prompt.ask(prompt_msg, choices=choices, show_choices=False) last_command = key if key.lower() in ["q", "quit"]: break + elif key == "": self.slice_start += self.step self.slice_end += self.step if self.slice_end > total_items: self.slice_end = total_items + elif (key.lower() in ["b", "back"]) and research_func: TVShowManager.run_back_command(research_func) else: @@ -259,36 +220,23 @@ def run(self, force_int_input: bool = False, max_int_input: int = 0) -> str: self.console.print("\n[green]You've reached the end. [red]Enter [green]for first page, [red]'q' [green]to quit, or [red]'back' [green]to search.") if not force_int_input: - prompt_msg = ("\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, " - "[yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end") - telegram_msg = "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" - - if is_telegram: - key = bot.ask("select_title_episode", telegram_msg, None) - else: - key = Prompt.ask(prompt_msg) + prompt_msg = ("\n[cyan]Insert media index [yellow](e.g., 1), [red]* [cyan]to download all media, [yellow](e.g., 1-2) [cyan]for a range of media, or [yellow](e.g., 3-*) [cyan]to download from a specific index to the end") + key = Prompt.ask(prompt_msg) else: # 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" - - if is_telegram: - key = bot.ask("select_title", telegram_msg, None) - else: - key = Prompt.ask(prompt_msg, choices=choices, show_choices=False) + key = Prompt.ask(prompt_msg, choices=choices, show_choices=False) last_command = key if key.lower() in ["q", "quit"]: break + elif key == "": self.slice_start = 0 self.slice_end = self.step + elif (key.lower() in ["b", "back"]) and research_func: TVShowManager.run_back_command(research_func) else: diff --git a/StreamingCommunity/__init__.py b/StreamingCommunity/__init__.py deleted file mode 100644 index 17d977133..000000000 --- a/StreamingCommunity/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# 11.03.25 - -from .run import main -from .Lib.Downloader.HLS.downloader import HLS_Downloader -from .Lib.Downloader.MP4.downloader import MP4_downloader -from .Lib.Downloader.TOR.downloader import TOR_downloader -from .Lib.Downloader.DASH.downloader import DASH_Downloader -from .Lib.Downloader.MEGA.mega import Mega_Downloader - -__all__ = [ - "main", - "HLS_Downloader", - "MP4_downloader", - "TOR_downloader", - "DASH_Downloader", - "Mega_Downloader", -] \ No newline at end of file diff --git a/StreamingCommunity/__main__.py b/StreamingCommunity/__main__.py index 25aa588c1..a190e3366 100644 --- a/StreamingCommunity/__main__.py +++ b/StreamingCommunity/__main__.py @@ -1,3 +1,5 @@ +# 17.12.25 + from .run import main -main() +main() \ No newline at end of file diff --git a/StreamingCommunity/global_search.py b/StreamingCommunity/global_search.py index d420de36e..19a2615d2 100644 --- a/StreamingCommunity/global_search.py +++ b/StreamingCommunity/global_search.py @@ -1,22 +1,19 @@ # 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 +from StreamingCommunity.Util import start_message +from StreamingCommunity.Api.Template import load_search_functions # Variable @@ -24,73 +21,6 @@ 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) - use_for = getattr(mod, '_useFor', 'other') - priority = getattr(mod, '_priority', 0) - - if priority == 0: - if not getattr(mod, '_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): """ @@ -118,10 +48,10 @@ def global_search(search_terms: str = None, selected_sites: list = None): # 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") + console.print("\n[green]Select sites to search:") + console.print("[cyan]1. Search all sites") + console.print("[cyan]2. Search by category") + console.print("[cyan]3. Select specific sites") choice = msg.ask("[green]Enter your choice (1-3)", choices=["1", "2", "3"], default="1") @@ -131,9 +61,9 @@ def global_search(search_terms: str = None, selected_sites: list = None): elif choice == "2": # Search by category - console.print("\n[bold green]Select categories to search:[/bold green]") + console.print("\n[green]Select categories to search:") for i, category in enumerate(sites_by_category.keys(), 1): - console.print(f"[bold cyan]{i}.[/bold cyan] {category.capitalize()}") + console.print(f"[cyan]{i}. {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(",")] @@ -145,55 +75,50 @@ def global_search(search_terms: str = None, selected_sites: list = None): else: # Select specific sites - console.print("\n[bold green]Select specific sites to search:[/bold green]") + console.print("\n[green]Select specific sites to search:") 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}") + console.print(f"[cyan]{i}.{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 \n") + console.print(f"\n[green]Searching for: [yellow]{search_terms}") + console.print(f"[green]Searching across: {len(selected_sites)} sites \n") - 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() + console.print(f"[cyan]Search url in: {site_name}") - # 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) - 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 + # 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"\n[green]Found {len(database.media_list)} results from {site_name}") + # Add source information + item_dict['source'] = site_name + item_dict['source_alias'] = alias + all_results[alias].append(item_dict) + + console.print(f"[green]Found result: {len(database.media_list)}\n") - except Exception as e: - console.print(f"[bold red]Error searching {site_name}:[/bold red] {str(e)}") - - progress.update(search_task, advance=1) + except Exception as e: + console.print(f"[red]Error searching {site_name}: {str(e)}") # Display the consolidated results if all_results: @@ -212,7 +137,7 @@ def global_search(search_terms: str = None, selected_sites: list = None): process_selected_item(selected_item, search_functions) else: - console.print(f"\n[bold red]No results found for:[/bold red] [yellow]{search_terms}[/yellow]") + console.print(f"\n[red]No results found for: [yellow]{search_terms}") # Optionally offer to search again or return to main menu if msg.ask("[green]Search again? (y/n)", choices=["y", "n"], default="y") == "y": @@ -231,9 +156,9 @@ def display_consolidated_results(all_media_items, search_terms): time.sleep(1) start_message() - console.print(f"\n[bold green]Search results for:[/bold green] [yellow]{search_terms}[/yellow] \n") + console.print(f"\n[green]Search results for: [yellow]{search_terms} \n") - table = Table(show_header=True, header_style="bold cyan") + table = Table(show_header=True, header_style="cyan") table.add_column("#", style="dim", width=4) table.add_column("Title", min_width=20) table.add_column("Type", width=15) @@ -291,13 +216,13 @@ def process_selected_item(selected_item, search_functions): """ 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]") + console.print("[red]Error: Cannot process this item - source information missing.") 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')}") + console.print(f"\n[green]Processing selection from: {selected_item.get('source')}") # Extract necessary information to pass to the site's search function item_id = None @@ -310,16 +235,16 @@ def process_selected_item(selected_item, search_functions): 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})") + console.print(f"[green]Selected item: {item_title} (ID: {item_id}, Type: {item_type})") try: func(direct_item=selected_item) except Exception as e: - console.print(f"[bold red]Error processing download:[/bold red] {str(e)}") + console.print(f"[red]Error processing download: {str(e)}") logging.exception("Download processing error") else: - console.print("[bold red]Error: Item ID not found. Available fields:[/bold red]") + console.print("[red]Error: Item ID not found. Available fields:") for key in selected_item.keys(): - console.print(f"[yellow]- {key}: {selected_item[key]}[/yellow]") + console.print(f"[yellow]- {key}: {selected_item[key]}") \ No newline at end of file diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index 698ad0697..507e35291 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -19,24 +19,19 @@ # Internal utilities from .global_search import global_search -from StreamingCommunity.Api.Template.loader import load_search_functions -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.Util.os import os_manager -from StreamingCommunity.Util.logger import Logger +from StreamingCommunity.Api.Template import load_search_functions +from StreamingCommunity.Api.Template.loader import folder_name as lazy_loader_folder +from StreamingCommunity.Util import config_manager, os_manager, start_message, Logger from StreamingCommunity.Upload.update import update as git_update -from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance, TelegramSession # Config -TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') COLOR_MAP = { "anime": "red", "film_&_serie": "yellow", - "serie": "blue", - "torrent": "white" + "serie": "blue" } -CATEGORY_MAP = {1: "anime", 2: "film_&_serie", 3: "serie", 4: "torrent"} +CATEGORY_MAP = {1: "anime", 2: "film_&_serie", 3: "serie"} # Variable @@ -55,7 +50,7 @@ def run_function(func: Callable[..., None], close_console: bool = False, search_ def initialize(): """Initialize the application with system checks and setup.""" - start_message() + start_message(False) # Windows 7 terminal size fix if platform.system() == "Windows" and "7" in platform.version(): @@ -196,13 +191,13 @@ def execute_hooks(stage: str) -> None: if stdout: logging.info(f"Hook '{name}' stdout: {stdout}") try: - console.print(f"[cyan][hook:{name} stdout][/cyan]\n{stdout}") + console.print(f"[cyan][hook:{name} stdout]\n{stdout}") except Exception: pass if stderr: logging.warning(f"Hook '{name}' stderr: {stderr}") try: - console.print(f"[yellow][hook:{name} stderr][/yellow]\n{stderr}") + console.print(f"[yellow][hook:{name} stderr]\n{stderr}") except Exception: pass @@ -275,7 +270,7 @@ def setup_argument_parser(search_functions): for alias, (_func, _use_for) in search_functions.items(): module_name = alias.split("_")[0].lower() try: - mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}') + mod = importlib.import_module(f'StreamingCommunity.Api.{lazy_loader_folder}.{module_name}') module_info[module_name] = int(getattr(mod, 'indice')) except Exception: continue @@ -290,7 +285,6 @@ def setup_argument_parser(search_functions): ) # Add arguments - parser.add_argument("script_id", nargs="?", default="unknown", help="ID dello script") parser.add_argument('-s', '--search', default=None, help='Search terms') parser.add_argument('--global', action='store_true', help='Global search across sites') parser.add_argument('--not_close', type=bool, help='Keep console open after execution') @@ -352,7 +346,7 @@ def build_function_mappings(search_functions): for alias, (func, use_for) in search_functions.items(): module_name = alias.split("_")[0] try: - mod = importlib.import_module(f'StreamingCommunity.Api.Site.{module_name}') + mod = importlib.import_module(f'StreamingCommunity.Api.{lazy_loader_folder}.{module_name}') site_index = str(getattr(mod, 'indice')) input_to_function[site_index] = func choice_labels[site_index] = (module_name.capitalize(), use_for.lower()) @@ -373,7 +367,7 @@ def handle_direct_site_selection(args, input_to_function, module_name_to_functio if func_to_run is None: available_sites = ", ".join(sorted(module_name_to_function.keys())) - console.print(f"[red]Unknown site:[/red] '{args.site}'. Available: [yellow]{available_sites}[/yellow]") + console.print(f"[red]Unknown site: '{args.site}'. Available: [yellow]{available_sites}") return False # Handle auto-first option @@ -386,9 +380,9 @@ def handle_direct_site_selection(args, input_to_function, module_name_to_functio func_to_run(direct_item=item_dict) return True else: - console.print("[yellow]No results found. Falling back to interactive mode.[/yellow]") + console.print("[yellow]No results found. Falling back to interactive mode.") except Exception as e: - console.print(f"[red]Auto-first failed:[/red] {str(e)}") + console.print(f"[red]Auto-first failed: {str(e)}") run_function(func_to_run, search_terms=search_terms) return True @@ -396,51 +390,29 @@ def handle_direct_site_selection(args, input_to_function, module_name_to_functio def get_user_site_selection(args, choice_labels): """Get site selection from user (interactive or category-based).""" - bot = get_bot_instance() if TELEGRAM_BOT else None - if args.category: selected_category = CATEGORY_MAP.get(args.category) category_sites = [(key, label[0]) for key, label in choice_labels.items() if label[1] == selected_category] if len(category_sites) == 1: - console.print(f"[green]Selezionato automaticamente: {category_sites[0][1]}[/green]") + console.print(f"[green]Selezionato automaticamente: {category_sites[0][1]}") return category_sites[0][0] - # Multiple sites in category - color = COLOR_MAP.get(selected_category, 'white') - prompt_items = [f"[{color}]({k}) {v}[/{color}]" for k, v in category_sites] - prompt_line = ", ".join(prompt_items) - - if TELEGRAM_BOT: - console.print(f"\nInsert site: {prompt_line}") - return bot.ask("select_site", f"Insert site: {prompt_line}", None) - else: - return msg.ask(f"\n[cyan]Insert site: {prompt_line}", choices=[k for k, _ in category_sites], show_choices=False) - else: # Show all sites legend_text = " | ".join([f"[{color}]{cat.capitalize()}[/{color}]" for cat, color in COLOR_MAP.items()]) legend_text += " | [magenta]Global[/magenta]" - console.print(f"\n[bold cyan]Category Legend:[/bold cyan] {legend_text}") + console.print(f"\n[cyan]Category Legend: {legend_text}") - if TELEGRAM_BOT: - category_legend = "Categorie: \n" + " | ".join([cat.capitalize() for cat in COLOR_MAP.keys()]) + " | Global" - prompt_message = "Inserisci il sito:\n" + "\n".join([f"{key}: {label[0]}" for key, label in choice_labels.items()]) - console.print(f"\n{prompt_message}") - return bot.ask("select_provider", f"{category_legend}\n\n{prompt_message}", None) - else: - choice_keys = list(choice_labels.keys()) + ["global"] - prompt_message = "[cyan]Insert site: " + ", ".join([ - f"[{COLOR_MAP.get(label[1], 'white')}]({key}) {label[0]}[/{COLOR_MAP.get(label[1], 'white')}]" - for key, label in choice_labels.items() - ]) + ", [magenta](global) Global[/magenta]" - return msg.ask(prompt_message, choices=choice_keys, default="0", show_choices=False, show_default=False) - + choice_keys = list(choice_labels.keys()) + ["global"] + prompt_message = "[cyan]Insert site: " + ", ".join([ + f"[{COLOR_MAP.get(label[1], 'white')}]({key}) {label[0]}[/{COLOR_MAP.get(label[1], 'white')}]" + for key, label in choice_labels.items() + ]) + ", [magenta](global) Global[/magenta]" + return msg.ask(prompt_message, choices=choice_keys, default="0", show_choices=False, show_default=False) -def main(script_id=0): - if TELEGRAM_BOT: - get_bot_instance().send_message(f"Avviato script {script_id}", None) +def main(): Logger() execute_hooks('pre_run') initialize() @@ -471,18 +443,12 @@ def main(script_id=0): if category in input_to_function: run_function(input_to_function[category], search_terms=args.search) else: - if TELEGRAM_BOT: - get_bot_instance().send_message("Categoria non valida", None) console.print("[red]Invalid category.") if getattr(args, 'not_close'): restart_script() else: force_exit() - if TELEGRAM_BOT: - get_bot_instance().send_message("Chiusura in corso", None) - script_id = TelegramSession.get_session() - if script_id != "unknown": - TelegramSession.deleteScriptId(script_id) + finally: - execute_hooks('post_run') + execute_hooks('post_run') \ No newline at end of file diff --git a/Test/Downloads/DASH.py b/Test/Downloads/DASH.py index b9d5f6857..41c3df572 100644 --- a/Test/Downloads/DASH.py +++ b/Test/Downloads/DASH.py @@ -10,29 +10,29 @@ sys.path.append(src_path) -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.logger import Logger -from StreamingCommunity import DASH_Downloader +from StreamingCommunity.Util import Logger, start_message +from StreamingCommunity.Lib.DASH.downloader import DASH_Downloader start_message() logger = Logger() -mpd_url = "" +mpd_url = '' mpd_headers = {} -license_url = "" -license_params = {} +license_url = '' license_headers = {} +license_params = {} +license_ley = None dash_process = DASH_Downloader( - license_url=license_url, mpd_url=mpd_url, - output_path="out.mp4", + license_url=license_url, + output_path=r".\Video\Prova.mp4" ) dash_process.parse_manifest(custom_headers=mpd_headers) -if dash_process.download_and_decrypt(custom_headers=license_headers, query_params=license_params): +if dash_process.download_and_decrypt(custom_headers=license_headers, query_params=license_params, key=license_ley): dash_process.finalize_output() status = dash_process.get_status() diff --git a/Test/Downloads/HLS.py b/Test/Downloads/HLS.py index c40ebbcdb..c39f76eb9 100644 --- a/Test/Downloads/HLS.py +++ b/Test/Downloads/HLS.py @@ -10,16 +10,17 @@ sys.path.append(src_path) -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.logger import Logger -from StreamingCommunity import HLS_Downloader +from StreamingCommunity.Util import Logger, start_message +from StreamingCommunity.Lib.HLS import HLS_Downloader start_message() Logger() hls_process = HLS_Downloader( - output_path=".\\Video\\test.mp4", - m3u8_url="https://acdn.ak-stream-videoplatform.sky.it/hls/2024/11/21/968275/master.m3u8" + m3u8_url="", + headers={}, + license_url=None, + output_path=r".\Video\Prova.", ).start() thereIsError = hls_process['error'] is not None diff --git a/Test/Downloads/MEGA.py b/Test/Downloads/MEGA.py index ce80a372b..f56f6c95d 100644 --- a/Test/Downloads/MEGA.py +++ b/Test/Downloads/MEGA.py @@ -10,17 +10,17 @@ sys.path.append(src_path) -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.logger import Logger -from StreamingCommunity import Mega_Downloader +from StreamingCommunity.Util import Logger, start_message +from StreamingCommunity.Lib.MEGA import MEGA_Downloader start_message() Logger() -mega = Mega_Downloader() -m = mega.login() +mega = MEGA_Downloader( + choose_files=True +) -output_path = m.download_url( - url="https://mega.nz/file/0kgCWZZB#7u....", - dest_path=".\\prova.mp4" +output_path = mega.download_url( + url="", + dest_path=r".\Video\Prova.mp4", ) \ No newline at end of file diff --git a/Test/Downloads/MP4.py b/Test/Downloads/MP4.py index 0b299b4d0..6bf5daf11 100644 --- a/Test/Downloads/MP4.py +++ b/Test/Downloads/MP4.py @@ -10,16 +10,15 @@ sys.path.append(src_path) -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.logger import Logger -from StreamingCommunity import MP4_downloader +from StreamingCommunity.Util import Logger, start_message +from StreamingCommunity.Lib.MP4 import MP4_Downloader start_message() Logger() -path, kill_handler = MP4_downloader( +path, kill_handler = MP4_Downloader( url="https://148-251-75-109.top/Getintopc.com/IDA_Pro_2020.mp4", - path=r".\\Video\\undefined.mp4" + path=r".\Video\Prova.mp4" ) thereIsError = path is None diff --git a/Test/Downloads/TOR.py b/Test/Downloads/TOR.py deleted file mode 100644 index e89fe14d5..000000000 --- a/Test/Downloads/TOR.py +++ /dev/null @@ -1,25 +0,0 @@ -# 23.06.24 -# ruff: noqa: E402 - -import os -import sys - - -# Fix import -src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) -sys.path.append(src_path) - - -from StreamingCommunity.Util.message import start_message -from StreamingCommunity.Util.logger import Logger -from StreamingCommunity import TOR_downloader - - -# Test -start_message() -Logger() -manager = TOR_downloader() - -magnet_link = """magnet:?xt=urn:btih:0E0CDB5387B4C71C740BD21E8144F3735C3F899E&dn=Krapopolis.S02E14.720p.x265-TiPEX&tr=udp%3A%2F%2Ftracker.torrent.eu.org%3A451%2Fannounce&tr=udp%3A%2F%2Fopen.stealth.si%3A80%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=udp%3A%2F%2Fexplodie.org%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.tiny-vps.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.dler.com%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.darkness.services%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.opentrackr.org%3A1337%2Fannounce&tr=http%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&tr=udp%3A%2F%2Fopentracker.i2p.rocks%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.internetwarriors.net%3A1337%2Fannounce&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969%2Fannounce&tr=udp%3A%2F%2Fcoppersurfer.tk%3A6969%2Fannounce&tr=udp%3A%2F%2Ftracker.zer0day.to%3A1337%2Fannounce""" -manager.add_magnet_link(magnet_link, save_path=os.path.join(src_path, "Video")) -manager.start_download() \ No newline at end of file diff --git a/Test/Util/hooks.py b/Test/Util/hooks.py index f2dbc800a..75b80ed9d 100644 --- a/Test/Util/hooks.py +++ b/Test/Util/hooks.py @@ -72,4 +72,4 @@ def main(): if __name__ == "__main__": - sys.exit(main()) + sys.exit(main()) \ No newline at end of file diff --git a/config.json b/config.json index 0e4d8d5a2..702b4b936 100644 --- a/config.json +++ b/config.json @@ -2,8 +2,7 @@ "DEFAULT": { "debug": false, "show_message": true, - "fetch_domain_online": true, - "telegram_bot": false + "fetch_domain_online": true }, "OUT_FOLDER": { "root_path": "Video", @@ -13,12 +12,6 @@ "map_episode_name": "%(episode_name) S%(season)E%(episode)", "add_siteName": false }, - "QBIT_CONFIG": { - "host": "192.168.1.1", - "port": "5555", - "user": "root", - "pass": "toor" - }, "M3U8_DOWNLOAD": { "default_video_workers": 8, "default_audio_workers": 8, @@ -58,6 +51,10 @@ "crunchyroll": { "device_id": "", "etp_rt": "" + }, + "tubi": { + "email": "", + "password": "" } } } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 478499ff1..46c02d212 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,4 @@ jsbeautifier pathvalidate pycryptodomex ua-generator -qbittorrent-api -pyTelegramBotAPI -pywidevine -python-dotenv \ No newline at end of file +pywidevine \ No newline at end of file diff --git a/test_run.py b/test_run.py index 70dc0b8e2..e8e70f4d6 100644 --- a/test_run.py +++ b/test_run.py @@ -1,25 +1,8 @@ # 26.11.24 -import sys - # Internal utilities from StreamingCommunity.run import main -from StreamingCommunity.Util.config_json import config_manager -from StreamingCommunity.TelegramHelp.telegram_bot import TelegramRequestManager, TelegramSession - - -# Variable -TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') - -if TELEGRAM_BOT: - request_manager = TelegramRequestManager() - request_manager.clear_file() - script_id = sys.argv[1] if len(sys.argv) > 1 else "unknown" - TelegramSession.set_session(script_id) - main(script_id) - -else: - main() \ No newline at end of file +main() \ No newline at end of file