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