From 7880f56a820620103f57c83557b70912b0933367 Mon Sep 17 00:00:00 2001 From: Lovi <62809003+Lovi-0@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:28:11 +0100 Subject: [PATCH] Commenti di alto spessore, fix streamingcommunity, ddlstreamitaly, guardaserie --- .gitignore | 2 +- Makefile | 5 - README.md | 196 ++++++------ .../Api/Player/Helper/Vixcloud/js_parser.py | 140 +++++++++ .../Src/Api/Player/Helper/Vixcloud/util.py | 156 ++-------- StreamingCommunity/Src/Api/Player/vixcloud.py | 282 +++++------------- .../Src/Api/Site/animeunity/__init__.py | 6 +- .../Src/Api/Site/animeunity/film_serie.py | 131 ++++++++ .../Api/Site/animeunity/util/ScrapeSerie.py | 97 ++++++ .../Src/Api/Site/ddlstreamitaly/series.py | 4 +- .../Site/ddlstreamitaly/util/ScrapeSerie.py | 83 ++++++ .../Src/Api/Site/guardaserie/series.py | 2 +- .../Api/Site/guardaserie/util/ScrapeSerie.py | 110 +++++++ .../Api/Site/streamingcommunity/__init__.py | 4 +- .../Src/Api/Site/streamingcommunity/film.py | 8 +- .../Src/Api/Site/streamingcommunity/series.py | 32 +- .../streamingcommunity/util/ScrapeSerie.py | 113 +++++++ .../Src/Api/Template/Util/manage_ep.py | 20 +- .../Src/Lib/Downloader/HLS/downloader.py | 2 +- .../Src/Lib/Downloader/HLS/segments.py | 124 ++++++-- .../Src/Lib/Downloader/MP4/downloader.py | 127 +++++--- StreamingCommunity/Src/Lib/TMBD/tmdb.py | 2 +- StreamingCommunity/Src/Util/os.py | 6 +- StreamingCommunity/run.py | 10 +- Test/Player/helper/vixcloud.py | 38 +++ dockerfile | 20 -- test_run.py | 2 + unix_install.sh | 200 ------------- update.py | 194 ------------ win_install.bat | 134 --------- 30 files changed, 1129 insertions(+), 1121 deletions(-) delete mode 100644 Makefile create mode 100644 StreamingCommunity/Src/Api/Player/Helper/Vixcloud/js_parser.py create mode 100644 StreamingCommunity/Src/Api/Site/animeunity/film_serie.py create mode 100644 StreamingCommunity/Src/Api/Site/animeunity/util/ScrapeSerie.py create mode 100644 StreamingCommunity/Src/Api/Site/ddlstreamitaly/util/ScrapeSerie.py create mode 100644 StreamingCommunity/Src/Api/Site/guardaserie/util/ScrapeSerie.py create mode 100644 StreamingCommunity/Src/Api/Site/streamingcommunity/util/ScrapeSerie.py create mode 100644 Test/Player/helper/vixcloud.py delete mode 100644 dockerfile delete mode 100644 unix_install.sh delete mode 100644 update.py delete mode 100644 win_install.bat diff --git a/.gitignore b/.gitignore index c118cf2..be7663d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,5 +58,5 @@ venv.bak/ Video note.txt list_proxy.txt -config.json +cmd.txt downloaded_files \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 656fcbd..0000000 --- a/Makefile +++ /dev/null @@ -1,5 +0,0 @@ -build-container: - docker build -t streaming-community-api . - -run-container: - docker run --rm -it -p 8000:8000 -v ${LOCAL_DIR}:/app/Video -v ./config.json:/app/config.json streaming-community-api diff --git a/README.md b/README.md index 9af8763..9d8dbc6 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,89 @@ -

- -

+# StreamingCommunity Downloader 🎬 -This repository provide a simple script designed to downloading films and series from a variety of supported streaming platforms. [SITE](#website-status-) +![Project Logo](https://i.ibb.co/f4h5Y2m/min-logo.png) -## Join us 🌟 +A versatile script designed to download films and series from various supported streaming platforms. -You can chat, help improve this repo, or just hang around for some fun in the **Git_StreamingCommunity** Discord [Server](https://discord.com/invite/8vV68UGRc7) +## 🤝 Join our Community -# Table of Contents +Chat, contribute, and have fun in our **Git_StreamingCommunity** Discord [Server](https://discord.com/invite/8vV68UGRc7) -* [INSTALLATION](#installation) - * [Automatic Installation](#automatic-installation) - * [Usage](#usage-automatic) - * [Supported OSs for Automatic Installation](#automatic-installation-os) - * [Manual Installation](#manual-installation) - * [Requirement](#requirement) - * [Usage](#usage-manual) - * [Win 7](https://github.com/Ghost6446/StreamingCommunity_api/wiki/Installation#win-7) - * [Termux](https://github.com/Ghost6446/StreamingCommunity_api/wiki/Termux) - * [Update](#update) -* [CONFIGURATION](#configuration) -* [DOCKER](#docker) -* [TUTORIAL](#tutorial) -* [TO DO](#to-do) +## 📋 Table of Contents -# INSTALLATION +- [Installation](#installation) + - [PyPI Installation](#pypi-installation) + - [Automatic Installation](#automatic-installation) + - [Manual Installation](#manual-installation) + - [Win 7](https://github.com/Ghost6446/StreamingCommunity_api/wiki/Installation#win-7) + - [Termux](https://github.com/Ghost6446/StreamingCommunity_api/wiki/Termux) +- [Configuration](#configuration) +- [Tutorial](#tutorial) +- [To Do](#to-do) -## Automatic Installation +## 💻 Installation -### Supported OSs for Automatic Installation 💿 +### 1. PyPI Installation -- Supported ✔️ -- Not tested ⏳ -- Not supported ❌ +Install directly from PyPI: + +```bash +pip install StreamingCommunity +``` + +#### Creating a Run Script + +Create `run_streaming.py`: + +```python +from StreamingCommunity.run import main + +if __name__ == "__main__": + main() +``` + +Run the script: +```bash +python run_streaming.py +``` + +#### Updating via PyPI + +```bash +pip install --upgrade StreamingCommunity +``` + +### 2. Automatic Installation + +#### Supported Operating Systems 💿 | OS | Automatic Installation Support | -| :-------------- | :----------------------------: | +|:----------------|:------------------------------:| | Windows 10/11 | ✔️ | -| Windows 7 | ❌ | +| Windows 7 | ❌ | | Debian Linux | ✔️ | | Arch Linux | ✔️ | | CentOS Stream 9 | ✔️ | -| FreeBSD | ⏳ | +| FreeBSD | ⏳ | | MacOS | ✔️ | -| Termux | ❌ | +| Termux | ❌ | -### Installation ⚙️ +#### Installation Steps -Run the following command inside the main directory: - -#### On Windows: +##### On Windows: ```powershell .\win_install.bat ``` -#### On Linux/MacOS/BSD: +##### On Linux/MacOS/BSD: ```bash sudo chmod +x unix_install.sh && ./unix_install.sh ``` -`` +#### Usage -### Usage 📚 - -Run the script with the following command: - -#### On Windows: +##### On Windows: ```powershell python .\run.py @@ -80,43 +95,35 @@ or source .venv/bin/activate && python run.py && deactivate ``` -#### On Linux/MacOS/BSD: +##### On Linux/MacOS/BSD: ```bash ./run.py ``` -## Manual Installation +### 3. Manual Installation -`` +#### Requirements 📋 -### Requirement 📋 +Prerequisites: +* [Python](https://www.python.org/downloads/) > 3.8 +* [FFmpeg](https://www.gyan.dev/ffmpeg/builds/) -Make sure you have the following prerequisites installed on your system: +#### Install Python Dependencies -* [python](https://www.python.org/downloads/) > 3.8 -* [ffmpeg](https://www.gyan.dev/ffmpeg/builds/) -* [openssl](https://www.openssl.org) or [pycryptodome](https://pypi.org/project/pycryptodome/) - -### Installation ⚙️ - -Install the required Python libraries using the following command: - -``` +```bash pip install -r requirements.txt ``` -### Usage 📚 +#### Usage -Run the script with the following command: - -#### On Windows: +##### On Windows: ```powershell python run.py ``` -#### On Linux/MacOS: +##### On Linux/MacOS: ```bash python3 run.py @@ -129,13 +136,13 @@ Keep your script up to date with the latest features by running: #### On Windows: ```powershell -python update_version.py +python update.py ``` #### On Linux/MacOS: ```bash -python3 update_version.py +python3 update.py ``` ## Configuration ⚙️ @@ -150,7 +157,6 @@ The configuration file is divided into several main sections: { "root_path": "Video", "map_episode_name": "%(tv_name)_S%(season)E%(episode)_%(episode_name)", - "special_chars_to_remove": "!@#$%^&*()[]{}<>|`~'\";:,?=+\u00e2\u20ac\u00a6", "not_close": false, "show_trending": false } @@ -176,7 +182,7 @@ The configuration file is divided into several main sections: * `%(episode)` : Is the number of the episode * `%(episode_name)` : Is the name of the episode `

` -- `special_chars_to_remove`: Special characters to be removed from filenames + - `not_close`: If true, continues running after downloading - `show_trending`: Display trending content on startup @@ -282,46 +288,11 @@ forced-ita hin - Hindi pol - Polish tur - Turkish - `force_resolution`: Force specific resolution (-1 for best available, or specify 1080, 720, 360) - `get_only_link`: Return M3U8 playlist/index URL instead of downloading -## Docker 🐳 -You can run the script in a docker container, to build the image just run - -``` -docker build -t streaming-community-api . -``` - -and to run it use - -``` -docker run -it -p 8000:8000 streaming-community-api -``` - -By default the videos will be saved in `/app/Video` inside the container, if you want to to save them in your machine instead of the container just run - -``` -docker run -it -p 8000:8000 -v /path/to/download:/app/Video streaming-community-api -``` - -### Docker quick setup with Make - -Inside the Makefile (install `make`) are already configured two commands to build and run the container: - -``` -make build-container - -# set your download directory as ENV variable -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. - -### Website Status 🌐 - -- Working ✅ -- Not Working ❌ +## 🌐 Website Status | Website | Status | -| :----------------- | :----: | +|:-------------------|:------:| | 1337xx | ✅ | | Altadefinizione | ✅ | | AnimeUnity | ✅ | @@ -333,13 +304,20 @@ The `run-container` command mounts also the `config.json` file, so any change to | PirateBays | ✅ | | StreamingCommunity | ✅ | -## Tutorial 📖 +## 📖 Tutorials -[win](https://www.youtube.com/watch?v=mZGqK4wdN-k) -[linux](https://www.youtube.com/watch?v=0qUNXPE_mTg) +- [Windows Tutorial](https://www.youtube.com/watch?v=mZGqK4wdN-k) +- [Linux Tutorial](https://www.youtube.com/watch?v=0qUNXPE_mTg) -## To do 📝 +## 📝 To Do -- GUI -- Website api -- Add other site +- Create website API + +## 🤝 Contributing + +Contributions are welcome! Steps: +1. Fork the repository +2. Create feature branch (`git checkout -b feature/AmazingFeature`) +3. Commit changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to branch (`git push origin feature/AmazingFeature`) +5. Open Pull Request diff --git a/StreamingCommunity/Src/Api/Player/Helper/Vixcloud/js_parser.py b/StreamingCommunity/Src/Api/Player/Helper/Vixcloud/js_parser.py new file mode 100644 index 0000000..ca28d4a --- /dev/null +++ b/StreamingCommunity/Src/Api/Player/Helper/Vixcloud/js_parser.py @@ -0,0 +1,140 @@ +# 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) + + return result + \ No newline at end of file diff --git a/StreamingCommunity/Src/Api/Player/Helper/Vixcloud/util.py b/StreamingCommunity/Src/Api/Player/Helper/Vixcloud/util.py index 0f36c64..4c29ee1 100644 --- a/StreamingCommunity/Src/Api/Player/Helper/Vixcloud/util.py +++ b/StreamingCommunity/Src/Api/Player/Helper/Vixcloud/util.py @@ -111,6 +111,29 @@ class SeasonManager: return f"SeasonManager(num_seasons={len(self.seasons)})" +class Stream: + def __init__(self, name: str, url: str, active: bool): + self.name = name + self.url = url + self.active = active + + def __repr__(self): + return f"Stream(name={self.name!r}, url={self.url!r}, active={self.active!r})" + +class StreamsCollection: + def __init__(self, streams: list): + self.streams = [Stream(**stream) for stream in streams] + + def __repr__(self): + return f"StreamsCollection(streams={self.streams})" + + def add_stream(self, name: str, url: str, active: bool): + self.streams.append(Stream(name, url, active)) + + def get_streams(self): + return self.streams + + class WindowVideo: def __init__(self, data: Dict[str, Any]): self.data = data @@ -134,133 +157,10 @@ class WindowVideo: class WindowParameter: def __init__(self, data: Dict[str, Any]): self.data = data - self.token: str = data.get('token', '') - self.token360p: str = data.get('token360p', '') - self.token480p: str = data.get('token480p', '') - self.token720p: str = data.get('token720p', '') - self.token1080p: str = data.get('token1080p', '') - self.expires: str = data.get('expires', '') + 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}', token360p='{self.token360p}', token480p='{self.token480p}', token720p='{self.token720p}', token1080p='{self.token1080p}', expires='{self.expires}')" - - -class DynamicJSONConverter: - """ - Class for converting an input string into dynamic JSON. - """ - - def __init__(self, input_string: str): - """ - Initialize the converter with the input string. - - Parameters: - input_string (str): The input string to convert. - """ - self.input_string = input_string - self.json_data = {} - - def _parse_key_value(self, key: str, value: str): - """ - Parse a key-value pair. - - Parameters: - key (str): The key. - value (str): The value. - - Returns: - object: The parsed value. - """ - try: - value = value.strip() - - if value.startswith('{'): - return self._parse_json_object(value) - else: - return self._parse_non_json_value(value) - - except Exception as e: - logging.error(f"Error parsing key-value pair '{key}': {e}") - raise - - def _parse_json_object(self, obj_str: str): - """ - Parse a JSON object. - - Parameters: - obj_str (str): The string representation of the JSON object. - - Returns: - dict: The parsed JSON object. - """ - try: - # Use regular expression to find key-value pairs in the JSON object string - obj_dict = dict(re.findall(r'"([^"]*)"\s*:\s*("[^"]*"|[^,]*)', obj_str)) - - # Strip double quotes from values and return the parsed dictionary - return {k: v.strip('"') for k, v in obj_dict.items()} - - except Exception as e: - logging.error(f"Error parsing JSON object: {e}") - raise - - def _parse_non_json_value(self, value: str): - """ - Parse a non-JSON value. - - Parameters: - value (str): The value to parse. - - Returns: - object: The parsed value. - """ - try: - - # Remove extra quotes and convert to lowercase - value = value.replace('"', "").strip().lower() - - if value.endswith('\n}'): - value = value.replace('\n}', '') - - # Check if the value matches 'true' or 'false' using regular expressions - if re.match(r'\btrue\b', value, re.IGNORECASE): - return True - - elif re.match(r'\bfalse\b', value, re.IGNORECASE): - return False - - return value - - except Exception as e: - logging.error(f"Error parsing non-JSON value: {e}") - raise - - def convert_to_dynamic_json(self): - """ - Convert the input string into dynamic JSON. - - Returns: - str: The JSON representation of the result. - """ - try: - - # Replace invalid characters with valid JSON syntax - self.input_string = "{" + self.input_string.replace("'", '"').replace("=", ":").replace(";", ",").replace("}\n", "},\n") + "}" - - # Find all key-value matches in the input string using regular expression - matches = re.findall(r'(\w+)\s*:\s*({[^}]*}|[^,]+)', self.input_string) - - for match in matches: - key = match[0].strip() - value = match[1].strip() - - # Parse each key-value pair and add it to the json_data dictionary - self.json_data[key] = self._parse_key_value(key, value) - - # Convert the json_data dictionary to a formatted JSON string - return self.json_data - - except Exception as e: - logging.error(f"Error converting to dynamic JSON: {e}") - raise - + return (f"WindowParameter(token='{self.token}', expires='{self.expires}', url='{self.url}', data={self.data})") diff --git a/StreamingCommunity/Src/Api/Player/vixcloud.py b/StreamingCommunity/Src/Api/Player/vixcloud.py index c832022..73c290a 100644 --- a/StreamingCommunity/Src/Api/Player/vixcloud.py +++ b/StreamingCommunity/Src/Api/Player/vixcloud.py @@ -2,7 +2,7 @@ import sys import logging -from urllib.parse import urljoin, urlparse, parse_qs, urlencode, urlunparse +from urllib.parse import urlparse, urlencode, urlunparse # External libraries @@ -14,107 +14,43 @@ from bs4 import BeautifulSoup from StreamingCommunity.Src.Util.headers import get_headers from StreamingCommunity.Src.Util.console import console, Panel from StreamingCommunity.Src.Util._jsonConfig import config_manager -from .Helper.Vixcloud.util import Episode, EpisodeManager, Season, SeasonManager, WindowVideo, WindowParameter, DynamicJSONConverter +from .Helper.Vixcloud.util import WindowVideo, WindowParameter, StreamsCollection +from .Helper.Vixcloud.js_parser import JavaScriptParser # Variable max_timeout = config_manager.get_int("REQUESTS", "timeout") - class VideoSource: - def __init__(self, site_name: str): + def __init__(self, site_name: str, is_series: bool): """ - Initialize a VideoSource object. + Initialize video source for streaming site. + + Args: + site_name (str): Name of streaming site + is_series (bool): Flag for series or movie content """ self.headers = {'user-agent': get_headers()} - self.is_series = False self.base_name = site_name + self.domain = config_manager.get_dict('SITE', self.base_name)['domain'] + self.is_series = is_series - def setup(self, version: str = None, domain: str = None, media_id: int = None, series_name: str = None): + def setup(self, media_id: int): """ - Set up the class - - Parameters: - - version (str): The version to set. - - media_id (int): The media ID to set. - - series_name (str): The series name to set. + Configure media-specific context. + + Args: + media_id (int): Unique identifier for media item """ - self.version = version - self.domain = domain self.media_id = media_id - if series_name is not None: - self.is_series = True - self.series_name = series_name - self.obj_season_manager: SeasonManager = SeasonManager() - self.obj_episode_manager: EpisodeManager = EpisodeManager() - - def collect_info_seasons(self) -> None: + def get_iframe(self, episode_id: int) -> None: """ - Collect information about seasons. - """ - - self.headers = { - 'user-agent': get_headers(), - 'x-inertia': 'true', - 'x-inertia-version': self.version, - } - - try: - - response = httpx.get( - url=f"https://{self.base_name}.{self.domain}/titles/{self.media_id}-{self.series_name}", - headers=self.headers, - timeout=max_timeout - ) - response.raise_for_status() - - # Extract JSON response if available - json_response = response.json().get('props', {}).get('title', {}).get('seasons', []) - - # Iterate over JSON data and add titles to the manager - for dict_season in json_response: - self.obj_season_manager.add_season(dict_season) - - except Exception as e: - logging.error(f"Error collecting season info: {e}") - raise - - def collect_title_season(self, number_season: int) -> None: - """ - Collect information about a specific season. - - Parameters: - - number_season (int): The season number. - """ - try: - - # Make a request to collect information about a specific season - response = httpx.get( - url=f'https://{self.base_name}.{self.domain}/titles/{self.media_id}-{self.series_name}/stagione-{number_season}', - headers=self.headers, - timeout=max_timeout - ) - response.raise_for_status() - - # Extract JSON response if available - json_response = response.json().get('props', {}).get('loadedSeason', {}).get('episodes', []) - - # Iterate over JSON data and add episodes to the manager - for dict_episode in json_response: - self.obj_episode_manager.add_episode(dict_episode) - - except Exception as e: - logging.error(f"Error collecting title season info: {e}") - raise - - def get_iframe(self, episode_id: int = None) -> None: - """ - Get iframe source. - - Parameters: - - episode_id (int): The episode ID, present only for series + Retrieve iframe source for specified episode. + + Args: + episode_id (int): Unique identifier for episode """ params = {} @@ -144,19 +80,18 @@ class VideoSource: def parse_script(self, script_text: str) -> None: """ - Parse script text. - - Parameters: - - script_text (str): The script text to parse. + Convert raw script to structured video metadata. + + Args: + script_text (str): Raw JavaScript/HTML script content """ try: + converter = JavaScriptParser.parse(js_string=str(script_text)) - converter = DynamicJSONConverter(script_text) - result = converter.convert_to_dynamic_json() - - # Create window video and parameter objects - self.window_video = WindowVideo(result['video']) - self.window_parameter = WindowParameter(result['masterPlaylist']) + # Create window video, streams and parameter objects + self.window_video = WindowVideo(converter.get('video')) + self.window_streams = StreamsCollection(converter.get('streams')) + self.window_parameter = WindowParameter(converter.get('masterPlaylist')) except Exception as e: logging.error(f"Error parsing script: {e}") @@ -164,11 +99,14 @@ class VideoSource: def get_content(self) -> None: """ - Get content. + Fetch and process video content from iframe source. + + Workflow: + - Validate iframe source + - Retrieve content + - Parse embedded script """ try: - - # Check if iframe source is available if self.iframe_src is not None: # Make a request to get content @@ -198,134 +136,52 @@ class VideoSource: def get_playlist(self) -> str: """ - Get playlist. - - Returns: - - str: The playlist URL, or None if there's an error. - """ - - iframe_url = self.iframe_src - - # Create base uri for playlist - base_url = f'https://vixcloud.co/playlist/{self.window_video.id}' - query = urlencode(list(self.window_parameter.data.items())) - master_playlist_url = urljoin(base_url, '?' + query) - - # Parse the current query string and the master playlist URL query string - current_params = parse_qs(iframe_url[1:]) - m = urlparse(master_playlist_url) - master_params = parse_qs(m.query) - - # Create the final parameters dictionary with token and expires from the master playlist - final_params = { - "token": master_params.get("token", [""])[0], - "expires": master_params.get("expires", [""])[0] - } - - # Add conditional parameters - if "b" in current_params: - final_params["b"] = "1" - if "canPlayFHD" in current_params: - final_params["h"] = "1" - - # Construct the new query string and final URL - new_query = urlencode(final_params) # Encode final_params into a query string - new_url = m._replace(query=new_query) # Replace the old query string with the new one - final_url = urlunparse(new_url) # Construct the final URL from the modified parts + Generate authenticated playlist URL. - return final_url + Returns: + str: Fully constructed playlist URL with authentication parameters + """ + params = {} + + if self.window_video.quality == 1080: + params['h'] = 1 + + if "b=1" in self.window_parameter.url: + params['b'] = 1 + + params.update({ + "token": self.window_parameter.token, + "expires": self.window_parameter.expires + }) + + query_string = urlencode(params) + return urlunparse(urlparse(self.window_parameter.url)._replace(query=query_string)) -class AnimeVideoSource(VideoSource): +class VideoSourceAnime(VideoSource): def __init__(self, site_name: str): """ - Initialize a VideoSource object. + Initialize anime-specific video source. + + Args: + site_name (str): Name of anime streaming site + + Extends base VideoSource with anime-specific initialization """ self.headers = {'user-agent': get_headers()} - self.is_series = False self.base_name = site_name self.domain = config_manager.get_dict('SITE', self.base_name)['domain'] + self.src_mp4 = None - def setup(self, media_id: int = None, series_name: str = None): - """ - Set up the class - - Parameters: - - media_id (int): The media ID to set. - - series_name (str): The series name to set. - """ - self.media_id = media_id - - if series_name is not None: - self.is_series = True - self.series_name = series_name - self.obj_episode_manager: EpisodeManager = EpisodeManager() - - def get_count_episodes(self): - """ - Fetches the total count of episodes available for the anime. - - Returns: - - int or None: Total count of episodes if successful, otherwise None. - """ - try: - - response = httpx.get( - url=f"https://www.{self.base_name}.{self.domain}/info_api/{self.media_id}/", - headers=self.headers, - timeout=max_timeout - ) - response.raise_for_status() - - # Parse JSON response and return episode count - return response.json()["episodes_count"] - - except Exception as e: - logging.error(f"(EpisodeDownloader) Error fetching episode count: {e}") - return None - - def get_info_episode(self, index_ep: int) -> Episode: - """ - Fetches information about a specific episode. - - Parameters: - - index_ep (int): Index of the episode. - - Returns: - - obj Episode or None: Information about the episode if successful, otherwise None. - """ - try: - - params = { - "start_range": index_ep, - "end_range": index_ep + 1 - } - - response = httpx.get( - url=f"https://www.{self.base_name}.{self.domain}/info_api/{self.media_id}/{index_ep}", - headers=self.headers, - params=params, - timeout=max_timeout - ) - response.raise_for_status() - - # Return information about the episode - json_data = response.json()["episodes"][-1] - return Episode(json_data) - - except Exception as e: - logging.error(f"(EpisodeDownloader) Error fetching episode information: {e}") - return None - def get_embed(self, episode_id: int): """ - Fetches the script text for a given episode ID. + Retrieve embed URL and extract video source. + + Args: + episode_id (int): Unique identifier for episode - Parameters: - - episode_id (int): ID of the episode. - Returns: - - str or None: Script successful, otherwise None. + str: Parsed script content """ try: @@ -352,5 +208,5 @@ class AnimeVideoSource(VideoSource): return script except Exception as e: - logging.error(f"(EpisodeDownloader) Error fetching embed URL: {e}") + logging.error(f"Error fetching embed URL: {e}") return None diff --git a/StreamingCommunity/Src/Api/Site/animeunity/__init__.py b/StreamingCommunity/Src/Api/Site/animeunity/__init__.py index c63b867..3e45978 100644 --- a/StreamingCommunity/Src/Api/Site/animeunity/__init__.py +++ b/StreamingCommunity/Src/Api/Site/animeunity/__init__.py @@ -9,7 +9,7 @@ from StreamingCommunity.Src.Util.console import console, msg # Logic class from .site import title_search, run_get_select_title, media_search_manager -from .anime import download_film, download_series +from .film_serie import download_film, download_series # Variable @@ -34,10 +34,10 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): if len_database > 0: - # Select title from list + # Select title from list (type: TV \ Movie \ OVA) select_title = run_get_select_title() - if select_title.type == 'Movie': + if select_title.type == 'Movie' or select_title.type == 'OVA': download_film(select_title) else: diff --git a/StreamingCommunity/Src/Api/Site/animeunity/film_serie.py b/StreamingCommunity/Src/Api/Site/animeunity/film_serie.py new file mode 100644 index 0000000..4333f2e --- /dev/null +++ b/StreamingCommunity/Src/Api/Site/animeunity/film_serie.py @@ -0,0 +1,131 @@ +# 11.03.24 + +import os +import sys +import logging + + +# Internal utilities +from StreamingCommunity.Src.Util.console import console, msg +from StreamingCommunity.Src.Util.os import os_manager +from StreamingCommunity.Src.Util.message import start_message +from StreamingCommunity.Src.Lib.Downloader import MP4_downloader + + +# Logic class +from .util.ScrapeSerie import ScrapeSerieAnime +from StreamingCommunity.Src.Api.Template.Util import manage_selection +from StreamingCommunity.Src.Api.Template.Class.SearchType import MediaItem + + +# Player +from StreamingCommunity.Src.Api.Player.vixcloud import VideoSourceAnime + + +# Variable +from .costant import ROOT_PATH, SITE_NAME, SERIES_FOLDER, MOVIE_FOLDER +scrape_serie = ScrapeSerieAnime(SITE_NAME) +video_source = VideoSourceAnime(SITE_NAME) + + +def download_episode(index_select: int): + """ + Downloads the selected episode. + + Parameters: + - index_select (int): Index of the episode to download. + """ + + # Get information about the selected episode + obj_episode = scrape_serie.get_info_episode(index_select) + + if obj_episode is not None: + + start_message() + console.print(f"[yellow]Download: [red]EP_{obj_episode.number} \n") + + # Collect mp4 url + video_source.get_embed(obj_episode.id) + + # Get the js script from the episode + #js_script = video_source.get_embed(obj_episode.id) + + # Parse parameter in embed text + #video_source.parse_script(js_script) + + # Create output path + title_name = f"{obj_episode.number}.mp4" + + if scrape_serie.is_series: + mp4_path = os_manager.get_sanitize_path( + os.path.join(ROOT_PATH, SITE_NAME, SERIES_FOLDER, scrape_serie.series_name) + ) + else: + mp4_path = os_manager.get_sanitize_path( + os.path.join(ROOT_PATH, SITE_NAME, MOVIE_FOLDER, scrape_serie.series_name) + ) + + # Create output folder + os_manager.create_path(mp4_path) + + # Start downloading + r_proc = MP4_downloader( + url = str(video_source.src_mp4).strip(), + path = os.path.join(mp4_path, title_name) + ) + + if r_proc != None: + console.print("[green]Result: ") + console.print(r_proc) + + else: + logging.error(f"Skip index: {index_select} cant find info with api.") + + +def download_series(select_title: MediaItem): + """ + Function to download episodes of a TV series. + + Parameters: + - tv_id (int): The ID of the TV series. + - tv_name (str): The name of the TV series. + """ + + # Set up video source + scrape_serie.setup(None, select_title.id, select_title.slug) + + # Get the count of episodes for the TV series + episoded_count = scrape_serie.get_count_episodes() + console.print(f"[cyan]Episodes find: [red]{episoded_count}") + + # Prompt user to select an episode index + last_command = msg.ask("\n[cyan]Insert media [red]index [yellow]or [red](*) [cyan]to download all media [yellow]or [red][1-2] [cyan]or [red][3-*] [cyan]for a range of media") + + # Manage user selection + list_episode_select = manage_selection(last_command, episoded_count) + + # Download selected episodes + if len(list_episode_select) == 1 and last_command != "*": + download_episode(list_episode_select[0]-1) + + # Download all other episodes selecter + else: + for i_episode in list_episode_select: + download_episode(i_episode-1) + + +def download_film(select_title: MediaItem): + """ + Function to download a film. + + Parameters: + - id_film (int): The ID of the film. + - title_name (str): The title of the film. + """ + + # Set up video source + scrape_serie.setup(None, select_title.id, select_title.slug) + scrape_serie.is_series = False + + # Start download + download_episode(0) diff --git a/StreamingCommunity/Src/Api/Site/animeunity/util/ScrapeSerie.py b/StreamingCommunity/Src/Api/Site/animeunity/util/ScrapeSerie.py new file mode 100644 index 0000000..c26f083 --- /dev/null +++ b/StreamingCommunity/Src/Api/Site/animeunity/util/ScrapeSerie.py @@ -0,0 +1,97 @@ +# 01.03.24 + +import logging + + +# External libraries +import httpx + + +# Internal utilities +from StreamingCommunity.Src.Util.headers import get_headers +from StreamingCommunity.Src.Util._jsonConfig import config_manager +from StreamingCommunity.Src.Api.Player.Helper.Vixcloud.util import EpisodeManager, Episode + + +# Variable +max_timeout = config_manager.get_int("REQUESTS", "timeout") + + + +class ScrapeSerieAnime(): + def __init__(self, site_name: str): + """ + Initialize the media scraper for a specific website. + + Args: + site_name (str): Name of the streaming site to scrape + """ + self.is_series = False + self.headers = {'user-agent': get_headers()} + self.base_name = site_name + self.domain = config_manager.get_dict('SITE', self.base_name)['domain'] + + def setup(self, version: str = None, media_id: int = None, series_name: str = None): + self.version = version + self.media_id = media_id + + if series_name is not None: + self.is_series = True + self.series_name = series_name + self.obj_episode_manager: EpisodeManager = EpisodeManager() + + def get_count_episodes(self): + """ + Retrieve total number of episodes for the selected media. + + Returns: + int: Total episode count + """ + try: + + response = httpx.get( + url=f"https://www.{self.base_name}.{self.domain}/info_api/{self.media_id}/", + headers=self.headers, + timeout=max_timeout + ) + response.raise_for_status() + + # Parse JSON response and return episode count + return response.json()["episodes_count"] + + except Exception as e: + logging.error(f"Error fetching episode count: {e}") + return None + + def get_info_episode(self, index_ep: int) -> Episode: + """ + Fetch detailed information for a specific episode. + + Args: + index_ep (int): Zero-based index of the target episode + + Returns: + Episode: Detailed episode information + """ + try: + + params = { + "start_range": index_ep, + "end_range": index_ep + 1 + } + + response = httpx.get( + url=f"https://www.{self.base_name}.{self.domain}/info_api/{self.media_id}/{index_ep}", + headers=self.headers, + params=params, + timeout=max_timeout + ) + response.raise_for_status() + + # Return information about the episode + json_data = response.json()["episodes"][-1] + return Episode(json_data) + + except Exception as e: + logging.error(f"Error fetching episode information: {e}") + return None diff --git a/StreamingCommunity/Src/Api/Site/ddlstreamitaly/series.py b/StreamingCommunity/Src/Api/Site/ddlstreamitaly/series.py index ee20614..7eda9ea 100644 --- a/StreamingCommunity/Src/Api/Site/ddlstreamitaly/series.py +++ b/StreamingCommunity/Src/Api/Site/ddlstreamitaly/series.py @@ -19,7 +19,7 @@ from StreamingCommunity.Src.Api.Template.Util import manage_selection, map_episo # Player -from .Player.ScrapeSerie import GetSerieInfo +from .util.ScrapeSerie import GetSerieInfo from StreamingCommunity.Src.Api.Player.ddl import VideoSource @@ -92,7 +92,7 @@ def download_thread(dict_serie: MediaItem): episodes_count = len(list_dict_episode) # Display episodes list and manage user selection - last_command = display_episodes_list() + last_command = display_episodes_list(scape_info_serie.list_episodes) list_episode_select = manage_selection(last_command, episodes_count) try: diff --git a/StreamingCommunity/Src/Api/Site/ddlstreamitaly/util/ScrapeSerie.py b/StreamingCommunity/Src/Api/Site/ddlstreamitaly/util/ScrapeSerie.py new file mode 100644 index 0000000..7c7abfa --- /dev/null +++ b/StreamingCommunity/Src/Api/Site/ddlstreamitaly/util/ScrapeSerie.py @@ -0,0 +1,83 @@ +# 13.06.24 + +import sys +import logging +from typing import List, Dict + + +# External libraries +import httpx +from bs4 import BeautifulSoup + + +# Internal utilities +from StreamingCommunity.Src.Util.headers import get_headers + + +# Logic class +from StreamingCommunity.Src.Api.Template.Class.SearchType import MediaItem + + +# Variable +from ..costant import COOKIE + + +class GetSerieInfo: + def __init__(self, dict_serie: MediaItem) -> None: + """ + Initializes the GetSerieInfo object with default values. + + Parameters: + - dict_serie (MediaItem): Dictionary containing series information (optional). + """ + self.headers = {'user-agent': get_headers()} + self.cookies = COOKIE + self.url = dict_serie.url + self.tv_name = None + self.list_episodes = None + + def get_episode_number(self) -> List[Dict[str, str]]: + """ + Retrieves the number of episodes for a specific season. + + Parameters: + n_season (int): The season number. + + Returns: + List[Dict[str, str]]: List of dictionaries containing episode information. + """ + + try: + response = httpx.get(f"{self.url}?area=online", cookies=self.cookies, headers=self.headers, timeout=10) + response.raise_for_status() + + except Exception as e: + logging.error(f"Insert value for [ips4_device_key, ips4_member_id, ips4_login_key] in config.json file SITE \\ ddlstreamitaly \\ cookie. Use browser debug and cookie request with a valid account, filter by DOC. Error: {e}") + sys.exit(0) + + # Parse HTML content of the page + soup = BeautifulSoup(response.text, "html.parser") + + # Get tv name + self.tv_name = soup.find("span", class_= "ipsType_break").get_text(strip=True) + + # Find the container of episodes for the specified season + table_content = soup.find('div', class_='ipsMargin_bottom:half') + list_dict_episode = [] + + for episode_div in table_content.find_all('a', href=True): + + # Get text of episode + part_name = episode_div.get_text(strip=True) + + if part_name: + obj_episode = { + 'name': part_name, + 'url': episode_div['href'] + } + + list_dict_episode.append(obj_episode) + + self.list_episodes = list_dict_episode + return list_dict_episode + \ No newline at end of file diff --git a/StreamingCommunity/Src/Api/Site/guardaserie/series.py b/StreamingCommunity/Src/Api/Site/guardaserie/series.py index 3304692..d385401 100644 --- a/StreamingCommunity/Src/Api/Site/guardaserie/series.py +++ b/StreamingCommunity/Src/Api/Site/guardaserie/series.py @@ -19,7 +19,7 @@ from StreamingCommunity.Src.Api.Template.Class.SearchType import MediaItem # Player -from .Player.ScrapeSerie import GetSerieInfo +from .util.ScrapeSerie import GetSerieInfo from StreamingCommunity.Src.Api.Player.supervideo import VideoSource diff --git a/StreamingCommunity/Src/Api/Site/guardaserie/util/ScrapeSerie.py b/StreamingCommunity/Src/Api/Site/guardaserie/util/ScrapeSerie.py new file mode 100644 index 0000000..83f3140 --- /dev/null +++ b/StreamingCommunity/Src/Api/Site/guardaserie/util/ScrapeSerie.py @@ -0,0 +1,110 @@ +# 13.06.24 + +import logging +from typing import List, Dict + + +# External libraries +import httpx +from bs4 import BeautifulSoup + + +# Internal utilities +from StreamingCommunity.Src.Util.headers import get_headers + + +# Logic class +from StreamingCommunity.Src.Api.Template .Class.SearchType import MediaItem + + +class GetSerieInfo: + def __init__(self, dict_serie: MediaItem) -> None: + """ + Initializes the GetSerieInfo object with default values. + + Parameters: + dict_serie (MediaItem): Dictionary containing series information (optional). + """ + self.headers = {'user-agent': get_headers()} + self.url = dict_serie.url + self.tv_name = None + self.list_episodes = None + + def get_seasons_number(self) -> int: + """ + Retrieves the number of seasons of a TV series. + + Returns: + int: Number of seasons of the TV series. + """ + try: + + # Make an HTTP request to the series URL + response = httpx.get(self.url, headers=self.headers, timeout=15) + response.raise_for_status() + + # Parse HTML content of the page + soup = BeautifulSoup(response.text, "html.parser") + + # Find the container of seasons + table_content = soup.find('div', class_="tt_season") + + # Count the number of seasons + seasons_number = len(table_content.find_all("li")) + + # Extract the name of the series + self.tv_name = soup.find("h1", class_="front_title").get_text(strip=True) + + return seasons_number + + except Exception as e: + logging.error(f"Error parsing HTML page: {e}") + + return -999 + + def get_episode_number(self, n_season: int) -> List[Dict[str, str]]: + """ + Retrieves the number of episodes for a specific season. + + Parameters: + n_season (int): The season number. + + Returns: + List[Dict[str, str]]: List of dictionaries containing episode information. + """ + try: + + # Make an HTTP request to the series URL + response = httpx.get(self.url, headers=self.headers, timeout=15) + response.raise_for_status() + + # Parse HTML content of the page + soup = BeautifulSoup(response.text, "html.parser") + + # Find the container of episodes for the specified season + table_content = soup.find('div', class_="tab-pane", id=f"season-{n_season}") + + # Extract episode information + episode_content = table_content.find_all("li") + list_dict_episode = [] + + for episode_div in episode_content: + index = episode_div.find("a").get("data-num") + link = episode_div.find("a").get("data-link") + name = episode_div.find("a").get("data-title") + + obj_episode = { + 'number': index, + 'name': name, + 'url': link + } + + list_dict_episode.append(obj_episode) + + self.list_episodes = list_dict_episode + return list_dict_episode + + except Exception as e: + logging.error(f"Error parsing HTML page: {e}") + + return [] diff --git a/StreamingCommunity/Src/Api/Site/streamingcommunity/__init__.py b/StreamingCommunity/Src/Api/Site/streamingcommunity/__init__.py index d523cf8..e161f8a 100644 --- a/StreamingCommunity/Src/Api/Site/streamingcommunity/__init__.py +++ b/StreamingCommunity/Src/Api/Site/streamingcommunity/__init__.py @@ -43,10 +43,10 @@ def search(string_to_search: str = None, get_onylDatabase: bool = False): select_title = run_get_select_title() if select_title.type == 'tv': - download_series(select_title, domain, site_version) + download_series(select_title, site_version) else: - download_film(select_title, domain, site_version) + download_film(select_title) else: console.print(f"\n[red]Nothing matching was found for[white]: [purple]{string_to_search}") diff --git a/StreamingCommunity/Src/Api/Site/streamingcommunity/film.py b/StreamingCommunity/Src/Api/Site/streamingcommunity/film.py index f1c545a..fb13f51 100644 --- a/StreamingCommunity/Src/Api/Site/streamingcommunity/film.py +++ b/StreamingCommunity/Src/Api/Site/streamingcommunity/film.py @@ -23,10 +23,10 @@ from StreamingCommunity.Src.Api.Player.vixcloud import VideoSource # Variable from .costant import ROOT_PATH, SITE_NAME, MOVIE_FOLDER -video_source = VideoSource(site_name=SITE_NAME) +video_source = VideoSource(SITE_NAME, False) -def download_film(select_title: MediaItem, domain: str, version: str): +def download_film(select_title: MediaItem): """ Downloads a film using the provided film ID, title name, and domain. @@ -40,10 +40,10 @@ def download_film(select_title: MediaItem, domain: str, version: str): console.print(f"[yellow]Download: [red]{select_title.slug} \n") # Set domain and media ID for the video source - video_source.setup(version, domain, select_title.id) + video_source.setup(select_title.id) # Retrieve scws and if available master playlist - video_source.get_iframe() + video_source.get_iframe(select_title.id) video_source.get_content() master_playlist = video_source.get_playlist() diff --git a/StreamingCommunity/Src/Api/Site/streamingcommunity/series.py b/StreamingCommunity/Src/Api/Site/streamingcommunity/series.py index 46644e5..8095c7b 100644 --- a/StreamingCommunity/Src/Api/Site/streamingcommunity/series.py +++ b/StreamingCommunity/Src/Api/Site/streamingcommunity/series.py @@ -14,6 +14,7 @@ from StreamingCommunity.Src.Lib.Downloader import HLS_Downloader # Logic class +from .util.ScrapeSerie import ScrapeSerie from StreamingCommunity.Src.Api.Template.Util import manage_selection, map_episode_title, validate_selection, validate_episode_selection, execute_search from StreamingCommunity.Src.Api.Template.Class.SearchType import MediaItem @@ -24,7 +25,8 @@ from StreamingCommunity.Src.Api.Player.vixcloud import VideoSource # Variable from .costant import ROOT_PATH, SITE_NAME, SERIES_FOLDER -video_source = VideoSource(site_name=SITE_NAME) +scrape_serie = ScrapeSerie(SITE_NAME) +video_source = VideoSource(SITE_NAME, True) table_show_manager = TVShowManager() @@ -42,7 +44,7 @@ def download_video(tv_name: str, index_season_selected: int, index_episode_selec start_message() # Get info about episode - obj_episode = video_source.obj_episode_manager.episodes[index_episode_selected - 1] + obj_episode = scrape_serie.obj_episode_manager.episodes[index_episode_selected - 1] console.print(f"[yellow]Download: [red]{index_season_selected}:{index_episode_selected} {obj_episode.name}") print() @@ -84,13 +86,13 @@ def download_episode(tv_name: str, index_season_selected: int, download_all: boo """ # Clean memory of all episodes and get the number of the season - video_source.obj_episode_manager.clear() - season_number = video_source.obj_season_manager.seasons[index_season_selected - 1].number + scrape_serie.obj_episode_manager.clear() + season_number = scrape_serie.obj_season_manager.seasons[index_season_selected - 1].number # Start message and collect information about episodes start_message() - video_source.collect_title_season(season_number) - episodes_count = video_source.obj_episode_manager.get_length() + scrape_serie.collect_title_season(season_number) + episodes_count = scrape_serie.obj_episode_manager.get_length() if download_all: @@ -115,13 +117,12 @@ def download_episode(tv_name: str, index_season_selected: int, download_all: boo for i_episode in list_episode_select: download_video(tv_name, index_season_selected, i_episode) - -def download_series(select_title: MediaItem, domain: str, version: str) -> None: +def download_series(select_season: MediaItem, version: str) -> None: """ Download episodes of a TV series based on user selection. Parameters: - - select_title (MediaItem): Selected media item (TV series). + - select_season (MediaItem): Selected media item (TV series). - domain (str): Domain from which to download. - version (str): Version of the site. """ @@ -130,11 +131,12 @@ def download_series(select_title: MediaItem, domain: str, version: str) -> None: start_message() # Setup video source - video_source.setup(version, domain, select_title.id, select_title.slug) + scrape_serie.setup(version, select_season.id, select_season.slug) + video_source.setup(select_season.id) # Collect information about seasons - video_source.collect_info_seasons() - seasons_count = video_source.obj_season_manager.get_length() + scrape_serie.collect_info_seasons() + seasons_count = scrape_serie.obj_season_manager.get_length() # Prompt user for season selection and download episodes console.print(f"\n[green]Seasons found: [red]{seasons_count}") @@ -157,11 +159,11 @@ def download_series(select_title: MediaItem, domain: str, version: str) -> None: if len(list_season_select) > 1 or index_season_selected == "*": # Download all episodes if multiple seasons are selected or if '*' is used - download_episode(select_title.slug, i_season, download_all=True) + download_episode(select_season.slug, i_season, download_all=True) else: # Otherwise, let the user select specific episodes for the single season - download_episode(select_title.slug, i_season, download_all=False) + download_episode(select_season.slug, i_season, download_all=False) def display_episodes_list() -> str: @@ -184,7 +186,7 @@ def display_episodes_list() -> str: table_show_manager.add_column(column_info) # Populate the table with episodes information - for i, media in enumerate(video_source.obj_episode_manager.episodes): + for i, media in enumerate(scrape_serie.obj_episode_manager.episodes): table_show_manager.add_tv_show({ 'Index': str(media.number), 'Name': media.name, diff --git a/StreamingCommunity/Src/Api/Site/streamingcommunity/util/ScrapeSerie.py b/StreamingCommunity/Src/Api/Site/streamingcommunity/util/ScrapeSerie.py new file mode 100644 index 0000000..cd96149 --- /dev/null +++ b/StreamingCommunity/Src/Api/Site/streamingcommunity/util/ScrapeSerie.py @@ -0,0 +1,113 @@ +# 01.03.24 + +import logging + + +# External libraries +import httpx + + +# Internal utilities +from StreamingCommunity.Src.Util.headers import get_headers +from StreamingCommunity.Src.Util._jsonConfig import config_manager +from StreamingCommunity.Src.Api.Player.Helper.Vixcloud.util import SeasonManager, EpisodeManager + + +# Variable +max_timeout = config_manager.get_int("REQUESTS", "timeout") + + +class ScrapeSerie: + def __init__(self, site_name: str): + """ + Initialize the ScrapeSerie class for scraping TV series information. + + Args: + site_name (str): Name of the streaming site to scrape from + """ + self.is_series = False + self.headers = {'user-agent': get_headers()} + self.base_name = site_name + self.domain = config_manager.get_dict('SITE', self.base_name)['domain'] + + def setup(self, version: str = None, media_id: int = None, series_name: str = None): + """ + Set up the scraper with specific media details. + + Args: + version (str, optional): Site version for request headers + media_id (int, optional): Unique identifier for the media + series_name (str, optional): Name of the TV series + """ + self.version = version + self.media_id = media_id + + # If series name is provided, initialize series-specific managers + if series_name is not None: + self.is_series = True + self.series_name = series_name + self.obj_season_manager: SeasonManager = SeasonManager() + self.obj_episode_manager: EpisodeManager = EpisodeManager() + + def collect_info_seasons(self) -> None: + """ + Retrieve season information for a TV series from the streaming site. + + Raises: + Exception: If there's an error fetching season information + """ + self.headers = { + 'user-agent': get_headers(), + 'x-inertia': 'true', + 'x-inertia-version': self.version, + } + + try: + + response = httpx.get( + url=f"https://{self.base_name}.{self.domain}/titles/{self.media_id}-{self.series_name}", + headers=self.headers, + timeout=max_timeout + ) + response.raise_for_status() + + # Extract seasons from JSON response + json_response = response.json().get('props', {}).get('title', {}).get('seasons', []) + + # Add each season to the season manager + for dict_season in json_response: + self.obj_season_manager.add_season(dict_season) + + except Exception as e: + logging.error(f"Error collecting season info: {e}") + raise + + def collect_title_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: + + response = httpx.get( + url=f'https://{self.base_name}.{self.domain}/titles/{self.media_id}-{self.series_name}/stagione-{number_season}', + headers=self.headers, + timeout=max_timeout + ) + response.raise_for_status() + + # Extract episodes from JSON response + json_response = response.json().get('props', {}).get('loadedSeason', {}).get('episodes', []) + + # Add each episode to the episode manager + for dict_episode in json_response: + self.obj_episode_manager.add_episode(dict_episode) + + except Exception as e: + logging.error(f"Error collecting title season info: {e}") + raise diff --git a/StreamingCommunity/Src/Api/Template/Util/manage_ep.py b/StreamingCommunity/Src/Api/Template/Util/manage_ep.py index d2916d2..22b0190 100644 --- a/StreamingCommunity/Src/Api/Template/Util/manage_ep.py +++ b/StreamingCommunity/Src/Api/Template/Util/manage_ep.py @@ -84,10 +84,22 @@ def map_episode_title(tv_name: str, number_season: int, episode_number: int, epi str: The mapped episode title. """ map_episode_temp = MAP_EPISODE - map_episode_temp = map_episode_temp.replace("%(tv_name)", os_manager.get_sanitize_file(tv_name)) - map_episode_temp = map_episode_temp.replace("%(season)", dynamic_format_number(number_season)) - map_episode_temp = map_episode_temp.replace("%(episode)", dynamic_format_number(episode_number)) - map_episode_temp = map_episode_temp.replace("%(episode_name)", os_manager.get_sanitize_file(episode_name)) + + if tv_name != None: + map_episode_temp = map_episode_temp.replace("%(tv_name)", os_manager.get_sanitize_file(tv_name)) + + if number_season != None: + map_episode_temp = map_episode_temp.replace("%(season)", dynamic_format_number(number_season)) + else: + map_episode_temp = map_episode_temp.replace("%(season)", dynamic_format_number(0)) + + if episode_number != None: + map_episode_temp = map_episode_temp.replace("%(episode)", dynamic_format_number(episode_number)) + else: + map_episode_temp = map_episode_temp.replace("%(episode)", dynamic_format_number(0)) + + if episode_name != None: + map_episode_temp = map_episode_temp.replace("%(episode_name)", os_manager.get_sanitize_file(episode_name)) logging.info(f"Map episode string return: {map_episode_temp}") return map_episode_temp diff --git a/StreamingCommunity/Src/Lib/Downloader/HLS/downloader.py b/StreamingCommunity/Src/Lib/Downloader/HLS/downloader.py index 7dec882..be8d4ea 100644 --- a/StreamingCommunity/Src/Lib/Downloader/HLS/downloader.py +++ b/StreamingCommunity/Src/Lib/Downloader/HLS/downloader.py @@ -786,7 +786,7 @@ class HLS_Downloader: else: console.log("[red]Error: URL passed to M3U8_Parser is an index playlist; expected a master playlist. Crucimorfo strikes again!") else: - console.log("[red]Error: m3u8_playlist failed request") + console.log(f"[red]Error: m3u8_playlist failed request for: {self.m3u8_playlist}") else: console.log("[red]Error: m3u8_playlist is None") diff --git a/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py b/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py index b9cf9b6..899734b 100644 --- a/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py +++ b/StreamingCommunity/Src/Lib/Downloader/HLS/segments.py @@ -7,9 +7,10 @@ import queue import logging import binascii import threading +import signal from queue import PriorityQueue from urllib.parse import urljoin, urlparse -from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import ThreadPoolExecutor, as_completed # External libraries @@ -81,6 +82,10 @@ class M3U8_Segments: self.stop_event = threading.Event() self.downloaded_segments = set() + # Stopping + self.interrupt_flag = threading.Event() + self.download_interrupted = False + def __get_key__(self, m3u8_parser: M3U8_Parser) -> bytes: """ Retrieves the encryption key from the M3U8 playlist. @@ -197,6 +202,19 @@ class M3U8_Segments: # Parser data of content of index pass in input to class self.parse_data(self.url) + + def setup_interrupt_handler(self): + """ + Set up a signal handler for graceful interruption. + """ + def interrupt_handler(signum, frame): + if not self.interrupt_flag.is_set(): + console.log("\n[red] Stopping download gracefully...") + self.interrupt_flag.set() + self.download_interrupted = True + self.stop_event.set() + + signal.signal(signal.SIGINT, interrupt_handler) def make_requests_stream(self, ts_url: str, index: int, progress_bar: tqdm, retries: int = 3, backoff_factor: float = 1.5) -> None: """ @@ -209,10 +227,16 @@ class M3U8_Segments: - retries (int): The number of times to retry on failure (default is 3). - backoff_factor (float): The backoff factor for exponential backoff (default is 1.5 seconds). """ + if self.interrupt_flag.is_set(): + return + need_verify = REQUEST_VERIFY min_segment_size = 100 # Minimum acceptable size for a TS segment in bytes for attempt in range(retries): + if self.interrupt_flag.is_set(): + return + try: start_time = time.time() @@ -317,6 +341,10 @@ class M3U8_Segments: segments_written = set() while not self.stop_event.is_set() or not self.queue.empty(): + + if self.interrupt_flag.is_set(): + break + try: index, segment_content = self.queue.get(timeout=1) @@ -365,6 +393,7 @@ class M3U8_Segments: Parameters: - add_desc (str): Additional description for the progress bar. """ + self.setup_interrupt_handler() # Get config site from prev stack frames = get_call_stack() @@ -420,45 +449,74 @@ class M3U8_Segments: mininterval=0.05 ) - # Start writer thread - writer_thread = threading.Thread(target=self.write_segments_to_file) - writer_thread.daemon = True - writer_thread.start() + try: - # Configure workers and delay - max_workers = len(self.valid_proxy) if THERE_IS_PROXY_LIST else TQDM_MAX_WORKER - delay = max(PROXY_START_MIN, min(PROXY_START_MAX, 1 / (len(self.valid_proxy) + 1))) if THERE_IS_PROXY_LIST else TQDM_DELAY_WORKER + # Start writer thread + writer_thread = threading.Thread(target=self.write_segments_to_file) + writer_thread.daemon = True + writer_thread.start() - # Download segments with completion verification - with ThreadPoolExecutor(max_workers=max_workers) as executor: - futures = [] - for index, segment_url in enumerate(self.segments): - time.sleep(delay) - futures.append(executor.submit(self.make_requests_stream, segment_url, index, progress_bar)) + # Configure workers and delay + max_workers = len(self.valid_proxy) if THERE_IS_PROXY_LIST else TQDM_MAX_WORKER + delay = max(PROXY_START_MIN, min(PROXY_START_MAX, 1 / (len(self.valid_proxy) + 1))) if THERE_IS_PROXY_LIST else TQDM_DELAY_WORKER - # Wait for all futures to complete - for future in futures: - try: - future.result() - except Exception as e: - logging.error(f"Error in download thread: {str(e)}") + # Download segments with completion verification + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = [] + for index, segment_url in enumerate(self.segments): + # Check for interrupt before submitting each task + if self.interrupt_flag.is_set(): + break - # Verify completion and retry missing segments - total_segments = len(self.segments) - completed_segments = len(self.downloaded_segments) - - if completed_segments < total_segments: - missing_segments = set(range(total_segments)) - self.downloaded_segments - logging.warning(f"Missing segments: {sorted(missing_segments)}") - - # Retry missing segments - for index in missing_segments: - if self.stop_event.is_set(): + time.sleep(delay) + futures.append(executor.submit(self.make_requests_stream, segment_url, index, progress_bar)) + + # Wait for futures with interrupt handling + for future in as_completed(futures): + if self.interrupt_flag.is_set(): break try: - self.make_requests_stream(self.segments[index], index, progress_bar) + future.result() except Exception as e: - logging.error(f"Failed to retry segment {index}: {str(e)}") + logging.error(f"Error in download thread: {str(e)}") + + # Interrupt handling for missing segments + if not self.interrupt_flag.is_set(): + total_segments = len(self.segments) + completed_segments = len(self.downloaded_segments) + + if completed_segments < total_segments: + missing_segments = set(range(total_segments)) - self.downloaded_segments + logging.warning(f"Missing segments: {sorted(missing_segments)}") + + # Retry missing segments with interrupt check + for index in missing_segments: + if self.interrupt_flag.is_set(): + break + try: + self.make_requests_stream(self.segments[index], index, progress_bar) + except Exception as e: + logging.error(f"Failed to retry segment {index}: {str(e)}") + + except Exception as e: + logging.error(f"Download failed: {str(e)}") + raise + + finally: + + # Clean up resources + self.stop_event.set() + writer_thread.join(timeout=30) + progress_bar.close() + + # Check if download was interrupted + if self.download_interrupted: + console.log("[red] Download was manually stopped.") + + # Optional: Delete partial download + if os.path.exists(self.tmp_file_path): + os.remove(self.tmp_file_path) + sys.exit(0) # Clean up self.stop_event.set() diff --git a/StreamingCommunity/Src/Lib/Downloader/MP4/downloader.py b/StreamingCommunity/Src/Lib/Downloader/MP4/downloader.py index e81a52b..3b24018 100644 --- a/StreamingCommunity/Src/Lib/Downloader/MP4/downloader.py +++ b/StreamingCommunity/Src/Lib/Downloader/MP4/downloader.py @@ -2,6 +2,8 @@ import os import sys +import ssl +import certifi import logging @@ -22,6 +24,11 @@ from StreamingCommunity.Src.Util.os import internet_manager from ...FFmpeg import print_duration_table +# Suppress SSL warnings +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + # Config GET_ONLY_LINK = config_manager.get_bool('M3U8_PARSER', 'get_only_link') TQDM_USE_LARGE_BAR = config_manager.get_int('M3U8_DOWNLOAD', 'tqdm_use_large_bar') @@ -30,72 +37,96 @@ REQUEST_TIMEOUT = config_manager.get_float('REQUESTS', 'timeout') -def MP4_downloader(url: str, path: str, referer: str = None, headers_: str = None): - +def MP4_downloader(url: str, path: str, referer: str = None, headers_: dict = None): """ - Downloads an MP4 video from a given URL using the specified referer header. + Downloads an MP4 video from a given URL with robust error handling and SSL bypass. - Parameter: + Parameters: - url (str): The URL of the MP4 video to download. - path (str): The local path where the downloaded MP4 file will be saved. - - referer (str): The referer header value to include in the HTTP request headers. + - referer (str, optional): The referer header value. + - headers_ (dict, optional): Custom headers for the request. """ + # Early return for link-only mode if GET_ONLY_LINK: return {'path': path, 'url': url} - headers = None + # Validate URL + 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]") + return None - if "http" not in str(url).lower().strip() or "https" not in str(url).lower().strip(): - logging.error(f"Invalid url: {url}") - sys.exit(0) - - if referer != None: - headers = {'Referer': referer, 'user-agent': get_headers()} - if headers == None: - headers = {'user-agent': get_headers()} - else: - headers = headers_ - - # Make request to get content of video - with httpx.Client(verify=REQUEST_VERIFY, timeout=REQUEST_TIMEOUT) as client: - with client.stream("GET", url, headers=headers, timeout=REQUEST_TIMEOUT) as response: - total = int(response.headers.get('content-length', 0)) + # Prepare headers + try: + headers = {} + if referer: + headers['Referer'] = referer + + # Use custom headers if provided, otherwise use default user agent + if headers_: + headers.update(headers_) + else: + headers['User-Agent'] = get_headers() - if total != 0: + except Exception as header_err: + logging.error(f"Error preparing headers: {header_err}") + console.print(f"[bold red]Error preparing headers: {header_err}[/bold red]") + return None - # Create bar format - if TQDM_USE_LARGE_BAR: - bar_format = (f"{Colors.YELLOW}[MP4] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): " - f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ " - f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] " - f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}} {Colors.WHITE}| " - f"{Colors.YELLOW}{{rate_fmt}}{{postfix}} {Colors.WHITE}]") - else: - bar_format = (f"{Colors.YELLOW}Proc{Colors.WHITE}: {Colors.RED}{{percentage:.2f}}% " - f"{Colors.WHITE}| {Colors.CYAN}{{remaining}}{{postfix}} {Colors.WHITE}]") + try: + # Create a custom transport that bypasses SSL verification + transport = httpx.HTTPTransport( + verify=False, # Disable SSL certificate verification + http2=True # Optional: enable HTTP/2 support + ) + + # Download with streaming and progress tracking + with httpx.Client(transport=transport, timeout=httpx.Timeout(60.0)) as client: + with client.stream("GET", url, headers=headers, timeout=REQUEST_TIMEOUT) as response: + response.raise_for_status() + + # Get total file size + total = int(response.headers.get('content-length', 0)) + + # Handle empty streams + if total == 0: + console.print("[bold red]No video stream found.[/bold red]") + return None # Create progress bar progress_bar = tqdm( total=total, ascii='░▒█', - bar_format=bar_format, + bar_format=f"{Colors.YELLOW}[MP4] {Colors.WHITE}({Colors.CYAN}video{Colors.WHITE}): " + f"{Colors.RED}{{percentage:.2f}}% {Colors.MAGENTA}{{bar}} {Colors.WHITE}[ " + f"{Colors.YELLOW}{{n_fmt}}{Colors.WHITE} / {Colors.RED}{{total_fmt}} {Colors.WHITE}] " + f"{Colors.YELLOW}{{elapsed}} {Colors.WHITE}< {Colors.CYAN}{{remaining}} {Colors.WHITE}| " + f"{Colors.YELLOW}{{rate_fmt}}{{postfix}} {Colors.WHITE}]", + unit='iB', unit_scale=True, - unit_divisor=1024, + desc='Downloading', mininterval=0.05 ) + # Ensure directory exists + os.makedirs(os.path.dirname(path), exist_ok=True) + # Download file with open(path, 'wb') as file, progress_bar as bar: + downloaded = 0 for chunk in response.iter_bytes(chunk_size=1024): if chunk: size = file.write(chunk) + downloaded += size bar.update(size) - else: - console.print("[red]Cant find any stream.") + # Optional: Add a check to stop download if needed + # if downloaded > MAX_DOWNLOAD_SIZE: + # break - # Get summary - if total != 0: + # Post-download processing + if os.path.exists(path) and os.path.getsize(path) > 0: console.print(Panel( f"[bold green]Download completed![/bold green]\n" f"[cyan]File size: [bold red]{internet_manager.format_file_size(os.path.getsize(path))}[/bold red]\n" @@ -103,3 +134,23 @@ def MP4_downloader(url: str, path: str, referer: str = None, headers_: str = Non title=f"{os.path.basename(path.replace('.mp4', ''))}", border_style="green" )) + return path + + else: + console.print("[bold red]Download failed or file is empty.[/bold red]") + return None + + except httpx.HTTPStatusError as http_err: + logging.error(f"HTTP error occurred: {http_err}") + console.print(f"[bold red]HTTP Error: {http_err}[/bold red]") + return None + + except httpx.RequestError as req_err: + logging.error(f"Request error: {req_err}") + console.print(f"[bold red]Request Error: {req_err}[/bold red]") + return None + + except Exception as e: + logging.error(f"Unexpected error during download: {e}") + console.print(f"[bold red]Unexpected Error: {e}[/bold red]") + return None diff --git a/StreamingCommunity/Src/Lib/TMBD/tmdb.py b/StreamingCommunity/Src/Lib/TMBD/tmdb.py index f671096..dbd4af4 100644 --- a/StreamingCommunity/Src/Lib/TMBD/tmdb.py +++ b/StreamingCommunity/Src/Lib/TMBD/tmdb.py @@ -99,7 +99,7 @@ class TheMovieDB: self.api_key = api_key self.base_url = "https://api.themoviedb.org/3" self.console = Console() - self.genres = self._fetch_genres() + #self.genres = self._fetch_genres() def _make_request(self, endpoint, params=None): """ diff --git a/StreamingCommunity/Src/Util/os.py b/StreamingCommunity/Src/Util/os.py index 6cba438..b1e5707 100644 --- a/StreamingCommunity/Src/Util/os.py +++ b/StreamingCommunity/Src/Util/os.py @@ -356,7 +356,6 @@ class OsSummary(): Includes: - Python version and implementation details. - Operating system and architecture. - - OpenSSL and glibc versions. - Versions of `ffmpeg` and `ffprobe` executables. - Installed Python libraries as listed in `requirements.txt`. """ @@ -370,11 +369,10 @@ class OsSummary(): python_implementation = platform.python_implementation() arch = platform.machine() os_info = platform.platform() - openssl_version = ssl.OPENSSL_VERSION glibc_version = 'glibc ' + '.'.join(map(str, platform.libc_ver()[1])) - console.print(f"[cyan]Python[white]: [bold red]{python_version} ({python_implementation} {arch}) - {os_info} ({openssl_version}, {glibc_version})[/bold red]") - logging.info(f"Python: {python_version} ({python_implementation} {arch}) - {os_info} ({openssl_version}, {glibc_version})") + console.print(f"[cyan]Python[white]: [bold red]{python_version} ({python_implementation} {arch}) - {os_info} ({glibc_version})[/bold red]") + logging.info(f"Python: {python_version} ({python_implementation} {arch}) - {os_info} ({glibc_version})") # ffmpeg and ffprobe versions ffmpeg_version = self.get_executable_version(['ffmpeg', '-version']) diff --git a/StreamingCommunity/run.py b/StreamingCommunity/run.py index 310978c..956296c 100644 --- a/StreamingCommunity/run.py +++ b/StreamingCommunity/run.py @@ -135,9 +135,9 @@ def main(): # Create logger log_not = Logger() + #initialize() # Load search functions - search_functions = load_search_functions() logging.info(f"Load module in: {time.time() - start} s") @@ -194,11 +194,3 @@ def main(): else: console.print("[red]Invalid category.") sys.exit(0) - - -def run(): - initialize() - main() - -if __name__ == '__main__': - run() diff --git a/Test/Player/helper/vixcloud.py b/Test/Player/helper/vixcloud.py new file mode 100644 index 0000000..e8adcf9 --- /dev/null +++ b/Test/Player/helper/vixcloud.py @@ -0,0 +1,38 @@ +# Fix import +import sys +import os +src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) +sys.path.append(src_path) + + +# Import +from urllib.parse import urlparse, urlencode, urlunparse +from StreamingCommunity.Src.Api.Player.Helper.Vixcloud.js_parser import JavaScriptParser +from StreamingCommunity.Src.Api.Player.Helper.Vixcloud.util import WindowVideo, WindowParameter, StreamsCollection + + +# Data +script_text = ''' + window.video = {"id":271977,"name":"Smile 2","filename":"Smile.2.2024.1080p.WEB-DL.DDP5.1.H.264-FHC.mkv","size":10779891,"quality":1080,"duration":7758,"views":0,"is_viewable":1,"status":"public","fps":24,"legacy":0,"folder_id":"301e469a-786f-493a-ad2b-302248aa2d23","created_at_diff":"4 giorni fa"}; + window.streams = [{"name":"Server1","active":false,"url":"https:\/\/vixcloud.co\/playlist\/271977?b=1\u0026ub=1"},{"name":"Server2","active":1,"url":"https:\/\/vixcloud.co\/playlist\/271977?b=1\u0026ab=1"}]; + window.masterPlaylist = { + params: { + 'token': '890a3e7db7f1c8213a11007947362b21', + 'expires': '1737812156', + }, + url: 'https://vixcloud.co/playlist/271977?b=1', + } + window.canPlayFHD = true +''' + + +# Test +converter = JavaScriptParser.parse(js_string=str(script_text)) + +window_video = WindowVideo(converter.get('video')) +window_streams = StreamsCollection(converter.get('streams')) +window_parameter = WindowParameter(converter.get('masterPlaylist')) + +print(window_video, "\n") +print(window_streams, "\n") +print(window_parameter, "\n") \ No newline at end of file diff --git a/dockerfile b/dockerfile deleted file mode 100644 index ff5a582..0000000 --- a/dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM python:3.11-slim - -COPY . /app -WORKDIR /app - -ENV TEMP /tmp -RUN mkdir -p $TEMP - -RUN apt-get update && apt-get install -y \ - ffmpeg \ - build-essential \ - libssl-dev \ - libffi-dev \ - python3-dev \ - libxml2-dev \ - libxslt1-dev - -RUN pip install --no-cache-dir -r requirements.txt - -CMD ["python", "run.py"] diff --git a/test_run.py b/test_run.py index 366afbc..74806a1 100644 --- a/test_run.py +++ b/test_run.py @@ -1,3 +1,5 @@ +# 26.11.24 + from StreamingCommunity.run import main main() \ No newline at end of file diff --git a/unix_install.sh b/unix_install.sh deleted file mode 100644 index a595cff..0000000 --- a/unix_install.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/sh - -# Function to check if a command exists -command_exists() { - command -v "$1" > /dev/null 2>&1 -} - -# Install on Debian/Ubuntu-based systems -install_on_debian() { - echo "Installing $1..." - sudo apt update - sudo apt install -y "$1" -} - -# Install on Red Hat/CentOS/Fedora-based systems -install_on_redhat() { - echo "Installing $1..." - sudo yum install -y "$1" -} - -# Install on Arch-based systems -install_on_arch() { - echo "Installing $1..." - sudo pacman -Sy --noconfirm "$1" -} - -# Install on BSD-based systems -install_on_bsd() { - echo "Installing $1..." - env ASSUME_ALWAYS_YES=yes sudo pkg install -y "$1" -} - -# Install on macOS -install_on_macos() { - echo "Installing $1..." - if command_exists brew; then - brew install "$1" - else - echo "Homebrew is not installed. Installing Homebrew first..." - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - brew install "$1" - fi -} - -set -e - - -# Check and install Python3 -# if command_exists python3 > /dev/null 2>&1; then -# echo "Checking Python..." -# else -# # Detect the platform and install Python3 accordingly -# if [[ "$OSTYPE" == "linux-gnu"* ]]; then -# # Detect the package manager -# if command_exists apt; then -# install_on_debian "python3" -# elif command_exists yum; then -# install_on_redhat "python3" -# elif command_exists pacman; then -# install_on_arch "python-pip" -# else -# echo "Unsupported Linux distribution." -# exit 1 -# fi -# elif [[ "$OSTYPE" == "bsd"* ]]; then -# echo "Detected BSD-based system." -# install_on_bsd "python39" -# elif [[ "$OSTYPE" == "darwin"* ]]; then -# install_on_macos "python" -# else -# echo "Unsupported operating system." -# exit 1 -# fi -# fi - -# Get the Python version -PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:3])))') - -# Compare the Python version with 3.8 -REQUIRED_VERSION="3.8" - -if [ "$(echo -e "$PYTHON_VERSION\n$REQUIRED_VERSION" | sort -V | head -n1)" = "$REQUIRED_VERSION" ]; then - echo "Python version $PYTHON_VERSION is >= $REQUIRED_VERSION. Continuing..." -else - echo "ERROR: Python version $PYTHON_VERSION is < $REQUIRED_VERSION. Exiting..." - exit 1 -fi - -if [ -d ".venv/" ]; then - echo ".venv exists. Installing requirements.txt..." - .venv/bin/pip install -r requirements.txt -else - echo "Making .venv and installing requirements.txt..." - - if [ "$(uname)" = "Linux" ]; then - # Detect the package manager for venv installation check. - if command_exists apt; then - echo "Detected Debian-based system. Checking python3-venv." - if dpkg -l | grep -q "python3-venv"; then - echo "python3-venv found." - else - echo "python3-venv not found, installing..." - install_on_debian "python3-venv" - fi - fi - fi - - python3 -m venv .venv - .venv/bin/pip install -r requirements.txt - -fi - -if command_exists ffmpeg; then - echo "ffmpeg exists." -else - echo "ffmpeg does not exist." - - # Detect the platform and install ffmpeg accordingly. - case "$(uname)" in - Linux) - if command_exists apt; then - echo "Detected Debian-based system." - install_on_debian "ffmpeg" - elif command_exists yum; then - echo "Detected Red Hat-based system." - echo "Installing needed repos for ffmpeg..." - sudo yum config-manager --set-enabled crb > /dev/null 2>&1 || true - sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-$(rpm -E %rhel).noarch.rpm https://dl.fedoraproject.org/pub/epel/epel-next-release-latest-$(rpm -E %rhel).noarch.rpm > /dev/null 2>&1 || true - sudo yum install -y --nogpgcheck https://mirrors.rpmfusion.org/free/el/rpmfusion-free-release-$(rpm -E %rhel).noarch.rpm https://mirrors.rpmfusion.org/nonfree/el/rpmfusion-nonfree-release-$(rpm -E %rhel).noarch.rpm > /dev/null 2>&1 || true - install_on_redhat "ffmpeg" - elif command_exists pacman; then - echo "Detected Arch-based system." - install_on_arch "ffmpeg" - else - echo "Unsupported Linux distribution." - exit 1 - fi - ;; - FreeBSD|NetBSD|OpenBSD) - echo "Detected BSD-based system." - install_on_bsd "ffmpeg" - ;; - Darwin) - echo "Detected macOS." - install_on_macos "ffmpeg" - ;; - *) - echo "Unsupported operating system." - exit 1 - ;; - esac -fi - -if command_exists openssl || .venv/bin/pip list | grep -q pycryptodome; then - echo "openssl or pycryptodome exists." -else - echo "Please choose an option:" - echo "1) openssl" - echo "2) pycryptodome" - read -p "Enter your choice (1): " choice - - case "$choice" in - 2) - echo "Installing pycryptodome." - .venv/bin/pip install pycryptodome - ;; - *) - # Detect the platform and install OpenSSL accordingly. - case "$(uname)" in - Linux) - if command_exists apt; then - install_on_debian openssl - elif command_exists yum; then - install_on_redhat openssl - elif command_exists pacman; then - install_on_arch openssl - else - echo "Unsupported Linux distribution." - exit 1 - fi - ;; - FreeBSD|NetBSD|OpenBSD) - install_on_bsd openssl - ;; - Darwin) - install_on_macos openssl - ;; - *) - echo "Unsupported operating system." - exit 1 - ;; - esac - ;; - esac -fi - -sed -i.bak '1s|.*|#!.venv/bin/python3|' run.py -sudo chmod +x run.py -echo 'Everything is installed!' -echo 'Run StreamingCommunity with "./run.py"' diff --git a/update.py b/update.py deleted file mode 100644 index a7fc08f..0000000 --- a/update.py +++ /dev/null @@ -1,194 +0,0 @@ -# 15.06.24 - -import os -import shutil -from io import BytesIO -from zipfile import ZipFile -from datetime import datetime - - -# External library -import httpx -from rich.console import Console -from rich.prompt import Prompt -from rich.panel import Panel -from rich.table import Table - - -# Variable -console = Console() -local_path = os.path.join(".") -from StreamingCommunity.Src.Upload.version import __author__, __title__ - - -def move_content(source: str, destination: str): - """ - Move all content from the source folder to the destination folder. - - Parameters: - - source (str): The path to the source folder. - - destination (str): The path to the destination folder. - """ - os.makedirs(destination, exist_ok=True) - - # Iterate through all elements in the source folder - for element in os.listdir(source): - source_path = os.path.join(source, element) - destination_path = os.path.join(destination, element) - - # If it's a directory, recursively call the function - if os.path.isdir(source_path): - move_content(source_path, destination_path) - else: - shutil.move(source_path, destination_path) - - -def keep_specific_items(directory: str, keep_folder: str, keep_file: str): - """ - Delete all items in the directory except for the specified folder and file. - - Parameters: - - directory (str): The path to the directory. - - keep_folder (str): The name of the folder to keep. - - keep_file (str): The name of the file to keep. - """ - try: - if not os.path.exists(directory) or not os.path.isdir(directory): - raise ValueError(f"Error: '{directory}' is not a valid directory.") - - # Iterate through items in the directory - for item in os.listdir(directory): - item_path = os.path.join(directory, item) - - # Check if the item is the specified folder or file - if os.path.isdir(item_path) and item != keep_folder: - shutil.rmtree(item_path) - - elif os.path.isfile(item_path) and item != keep_file: - os.remove(item_path) - - except PermissionError as pe: - console.print(f"[red]PermissionError: {pe}. Check permissions and try again.") - - except Exception as e: - console.print(f"[red]Error: {e}") - - -def print_commit_info(commit_info: dict): - """ - Print detailed information about the commit in a formatted table. - - Parameters: - - commit_info (dict): The commit information from GitHub API - """ - - # Create a table for commit information - table = Table(title=f"[bold green]Latest Commit Information - {__title__}", show_header=False) - table.add_column("Field", style="cyan") - table.add_column("Value", style="yellow") - - # Basic commit info - commit = commit_info['commit'] - commit_date = datetime.strptime(commit['author']['date'], "%Y-%m-%dT%H:%M:%SZ") - formatted_date = commit_date.strftime("%Y-%m-%d %H:%M:%S") - - # Add rows to the table - table.add_row("Repository", f"{__author__}/{__title__}") - table.add_row("Commit SHA", commit_info['sha'][:8]) - table.add_row("Author", f"{commit['author']['name']} <{commit['author']['email']}>") - table.add_row("Date", formatted_date) - table.add_row("Committer", f"{commit['committer']['name']} <{commit['committer']['email']}>") - table.add_row("Message", commit['message']) - - # Add stats if available - if 'stats' in commit_info: - stats = commit_info['stats'] - table.add_row("Changes", f"+{stats['additions']} -[red]{stats['deletions']}[/red] ({stats['total']} total)") - - # Add URL info - table.add_row("HTML URL", commit_info['html_url']) - - # Print the table in a panel - console.print(Panel.fit(table)) - -def download_and_extract_latest_commit(): - """ - Download and extract the latest commit from a GitHub repository. - """ - try: - - # Get the latest commit information using GitHub API - api_url = f'https://api.github.com/repos/{__author__}/{__title__}/commits?per_page=1' - console.log("[green]Requesting latest commit from GitHub repository...") - - headers = { - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': f'{__title__}-updater' - } - response = httpx.get(api_url, headers=headers, timeout=10) - - if response.status_code == 200: - commit_info = response.json()[0] - commit_sha = commit_info['sha'] - - # Print detailed commit information - print_commit_info(commit_info) - - zipball_url = f'https://github.com/{__author__}/{__title__}/archive/{commit_sha}.zip' - console.log("[green]Downloading latest commit zip file...") - - # Download the zipball - response = httpx.get(zipball_url, follow_redirects=True, timeout=10) - temp_path = os.path.join(os.path.dirname(os.getcwd()), 'temp_extracted') - - # Extract the content of the zipball into a temporary folder - with ZipFile(BytesIO(response.content)) as zip_ref: - zip_ref.extractall(temp_path) - console.log("[green]Extracting files...") - - # Move files from the temporary folder to the current folder - for item in os.listdir(temp_path): - item_path = os.path.join(temp_path, item) - destination_path = os.path.join(local_path, item) - shutil.move(item_path, destination_path) - - # Remove the temporary folder - shutil.rmtree(temp_path) - - # Move all folder to main folder - new_folder_name = f"{__title__}-{commit_sha}" - move_content(new_folder_name, ".") - shutil.rmtree(new_folder_name) - - console.log("[cyan]Latest commit downloaded and extracted successfully.") - else: - console.log(f"[red]Failed to fetch commit information. Status code: {response.status_code}") - - except httpx.RequestError as e: - console.print(f"[red]Request failed: {e}") - - except Exception as e: - console.print(f"[red]An unexpected error occurred: {e}") - - -def main_upload(): - """ - Main function to upload the latest commit of a GitHub repository. - """ - cmd_insert = Prompt.ask( - "[bold red]Are you sure you want to delete all files? (Only 'Video' folder and 'update_version.py' will remain)", - choices=['y', 'n'], - default='y', - show_choices=True - ) - - if cmd_insert.lower().strip() == 'y' or cmd_insert.lower().strip() == 'yes': - console.print("[red]Deleting all files except 'Video' folder and 'update_version.py'...") - keep_specific_items(".", "Video", "upload.py") - download_and_extract_latest_commit() - else: - console.print("[red]Operation cancelled.") - - -if __name__ == "__main__": - main_upload() \ No newline at end of file diff --git a/win_install.bat b/win_install.bat deleted file mode 100644 index cdf4bd4..0000000 --- a/win_install.bat +++ /dev/null @@ -1,134 +0,0 @@ -@echo off -:: Check if the script is running as administrator -net session >nul 2>&1 -if %errorlevel% neq 0 ( - echo Running as administrator... - :: Restart the script with administrator privileges - powershell -Command "Start-Process '%~f0' -Verb RunAs" - exit /b -) - -chcp 65001 > nul -SETLOCAL ENABLEDELAYEDEXPANSION - -echo Script starting... - -:: Check if Chocolatey is already installed -:check_choco -echo Checking if Chocolatey is installed... -choco --version >nul 2>&1 -IF %ERRORLEVEL% EQU 0 ( - echo Chocolatey is already installed. Skipping installation. - goto install_python -) ELSE ( - echo Installing Chocolatey... - @"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command "iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))" || ( - echo Error during Chocolatey installation. - exit /b 1 - ) - echo Chocolatey installed successfully. - call choco --version - echo. -) - -:: Check if Python is already installed -:install_python -echo Checking if Python is installed... -python -V >nul 2>&1 -IF %ERRORLEVEL% EQU 0 ( - echo Python is already installed. Skipping installation. - goto install_openssl -) ELSE ( - echo Installing Python... - choco install python --confirm --params="'/NoStore'" --allow-downgrade || ( - echo Error during Python installation. - exit /b 1 - ) - echo Python installed successfully. - call python -V - echo. -) - -:: Ask to restart the terminal -echo Please restart the terminal to continue... -pause -exit /b - -:: Check if OpenSSL is already installed -:install_openssl -echo Checking if OpenSSL is installed... -openssl version -a >nul 2>&1 -IF %ERRORLEVEL% EQU 0 ( - echo OpenSSL is already installed. Skipping installation. - goto install_ffmpeg -) ELSE ( - echo Installing OpenSSL... - choco install openssl --confirm || ( - echo Error during OpenSSL installation. - exit /b 1 - ) - echo OpenSSL installed successfully. - call openssl version -a - echo. -) - -:: Check if FFmpeg is already installed -:install_ffmpeg -echo Checking if FFmpeg is installed... -ffmpeg -version >nul 2>&1 -IF %ERRORLEVEL% EQU 0 ( - echo FFmpeg is already installed. Skipping installation. - goto create_venv -) ELSE ( - echo Installing FFmpeg... - choco install ffmpeg --confirm || ( - echo Error during FFmpeg installation. - exit /b 1 - ) - echo FFmpeg installed successfully. - call ffmpeg -version - echo. -) - -:: Verify installations -:verifica_installazioni -echo Verifying installations... -call choco --version -call python -V -call openssl version -a -call ffmpeg -version - -echo All programs have been successfully installed and verified. - -:: Create a virtual environment .venv -:create_venv -echo Checking if the .venv virtual environment already exists... -if exist .venv ( - echo The .venv virtual environment already exists. Skipping creation. -) ELSE ( - echo Creating the .venv virtual environment... - python -m venv .venv || ( - echo Error during virtual environment creation. - exit /b 1 - ) - echo Virtual environment created successfully. -) - -:: Activate the virtual environment and install requirements -echo Installing requirements... -call .venv\Scripts\activate.bat -pip install -r requirements.txt || ( - echo Error during requirements installation. - exit /b 1 -) - -:: Run run.py -echo Running run.py... -call .venv\Scripts\python .\run.py || ( - echo Error during run.py execution. - exit /b 1 -) - -echo End of script. - -ENDLOCAL