├── GUI ├── webgui │ ├── __init__.py │ ├── asgi.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── requirements.txt ├── searchapp │ ├── apps.py │ ├── urls.py │ ├── forms.py │ ├── api │ │ ├── __init__.py │ │ ├── animeunity.py │ │ └── base.py │ └── tests.py ├── README.md └── manage.py ├── .github ├── FUNDING.yml ├── .domain │ ├── loc-badge.json │ └── domains.json ├── .site │ ├── img │ │ ├── crunchyroll_etp_rt.png │ │ └── crunchyroll_x_cr_tab_id.png │ ├── login.md │ └── index.html ├── workflows │ ├── testing.yml │ ├── pages.yml │ ├── docker-publish.yml │ └── update_domain.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── StreamingCommunity ├── TelegramHelp │ └── __init__.py ├── __main__.py ├── Api │ ├── Template │ │ ├── __init__.py │ │ ├── Util │ │ │ └── __init__.py │ │ ├── config_loader.py │ │ ├── Class │ │ │ └── SearchType.py │ │ └── site.py │ ├── Site │ │ ├── raiplay │ │ │ ├── util │ │ │ │ └── get_license.py │ │ │ ├── film.py │ │ │ └── site.py │ │ ├── animeunity │ │ │ ├── film.py │ │ │ ├── util │ │ │ │ └── ScrapeSerie.py │ │ │ └── site.py │ │ ├── plutotv │ │ │ ├── util │ │ │ │ └── get_license.py │ │ │ ├── site.py │ │ │ └── __init__.py │ │ ├── realtime │ │ │ ├── util │ │ │ │ └── get_license.py │ │ │ ├── site.py │ │ │ └── __init__.py │ │ ├── mediasetinfinity │ │ │ ├── util │ │ │ │ └── fix_mpd.py │ │ │ ├── film.py │ │ │ └── site.py │ │ ├── animeworld │ │ │ ├── film.py │ │ │ ├── util │ │ │ │ └── ScrapeSerie.py │ │ │ ├── site.py │ │ │ └── serie.py │ │ ├── guardaserie │ │ │ ├── site.py │ │ │ └── __init__.py │ │ ├── hd4me │ │ │ ├── site.py │ │ │ ├── film.py │ │ │ └── __init__.py │ │ ├── dmax │ │ │ ├── site.py │ │ │ └── __init__.py │ │ ├── streamingcommunity │ │ │ ├── film.py │ │ │ └── site.py │ │ ├── altadefinizione │ │ │ ├── site.py │ │ │ ├── film.py │ │ │ └── util │ │ │ │ └── ScrapeSerie.py │ │ └── crunchyroll │ │ │ ├── site.py │ │ │ └── film.py │ └── Player │ │ ├── sweetpixel.py │ │ ├── hdplayer.py │ │ ├── mediapolisvod.py │ │ └── Helper │ │ └── Vixcloud │ │ └── js_parser.py ├── Lib │ ├── TMBD │ │ ├── __init__.py │ │ └── obj_tmbd.py │ ├── FFmpeg │ │ └── __init__.py │ ├── M3U8 │ │ ├── __init__.py │ │ ├── url_fixer.py │ │ └── decryptor.py │ └── Downloader │ │ ├── __init__.py │ │ ├── MEGA │ │ ├── errors.py │ │ └── crypto.py │ │ └── DASH │ │ ├── cdm_helpher.py │ │ └── decrypt.py ├── Upload │ ├── version.py │ └── update.py ├── Util │ ├── installer │ │ ├── __init__.py │ │ └── binary_paths.py │ ├── headers.py │ ├── color.py │ ├── message.py │ └── logger.py └── __init__.py ├── MANIFEST.in ├── Makefile ├── requirements.txt ├── Test ├── Downloads │ ├── MEGA.py │ ├── MP4.py │ ├── HLS.py │ ├── DASH.py │ └── TOR.py └── Util │ └── hooks.py ├── dockerfile ├── test_run.py ├── .gitignore ├── setup.py └── config.json /GUI/webgui/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: E1E11LVC83 -------------------------------------------------------------------------------- /GUI/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=4.2,<5.0 -------------------------------------------------------------------------------- /StreamingCommunity/TelegramHelp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /StreamingCommunity/__main__.py: -------------------------------------------------------------------------------- 1 | from .run import main 2 | 3 | main() 4 | -------------------------------------------------------------------------------- /.github/.domain/loc-badge.json: -------------------------------------------------------------------------------- 1 | {"schemaVersion": 1, "label": "Lines of Code", "message": "9110", "color": "green"} 2 | -------------------------------------------------------------------------------- /.github/.site/img/crunchyroll_etp_rt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrowar/StreamingCommunity/HEAD/.github/.site/img/crunchyroll_etp_rt.png -------------------------------------------------------------------------------- /.github/.site/img/crunchyroll_x_cr_tab_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arrowar/StreamingCommunity/HEAD/.github/.site/img/crunchyroll_x_cr_tab_id.png -------------------------------------------------------------------------------- /StreamingCommunity/Api/Template/__init__.py: -------------------------------------------------------------------------------- 1 | # 19.06.24 2 | 3 | from .site import get_select_title 4 | 5 | __all__ = [ 6 | "get_select_title" 7 | ] -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include StreamingCommunity * 2 | recursive-include StreamingCommunity/Api * 3 | recursive-include StreamingCommunity/Lib * 4 | include requirements.txt -------------------------------------------------------------------------------- /StreamingCommunity/Lib/TMBD/__init__.py: -------------------------------------------------------------------------------- 1 | # 17.09.24 2 | 3 | from .tmdb import tmdb 4 | from .obj_tmbd import Json_film 5 | 6 | __all__ = [ 7 | "tmdb", 8 | "Json_film" 9 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Upload/version.py: -------------------------------------------------------------------------------- 1 | __title__ = 'StreamingCommunity' 2 | __version__ = '3.4.8' 3 | __author__ = 'Arrowar' 4 | __description__ = 'A command-line program to download film' 5 | __copyright__ = 'Copyright 2025' -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-container: 2 | docker build -t streaming-community-api . 3 | 4 | run-container: 5 | docker run --rm -it --dns 9.9.9.9 -p 8000:8000 -v ${LOCAL_DIR}:/app/Video -v ./config.json:/app/config.json streaming-community-api -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx 2 | bs4 3 | rich 4 | tqdm 5 | m3u8 6 | psutil 7 | unidecode 8 | curl_cffi 9 | jsbeautifier 10 | pathvalidate 11 | pycryptodomex 12 | ua-generator 13 | qbittorrent-api 14 | pyTelegramBotAPI 15 | pywidevine 16 | python-dotenv -------------------------------------------------------------------------------- /GUI/searchapp/apps.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class SearchappConfig(AppConfig): 8 | default_auto_field = "django.db.models.BigAutoField" 9 | name = "searchapp" -------------------------------------------------------------------------------- /GUI/webgui/asgi.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | import os 5 | from django.core.asgi import get_asgi_application 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webgui.settings") 8 | application = get_asgi_application() -------------------------------------------------------------------------------- /GUI/webgui/wsgi.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | import os 5 | from django.core.wsgi import get_wsgi_application 6 | 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webgui.settings") 8 | application = get_wsgi_application() -------------------------------------------------------------------------------- /GUI/webgui/urls.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | from django.contrib import admin 5 | from django.urls import path, include 6 | 7 | urlpatterns = [ 8 | path("admin/", admin.site.urls), 9 | path("", include("searchapp.urls")), 10 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Util/installer/__init__.py: -------------------------------------------------------------------------------- 1 | # 18.07.25 2 | 3 | from .ffmpeg_install import check_ffmpeg 4 | from .bento4_install import check_mp4decrypt 5 | from .device_install import check_device_wvd_path 6 | 7 | __all__ = [ 8 | "check_ffmpeg", 9 | "check_mp4decrypt", 10 | "check_device_wvd_path" 11 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Lib/FFmpeg/__init__.py: -------------------------------------------------------------------------------- 1 | # 18.04.24 2 | 3 | from .command import join_video, join_audios, join_subtitle 4 | from .util import print_duration_table, get_video_duration 5 | 6 | 7 | __all__ = [ 8 | "join_video", 9 | "join_audios", 10 | "join_subtitle", 11 | "print_duration_table", 12 | "get_video_duration", 13 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Lib/M3U8/__init__.py: -------------------------------------------------------------------------------- 1 | # 02.04.24 2 | 3 | from .decryptor import M3U8_Decryption 4 | from .estimator import M3U8_Ts_Estimator 5 | from .parser import M3U8_Parser, M3U8_Codec 6 | from .url_fixer import M3U8_UrlFix 7 | 8 | __all__ = [ 9 | "M3U8_Decryption", 10 | "M3U8_Ts_Estimator", 11 | "M3U8_Parser", 12 | "M3U8_Codec", 13 | "M3U8_UrlFix" 14 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Util/headers.py: -------------------------------------------------------------------------------- 1 | # 4.04.24 2 | 3 | # External library 4 | import ua_generator 5 | 6 | 7 | # Variable 8 | ua = ua_generator.generate(device='desktop', browser=('chrome', 'edge')) 9 | 10 | 11 | def get_userAgent() -> str: 12 | user_agent = ua_generator.generate().text 13 | return user_agent 14 | 15 | 16 | def get_headers() -> dict: 17 | return ua.headers.get() -------------------------------------------------------------------------------- /StreamingCommunity/Lib/Downloader/__init__.py: -------------------------------------------------------------------------------- 1 | # 23.06.24 2 | 3 | from .HLS.downloader import HLS_Downloader 4 | from .MP4.downloader import MP4_downloader 5 | from .TOR.downloader import TOR_downloader 6 | from .DASH.downloader import DASH_Downloader 7 | from .MEGA.mega import Mega_Downloader 8 | 9 | __all__ = [ 10 | "HLS_Downloader", 11 | "MP4_downloader", 12 | "TOR_downloader", 13 | "DASH_Downloader", 14 | "Mega_Downloader" 15 | ] -------------------------------------------------------------------------------- /GUI/searchapp/urls.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | from django.urls import path 5 | from . import views 6 | 7 | urlpatterns = [ 8 | path("", views.search_home, name="search_home"), 9 | path("search/", views.search, name="search"), 10 | path("download/", views.start_download, name="start_download"), 11 | path("series-metadata/", views.series_metadata, name="series_metadata"), 12 | path("series-detail/", views.series_detail, name="series_detail"), 13 | ] -------------------------------------------------------------------------------- /GUI/README.md: -------------------------------------------------------------------------------- 1 | # Web GUI per ricerca e download 2 | 3 | ## Comandi rapidi: 4 | - Installazione deps: `pip install -r .\GUI\requirements.txt` 5 | - Esecuzione migrazioni: `python GUI/manage.py migrate` 6 | - Avvio dev: `python GUI/manage.py runserver 0.0.0.0:8000` 7 | 8 | ## Problemi di CSRF 9 | - In caso di problemi quando si effettua una ricerca al di fuori della propria rete (magari sotto un reverse proxy) impostare la variabile d'ambiente `CSRF_TRUSTED_ORIGINS` 10 | > Esempio: `CSRF_TRUSTED_ORIGINS="http://127.0.0.1:8000 https://altrodominio.it"` -------------------------------------------------------------------------------- /GUI/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | 6 | # Fix PYTHONPATH 7 | current_dir = os.path.dirname(os.path.abspath(__file__)) 8 | parent_dir = os.path.dirname(current_dir) 9 | if parent_dir not in sys.path: 10 | sys.path.insert(0, parent_dir) 11 | 12 | 13 | def main(): 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webgui.settings") 15 | from django.core.management import execute_from_command_line 16 | 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() -------------------------------------------------------------------------------- /StreamingCommunity/__init__.py: -------------------------------------------------------------------------------- 1 | # 11.03.25 2 | 3 | from .run import main 4 | from .Lib.Downloader.HLS.downloader import HLS_Downloader 5 | from .Lib.Downloader.MP4.downloader import MP4_downloader 6 | from .Lib.Downloader.TOR.downloader import TOR_downloader 7 | from .Lib.Downloader.DASH.downloader import DASH_Downloader 8 | from .Lib.Downloader.MEGA.mega import Mega_Downloader 9 | 10 | __all__ = [ 11 | "main", 12 | "HLS_Downloader", 13 | "MP4_downloader", 14 | "TOR_downloader", 15 | "DASH_Downloader", 16 | "Mega_Downloader", 17 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Api/Template/Util/__init__.py: -------------------------------------------------------------------------------- 1 | # 23.11.24 2 | 3 | from .manage_ep import ( 4 | manage_selection, 5 | map_episode_title, 6 | validate_episode_selection, 7 | validate_selection, 8 | dynamic_format_number, 9 | display_episodes_list, 10 | display_seasons_list 11 | ) 12 | 13 | __all__ = [ 14 | "manage_selection", 15 | "map_episode_title", 16 | "validate_episode_selection", 17 | "validate_selection", 18 | "dynamic_format_number", 19 | "display_episodes_list", 20 | display_seasons_list 21 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Util/color.py: -------------------------------------------------------------------------------- 1 | # 24.05.24 2 | 3 | class Colors: 4 | BLACK = "\033[30m" 5 | RED = "\033[31m" 6 | GREEN = "\033[32m" 7 | YELLOW = "\033[33m" 8 | BLUE = "\033[34m" 9 | MAGENTA = "\033[35m" 10 | CYAN = "\033[36m" 11 | LIGHT_GRAY = "\033[37m" 12 | DARK_GRAY = "\033[90m" 13 | LIGHT_RED = "\033[91m" 14 | LIGHT_GREEN = "\033[92m" 15 | LIGHT_YELLOW = "\033[93m" 16 | LIGHT_BLUE = "\033[94m" 17 | LIGHT_MAGENTA = "\033[95m" 18 | LIGHT_CYAN = "\033[96m" 19 | WHITE = "\033[97m" 20 | RESET = "\033[0m" -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ main, master, develop ] 6 | pull_request: 7 | branches: [ main, master, develop ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | ruff-lint: 12 | name: Lint with Ruff 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: '3.11' 20 | - name: Install Ruff 21 | run: | 22 | python -m pip install ruff 23 | - name: Run Ruff check 24 | run: | 25 | ruff check . --fix -------------------------------------------------------------------------------- /Test/Downloads/MEGA.py: -------------------------------------------------------------------------------- 1 | # 25-06-2020 2 | # ruff: noqa: E402 3 | 4 | import os 5 | import sys 6 | 7 | 8 | # Fix import 9 | src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 10 | sys.path.append(src_path) 11 | 12 | 13 | from StreamingCommunity.Util.message import start_message 14 | from StreamingCommunity.Util.logger import Logger 15 | from StreamingCommunity import Mega_Downloader 16 | 17 | 18 | start_message() 19 | Logger() 20 | mega = Mega_Downloader() 21 | m = mega.login() 22 | 23 | output_path = m.download_url( 24 | url="https://mega.nz/file/0kgCWZZB#7u....", 25 | dest_path=".\\prova.mp4" 26 | ) -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | RUN apt-get update && apt-get install -y --no-install-recommends \ 4 | ffmpeg \ 5 | build-essential \ 6 | libxml2-dev \ 7 | libxslt1-dev \ 8 | && apt-get clean \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /app 12 | 13 | COPY requirements.txt ./ 14 | RUN pip install --no-cache-dir -r requirements.txt 15 | 16 | COPY GUI/requirements.txt ./GUI/requirements.txt 17 | RUN pip install --no-cache-dir -r GUI/requirements.txt 18 | 19 | COPY . . 20 | 21 | ENV PYTHONPATH="/app:${PYTHONPATH}" 22 | 23 | EXPOSE 8000 24 | 25 | CMD ["python", "GUI/manage.py", "runserver", "0.0.0.0:8000"] -------------------------------------------------------------------------------- /Test/Downloads/MP4.py: -------------------------------------------------------------------------------- 1 | # 23.06.24 2 | # ruff: noqa: E402 3 | 4 | import os 5 | import sys 6 | 7 | 8 | # Fix import 9 | src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 10 | sys.path.append(src_path) 11 | 12 | 13 | from StreamingCommunity.Util.message import start_message 14 | from StreamingCommunity.Util.logger import Logger 15 | from StreamingCommunity import MP4_downloader 16 | 17 | 18 | start_message() 19 | Logger() 20 | path, kill_handler = MP4_downloader( 21 | url="https://148-251-75-109.top/Getintopc.com/IDA_Pro_2020.mp4", 22 | path=r".\\Video\\undefined.mp4" 23 | ) 24 | 25 | thereIsError = path is None 26 | print(thereIsError) -------------------------------------------------------------------------------- /test_run.py: -------------------------------------------------------------------------------- 1 | # 26.11.24 2 | 3 | import sys 4 | 5 | 6 | # Internal utilities 7 | from StreamingCommunity.run import main 8 | from StreamingCommunity.Util.config_json import config_manager 9 | from StreamingCommunity.TelegramHelp.telegram_bot import TelegramRequestManager, TelegramSession 10 | 11 | 12 | # Variable 13 | TELEGRAM_BOT = config_manager.get_bool('DEFAULT', 'telegram_bot') 14 | 15 | 16 | if TELEGRAM_BOT: 17 | request_manager = TelegramRequestManager() 18 | request_manager.clear_file() 19 | script_id = sys.argv[1] if len(sys.argv) > 1 else "unknown" 20 | 21 | TelegramSession.set_session(script_id) 22 | main(script_id) 23 | 24 | else: 25 | main() -------------------------------------------------------------------------------- /.github/.site/login.md: -------------------------------------------------------------------------------- 1 | # How to Extract Login Keys 2 | 3 | Follow the instructions below to obtain the required keys for each streaming service and add them to your `config.json`. 4 | 5 | ## Crunchyroll: Get `etp_rt` and `device_id` 6 | 7 | 1. **Log in** to [Crunchyroll](https://www.crunchyroll.com/). 8 | 9 | 2. **Open Developer Tools** (F12). 10 | 11 | 3. **Get `etp_rt` and `device_id`:** 12 | - Go to the **Application** tab. 13 | - Find the `etp_rt` and `device_id` cookies under **Cookies** for the site (you can use the filter/search field and search for `etp_rt` or `device_id`). 14 | - **Copy** their values for `config.json`. 15 | - ![etp_rt location](./img/crunchyroll_etp_rt.png) -------------------------------------------------------------------------------- /Test/Downloads/HLS.py: -------------------------------------------------------------------------------- 1 | # 23.06.24 2 | # ruff: noqa: E402 3 | 4 | import os 5 | import sys 6 | 7 | 8 | # Fix import 9 | src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 10 | sys.path.append(src_path) 11 | 12 | 13 | from StreamingCommunity.Util.message import start_message 14 | from StreamingCommunity.Util.logger import Logger 15 | from StreamingCommunity import HLS_Downloader 16 | 17 | 18 | start_message() 19 | Logger() 20 | hls_process = HLS_Downloader( 21 | output_path=".\\Video\\test.mp4", 22 | m3u8_url="https://acdn.ak-stream-videoplatform.sky.it/hls/2024/11/21/968275/master.m3u8" 23 | ).start() 24 | 25 | thereIsError = hls_process['error'] is not None 26 | print(thereIsError) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Distribution / packaging 7 | .Python 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | eggs/ 12 | .eggs/ 13 | parts/ 14 | sdist/ 15 | var/ 16 | wheels/ 17 | share/python-wheels/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | 22 | # PyInstaller 23 | *.manifest 24 | *.spec 25 | 26 | # Installer logs 27 | pip-log.txt 28 | pip-delete-this-directory.txt 29 | 30 | # Translations 31 | *.mo 32 | *.pot 33 | 34 | # Environments 35 | .env 36 | .venv 37 | env/ 38 | venv/ 39 | ENV/ 40 | env.bak/ 41 | venv.bak/ 42 | downloaded_files/ 43 | 44 | # IDEs 45 | .idea 46 | .vscode 47 | .ruff_cache 48 | 49 | # Other 50 | Video 51 | note.txt 52 | cmd.txt 53 | bot_config.json 54 | scripts.json 55 | active_requests.json 56 | working_proxies.json 57 | start.sh 58 | .DS_Store 59 | GUI/db.sqlite3 60 | -------------------------------------------------------------------------------- /Test/Downloads/DASH.py: -------------------------------------------------------------------------------- 1 | # 29.07.25 2 | # ruff: noqa: E402 3 | 4 | import os 5 | import sys 6 | 7 | 8 | # Fix import 9 | src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 10 | sys.path.append(src_path) 11 | 12 | 13 | from StreamingCommunity.Util.message import start_message 14 | from StreamingCommunity.Util.logger import Logger 15 | from StreamingCommunity import DASH_Downloader 16 | 17 | 18 | start_message() 19 | logger = Logger() 20 | 21 | 22 | mpd_url = "" 23 | mpd_headers = {} 24 | license_url = "" 25 | license_params = {} 26 | license_headers = {} 27 | 28 | dash_process = DASH_Downloader( 29 | license_url=license_url, 30 | mpd_url=mpd_url, 31 | output_path="out.mp4", 32 | ) 33 | dash_process.parse_manifest(custom_headers=mpd_headers) 34 | 35 | if dash_process.download_and_decrypt(custom_headers=license_headers, query_params=license_params): 36 | dash_process.finalize_output() 37 | 38 | status = dash_process.get_status() 39 | print(status) -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/raiplay/util/get_license.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | 4 | # Internal utilities 5 | from StreamingCommunity.Util.http_client import create_client 6 | from StreamingCommunity.Util.headers import get_headers 7 | 8 | 9 | def generate_license_url(mpd_id: str): 10 | """ 11 | Generates the URL to obtain the Widevine license. 12 | 13 | Args: 14 | mpd_id (str): The ID of the MPD (Media Presentation Description) file. 15 | 16 | Returns: 17 | str: The full license URL. 18 | """ 19 | params = { 20 | 'cont': mpd_id, 21 | 'output': '62', 22 | } 23 | 24 | response = create_client(headers=get_headers()).get('https://mediapolisvod.rai.it/relinker/relinkerServlet.htm', params=params) 25 | response.raise_for_status() 26 | 27 | # Extract the license URL from the response in two lines 28 | json_data = response.json() 29 | license_url = json_data.get('licence_server_map').get('drmLicenseUrlValues')[0].get('licenceUrl') 30 | 31 | return license_url -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: ["main"] 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | 11 | concurrency: 12 | group: "pages" 13 | cancel-in-progress: false 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Pages 23 | uses: actions/configure-pages@v5 24 | 25 | - name: Copy site files 26 | run: | 27 | mkdir -p _site 28 | cp -r .github/.site/* _site/ 29 | ls -la _site/ 30 | 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v3 33 | with: 34 | path: _site 35 | 36 | deploy: 37 | environment: 38 | name: github-pages 39 | url: ${{ steps.deployment.outputs.page_url }} 40 | runs-on: ubuntu-latest 41 | needs: build 42 | steps: 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/.domain/domains.json: -------------------------------------------------------------------------------- 1 | { 2 | "cb01new": { 3 | "domain": "shop", 4 | "full_url": "https://cb01net.shop/", 5 | "old_domain": "homes", 6 | "time_change": "2025-12-15 19:20:30" 7 | }, 8 | "animeunity": { 9 | "domain": "so", 10 | "full_url": "https://www.animeunity.so/", 11 | "old_domain": "so", 12 | "time_change": "2025-07-08 13:59:31" 13 | }, 14 | "animeworld": { 15 | "domain": "ac", 16 | "full_url": "https://www.animeworld.ac/", 17 | "old_domain": "ac", 18 | "time_change": "2025-03-21 12:20:27" 19 | }, 20 | "guardaserie": { 21 | "domain": "asia", 22 | "full_url": "https://guardaserietv.asia/", 23 | "old_domain": "club", 24 | "time_change": "2025-12-11 19:21:07" 25 | }, 26 | "altadefinizione": { 27 | "domain": "bond", 28 | "full_url": "https://altadefinizionegratis.bond/", 29 | "old_domain": "cfd", 30 | "time_change": "2025-12-17 07:23:58" 31 | }, 32 | "streamingcommunity": { 33 | "domain": "fun", 34 | "full_url": "https://streamingcommunityz.fun/", 35 | "old_domain": "cam", 36 | "time_change": "2025-12-18 15:25:18" 37 | } 38 | } -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/animeunity/film.py: -------------------------------------------------------------------------------- 1 | # 11.03.24 2 | 3 | # External library 4 | from rich.console import Console 5 | 6 | 7 | # Logic class 8 | from .serie import download_episode 9 | from .util.ScrapeSerie import ScrapeSerieAnime 10 | from StreamingCommunity.Api.Template.config_loader import site_constant 11 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 12 | 13 | 14 | # Player 15 | from StreamingCommunity.Api.Player.vixcloud import VideoSourceAnime 16 | 17 | 18 | # Variable 19 | console = Console() 20 | 21 | 22 | def download_film(select_title: MediaItem): 23 | """ 24 | Function to download a film. 25 | 26 | Parameters: 27 | - id_film (int): The ID of the film. 28 | - title_name (str): The title of the film. 29 | """ 30 | 31 | # Init class 32 | scrape_serie = ScrapeSerieAnime(site_constant.FULL_URL) 33 | video_source = VideoSourceAnime(site_constant.FULL_URL) 34 | 35 | # Set up video source (only configure scrape_serie now) 36 | scrape_serie.setup(None, select_title.id, select_title.slug) 37 | scrape_serie.is_series = False 38 | 39 | # Start download 40 | download_episode(0, scrape_serie, video_source) -------------------------------------------------------------------------------- /StreamingCommunity/Util/message.py: -------------------------------------------------------------------------------- 1 | # 3.12.23 2 | 3 | import os 4 | import platform 5 | 6 | 7 | # External library 8 | from rich.console import Console 9 | 10 | 11 | # Internal utilities 12 | from StreamingCommunity.Util.config_json import config_manager 13 | 14 | 15 | # Variable 16 | console = Console() 17 | CLEAN = config_manager.get_bool('DEFAULT', 'show_message') 18 | SHOW = config_manager.get_bool('DEFAULT', 'show_message') 19 | 20 | 21 | def start_message(): 22 | """Display a stylized start message in the console.""" 23 | 24 | msg = r''' 25 | ___ ______ _ 26 | / _ | ___________ _ _____ _____ __ __ / __/ /________ ___ ___ _ (_)__ ___ _ 27 | / __ |/ __/ __/ _ \ |/|/ / _ `/ __/ \ \ / _\ \/ __/ __/ -_) _ `/ ' \/ / _ \/ _ `/ 28 | /_/ |_/_/ /_/ \___/__,__/\_,_/_/ /_\_\ /___/\__/_/ \__/\_,_/_/_/_/_/_//_/\_, / 29 | /___/ 30 | '''.rstrip() 31 | 32 | if CLEAN: 33 | os.system("cls" if platform.system() == 'Windows' else "clear") 34 | 35 | if SHOW: 36 | console.print(f"[purple]{msg}") -------------------------------------------------------------------------------- /StreamingCommunity/Lib/TMBD/obj_tmbd.py: -------------------------------------------------------------------------------- 1 | # 17.09.24 2 | 3 | from typing import Dict 4 | 5 | 6 | class Json_film: 7 | def __init__(self, data: Dict): 8 | self.id = data.get('id', 0) 9 | self.imdb_id = data.get('imdb_id') 10 | self.origin_country = data.get('origin_country', []) 11 | self.original_language = data.get('original_language') 12 | self.original_title = data.get('original_title') 13 | self.popularity = data.get('popularity', 0.0) 14 | self.poster_path = data.get('poster_path') 15 | self.release_date = data.get('release_date') 16 | self.status = data.get('status') 17 | self.title = data.get('title') 18 | self.vote_average = data.get('vote_average', 0.0) 19 | self.vote_count = data.get('vote_count', 0) 20 | 21 | def __repr__(self): 22 | return (f"Json_film(id={self.id}, imdb_id='{self.imdb_id}', origin_country={self.origin_country}, " 23 | f"original_language='{self.original_language}', original_title='{self.original_title}', " 24 | f"popularity={self.popularity}, poster_path='{self.poster_path}', release_date='{self.release_date}', " 25 | f"status='{self.status}', title='{self.title}', vote_average={self.vote_average}, vote_count={self.vote_count})") -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | packages: write 11 | 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository }} 15 | 16 | jobs: 17 | build-and-push: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | 26 | - name: Log in to GHCR 27 | uses: docker/login-action@v3 28 | with: 29 | registry: ${{ env.REGISTRY }} 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | - name: Extract metadata (tags, labels) 34 | id: meta 35 | uses: docker/metadata-action@v5 36 | with: 37 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 38 | tags: | 39 | type=raw,value=latest,enable={{is_default_branch}} 40 | type=sha,format=short 41 | 42 | - name: Build and push Docker image 43 | uses: docker/build-push-action@v6 44 | with: 45 | context: . 46 | platforms: linux/amd64,linux/arm64 47 | push: true 48 | tags: ${{ steps.meta.outputs.tags }} 49 | labels: ${{ steps.meta.outputs.labels }} 50 | provenance: false 51 | -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/plutotv/util/get_license.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | import uuid 4 | import random 5 | 6 | 7 | # Internal utilities 8 | from StreamingCommunity.Util.headers import get_headers 9 | from StreamingCommunity.Util.http_client import create_client 10 | 11 | 12 | def generate_params(): 13 | """Generate all params automatically""" 14 | device_makes = ['opera', 'chrome', 'firefox', 'safari', 'edge'] 15 | 16 | return { 17 | 'appName': 'web', 18 | 'appVersion': str(random.randint(100, 999)), 19 | 'deviceVersion': str(random.randint(100, 999)), 20 | 'deviceModel': 'web', 21 | 'deviceMake': random.choice(device_makes), 22 | 'deviceType': 'web', 23 | 'clientID': str(uuid.uuid4()), 24 | 'clientModelNumber': f"{random.randint(1, 9)}.{random.randint(0, 9)}.{random.randint(0, 9)}", 25 | 'channelID': ''.join(random.choice('0123456789abcdef') for _ in range(24)) 26 | } 27 | 28 | def get_bearer_token(): 29 | """ 30 | Get the Bearer token required for authentication. 31 | 32 | Returns: 33 | str: Token Bearer 34 | """ 35 | response = create_client(headers=get_headers()).get('https://boot.pluto.tv/v4/start', params=generate_params()) 36 | return response.json()['sessionToken'] 37 | 38 | 39 | def get_playback_url_episode(id_episode): 40 | return f"https://cfd-v4-service-stitcher-dash-use1-1.prd.pluto.tv/v2/stitch/dash/episode/{id_episode}/main.mpd" -------------------------------------------------------------------------------- /Test/Downloads/TOR.py: -------------------------------------------------------------------------------- 1 | # 23.06.24 2 | # ruff: noqa: E402 3 | 4 | import os 5 | import sys 6 | 7 | 8 | # Fix import 9 | src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')) 10 | sys.path.append(src_path) 11 | 12 | 13 | from StreamingCommunity.Util.message import start_message 14 | from StreamingCommunity.Util.logger import Logger 15 | from StreamingCommunity import TOR_downloader 16 | 17 | 18 | # Test 19 | start_message() 20 | Logger() 21 | manager = TOR_downloader() 22 | 23 | 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""" 24 | manager.add_magnet_link(magnet_link, save_path=os.path.join(src_path, "Video")) 25 | manager.start_download() -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug you encountered 4 | title: "[BUG] Brief description of the issue" 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Checklist before submitting 10 | Please make sure to check the following: 11 | 12 | - [ ] You are using the latest version of the project/repository. 13 | - [ ] You have the latest commit installed. 14 | - [ ] The issue relates to a website or a specific functionality. 15 | - [ ] If the issue is related to a website, you have verified that the URL works correctly in your browser. 16 | - [ ] You have searched through closed issues for similar problems or potential solutions. Issues that can be resolved by already closed topics may be automatically closed. 17 | 18 | ## Describe the issue 19 | Provide a clear and detailed description of the problem. 20 | 21 | ## Expected behavior 22 | Explain what you expected to happen instead of the observed behavior. 23 | 24 | ## Steps to reproduce 25 | List the steps to reproduce the issue, if applicable: 26 | 1. 27 | 2. 28 | 3. 29 | 30 | ## Screenshots 31 | If applicable, attach screenshots that illustrate the issue. 32 | 33 | ## Priority / Severity 34 | Indicate the severity of the issue (e.g., Low, Medium, High, Critical). 35 | 36 | ## Logs / Console Output 37 | If applicable, paste relevant logs or console errors here. 38 | 39 | ## Related issues 40 | Provide links to any related or similar issues. 41 | 42 | ## Environment (please complete the following information): 43 | - OS: [e.g., Windows, macOS, Linux] 44 | 45 | ## Additional context 46 | Add any other relevant information that could help in diagnosing the problem. -------------------------------------------------------------------------------- /StreamingCommunity/Api/Player/sweetpixel.py: -------------------------------------------------------------------------------- 1 | # 21.03.25 2 | 3 | import logging 4 | 5 | 6 | # Internal utilities 7 | from StreamingCommunity.Util.headers import get_userAgent 8 | from StreamingCommunity.Util.http_client import create_client 9 | 10 | 11 | class VideoSource: 12 | def __init__(self, site_url, episode_data, session_id, csrf_token): 13 | """Initialize the VideoSource with session details, episode data, and URL.""" 14 | self.session_id = session_id 15 | self.csrf_token = csrf_token 16 | self.episode_data = episode_data 17 | self.number = episode_data['number'] 18 | self.link = site_url + episode_data['link'] 19 | 20 | # Create an HTTP client with session cookies, headers, and base URL. 21 | self.client = create_client( 22 | cookies={"sessionId": session_id}, 23 | headers={"User-Agent": get_userAgent(), "csrf-token": csrf_token} 24 | ) 25 | 26 | def get_playlist(self): 27 | """Fetch the download link from AnimeWorld using the episode link.""" 28 | try: 29 | # Make a POST request to the episode link and follow any redirects 30 | res = self.client.post(self.link, follow_redirects=True) 31 | data = res.json() 32 | 33 | # Extract the first available server link and return it after modifying the URL 34 | server_link = data["links"]["9"][list(data["links"]["9"].keys())[0]]["link"] 35 | return server_link.replace('download-file.php?id=', '') 36 | 37 | except Exception as e: 38 | logging.error(f"Error in new API system: {e}") 39 | return None -------------------------------------------------------------------------------- /GUI/searchapp/forms.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | from django import forms 5 | from GUI.searchapp.api import get_available_sites 6 | 7 | 8 | def get_site_choices(): 9 | sites = get_available_sites() 10 | return [(site, site.replace('_', ' ').title()) for site in sites] 11 | 12 | 13 | class SearchForm(forms.Form): 14 | site = forms.ChoiceField( 15 | label="Sito", 16 | widget=forms.Select( 17 | attrs={ 18 | "class": "block w-full appearance-none rounded-lg border border-gray-300 bg-white py-3 pl-12 pr-12 text-gray-900 placeholder-gray-500 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500", 19 | } 20 | ), 21 | ) 22 | query = forms.CharField( 23 | max_length=200, 24 | label="Cosa cerchi?", 25 | widget=forms.TextInput( 26 | attrs={ 27 | "class": "block w-full rounded-lg border border-gray-300 bg-white py-3 pl-12 pr-12 text-gray-900 placeholder-gray-500 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500", 28 | "placeholder": "Cerca titolo...", 29 | "autocomplete": "off", 30 | } 31 | ), 32 | ) 33 | 34 | def __init__(self, *args, **kwargs): 35 | super().__init__(*args, **kwargs) 36 | self.fields['site'].choices = get_site_choices() 37 | 38 | 39 | class DownloadForm(forms.Form): 40 | source_alias = forms.CharField(widget=forms.HiddenInput) 41 | item_payload = forms.CharField(widget=forms.HiddenInput) 42 | season = forms.CharField(max_length=10, required=False, label="Stagione") 43 | episode = forms.CharField(max_length=20, required=False, label="Episodio (es: 1-3)") -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from setuptools import setup, find_packages 4 | 5 | def read_readme(): 6 | with open("README.md", "r", encoding="utf-8") as fh: 7 | return fh.read() 8 | 9 | with open(os.path.join(os.path.dirname(__file__), "requirements.txt"), "r", encoding="utf-8-sig") as f: 10 | required_packages = f.read().splitlines() 11 | 12 | def get_version(): 13 | try: 14 | import pkg_resources 15 | return pkg_resources.get_distribution('StreamingCommunity').version 16 | 17 | except Exception: 18 | version_file_path = os.path.join(os.path.dirname(__file__), "StreamingCommunity", "Upload", "version.py") 19 | with open(version_file_path, "r", encoding="utf-8") as f: 20 | version_match = re.search(r"^__version__\s*=\s*['\"]([^'\"]*)['\"]", f.read(), re.M) 21 | if version_match: 22 | return version_match.group(1) 23 | raise RuntimeError("Unable to find version string in StreamingCommunity/Upload/version.py.") 24 | 25 | setup( 26 | name="StreamingCommunity", 27 | version=get_version(), 28 | long_description=read_readme(), 29 | long_description_content_type="text/markdown", 30 | author="Arrowar", 31 | url="https://github.com/Arrowar/StreamingCommunity", 32 | packages=find_packages(include=["StreamingCommunity", "StreamingCommunity.*"]), 33 | install_requires=required_packages, 34 | python_requires='>=3.8', 35 | entry_points={ 36 | "console_scripts": [ 37 | "streamingcommunity=StreamingCommunity.run:main", 38 | ], 39 | }, 40 | include_package_data=True, 41 | keywords="streaming community", 42 | project_urls={ 43 | "Bug Reports": "https://github.com/Arrowar/StreamingCommunity/issues", 44 | "Source": "https://github.com/Arrowar/StreamingCommunity", 45 | } 46 | ) -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "DEFAULT": { 3 | "debug": false, 4 | "show_message": true, 5 | "fetch_domain_online": true, 6 | "telegram_bot": false 7 | }, 8 | "OUT_FOLDER": { 9 | "root_path": "Video", 10 | "movie_folder_name": "Movie", 11 | "serie_folder_name": "Serie", 12 | "anime_folder_name": "Anime", 13 | "map_episode_name": "%(episode_name) S%(season)E%(episode)", 14 | "add_siteName": false 15 | }, 16 | "QBIT_CONFIG": { 17 | "host": "192.168.1.1", 18 | "port": "5555", 19 | "user": "root", 20 | "pass": "toor" 21 | }, 22 | "M3U8_DOWNLOAD": { 23 | "default_video_workers": 8, 24 | "default_audio_workers": 8, 25 | "segment_timeout": 6, 26 | "enable_retry": true, 27 | "specific_list_audio": [ 28 | "ita" 29 | ], 30 | "merge_subs": true, 31 | "specific_list_subtitles": [ 32 | "ita", 33 | "eng" 34 | ], 35 | "limit_segment": 0, 36 | "cleanup_tmp_folder": true, 37 | "get_only_link": false 38 | }, 39 | "M3U8_CONVERSION": { 40 | "use_gpu": false, 41 | "param_video": ["-c:v", "libx265", "-crf", "28", "-preset", "medium"], 42 | "param_audio": ["-c:a", "libopus", "-b:a", "128k"], 43 | "param_subtitles": ["-c:s", "webvtt"], 44 | "param_final": ["-c", "copy"], 45 | "force_resolution": "Best", 46 | "extension": "mkv" 47 | }, 48 | "REQUESTS": { 49 | "verify": false, 50 | "timeout": 12, 51 | "max_retry": 8 52 | }, 53 | "HOOKS": { 54 | "pre_run": [], 55 | "post_run": [] 56 | }, 57 | "SITE_LOGIN": { 58 | "crunchyroll": { 59 | "device_id": "", 60 | "etp_rt": "" 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/realtime/util/get_license.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | 4 | # Internal utilities 5 | from StreamingCommunity.Util.headers import get_userAgent, get_headers 6 | from StreamingCommunity.Util.http_client import create_client 7 | 8 | 9 | 10 | def get_playback_url(video_id: str, bearer_token: str, get_dash: bool, channel: str = "") -> str: 11 | """ 12 | Get the playback URL (HLS or DASH) for a given video ID. 13 | 14 | Parameters: 15 | - video_id (str): ID of the video. 16 | """ 17 | headers = { 18 | 'authorization': f"Bearer {bearer_token[channel]['key']}", 19 | 'user-agent': get_userAgent() 20 | } 21 | 22 | json_data = { 23 | 'deviceInfo': { 24 | "adBlocker": False, 25 | "drmSupported": True 26 | }, 27 | 'videoId': video_id, 28 | } 29 | 30 | response = create_client().post(bearer_token[channel]['endpoint'], headers=headers, json=json_data) 31 | 32 | if not get_dash: 33 | return response.json()['data']['attributes']['streaming'][0]['url'] 34 | else: 35 | return response.json()['data']['attributes']['streaming'][1]['url'] 36 | 37 | 38 | def get_bearer_token(): 39 | """ 40 | Get the Bearer token required for authentication. 41 | 42 | Returns: 43 | str: Token Bearer 44 | """ 45 | response = create_client(headers=get_headers()).get('https://public.aurora.enhanced.live/site/page/homepage/?include=default&filter[environment]=realtime&v=2') 46 | return { 47 | 'X-REALM-IT': { 48 | 'endpoint': 'https://public.aurora.enhanced.live/playback/v3/videoPlaybackInfo', 49 | 'key': response.json()['userMeta']['realm']['X-REALM-IT'] 50 | }, 51 | 'X-REALM-DPLAY': { 52 | 'endpoint': 'https://eu1-prod.disco-api.com/playback/v3/videoPlaybackInfo', 53 | 'key': response.json()['userMeta']['realm']['X-REALM-DPLAY'] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /StreamingCommunity/Lib/M3U8/url_fixer.py: -------------------------------------------------------------------------------- 1 | # 20.03.24 2 | 3 | import logging 4 | from urllib.parse import urlparse, urljoin 5 | 6 | 7 | class M3U8_UrlFix: 8 | def __init__(self, url: str = None) -> None: 9 | """ 10 | Initializes an M3U8_UrlFix object with the provided playlist URL. 11 | 12 | Parameters: 13 | - url (str, optional): The URL of the playlist. Defaults to None. 14 | """ 15 | self.url_playlist: str = url 16 | 17 | def set_playlist(self, url: str) -> None: 18 | """ 19 | Set the M3U8 playlist URL. 20 | 21 | Parameters: 22 | - url (str): The M3U8 playlist URL. 23 | """ 24 | self.url_playlist = url 25 | 26 | def generate_full_url(self, url_resource: str) -> str: 27 | """ 28 | Generate a full URL for a given resource using the base URL from the playlist. 29 | 30 | Parameters: 31 | - url_resource (str): The relative URL of the resource within the playlist. 32 | 33 | Returns: 34 | str: The full URL for the specified resource. 35 | """ 36 | if self.url_playlist is None: 37 | logging.error("[M3U8_UrlFix] Cant generate full url, playlist not present") 38 | raise 39 | 40 | # Parse the playlist URL to extract the base URL components 41 | parsed_playlist_url = urlparse(self.url_playlist) 42 | 43 | # Construct the base URL using the scheme, netloc, and path from the playlist URL 44 | base_url = f"{parsed_playlist_url.scheme}://{parsed_playlist_url.netloc}{parsed_playlist_url.path}" 45 | 46 | # Join the base URL with the relative resource URL to get the full URL 47 | full_url = urljoin(base_url, url_resource) 48 | 49 | return full_url 50 | 51 | def reset_playlist(self) -> None: 52 | """ 53 | Reset the M3U8 playlist URL to its default state (None). 54 | """ 55 | self.url_playlist = None -------------------------------------------------------------------------------- /GUI/searchapp/api/__init__.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | from typing import Dict, Type 5 | 6 | 7 | # Internal utilities 8 | from .base import BaseStreamingAPI 9 | from .streamingcommunity import StreamingCommunityAPI 10 | from .animeunity import AnimeUnityAPI 11 | 12 | 13 | _API_REGISTRY: Dict[str, Type[BaseStreamingAPI]] = { 14 | 'streamingcommunity': StreamingCommunityAPI, 15 | 'animeunity': AnimeUnityAPI, 16 | } 17 | 18 | 19 | def get_api(site_name: str) -> BaseStreamingAPI: 20 | """ 21 | Get API instance for a specific site. 22 | 23 | Args: 24 | site_name: Name of the streaming site 25 | 26 | Returns: 27 | Instance of the appropriate API class 28 | """ 29 | site_key = site_name.lower().split('_')[0] 30 | 31 | if site_key not in _API_REGISTRY: 32 | raise ValueError( 33 | f"Unsupported site: {site_name}. " 34 | f"Available sites: {', '.join(_API_REGISTRY.keys())}" 35 | ) 36 | 37 | api_class = _API_REGISTRY[site_key] 38 | return api_class() 39 | 40 | 41 | def get_available_sites() -> list: 42 | """ 43 | Get list of available streaming sites. 44 | 45 | Returns: 46 | List of site names 47 | """ 48 | return list(_API_REGISTRY.keys()) 49 | 50 | 51 | def register_api(site_name: str, api_class: Type[BaseStreamingAPI]): 52 | """ 53 | Register a new API class. 54 | 55 | Args: 56 | site_name: Name of the site 57 | api_class: API class that inherits from BaseStreamingAPI 58 | """ 59 | if not issubclass(api_class, BaseStreamingAPI): 60 | raise ValueError(f"{api_class} must inherit from BaseStreamingAPI") 61 | 62 | _API_REGISTRY[site_name.lower()] = api_class 63 | 64 | 65 | __all__ = [ 66 | 'BaseStreamingAPI', 67 | 'StreamingCommunityAPI', 68 | 'AnimeUnityAPI', 69 | 'get_api', 70 | 'get_available_sites', 71 | 'register_api' 72 | ] -------------------------------------------------------------------------------- /StreamingCommunity/Api/Player/hdplayer.py: -------------------------------------------------------------------------------- 1 | # 29.04.25 2 | 3 | import re 4 | 5 | # External library 6 | from bs4 import BeautifulSoup 7 | 8 | 9 | # Internal utilities 10 | from StreamingCommunity.Util.headers import get_userAgent 11 | from StreamingCommunity.Util.http_client import create_client 12 | 13 | 14 | 15 | class VideoSource: 16 | def __init__(self): 17 | self.client = create_client(headers={'user-agent': get_userAgent()}) 18 | 19 | def extractLinkHdPlayer(self, response): 20 | """Extract iframe source from the page.""" 21 | soup = BeautifulSoup(response.content, 'html.parser') 22 | iframes = soup.find_all("iframe") 23 | if iframes: 24 | return iframes[0].get('data-lazy-src') 25 | return None 26 | 27 | def get_m3u8_url(self, page_url): 28 | """ 29 | Extract m3u8 URL from hdPlayer page. 30 | """ 31 | try: 32 | base_domain = re.match(r'https?://(?:www\.)?([^/]+)', page_url).group(0) 33 | self.client.headers.update({'referer': base_domain}) 34 | 35 | # Get the page content 36 | response = self.client.get(page_url) 37 | 38 | # Extract HDPlayer iframe URL 39 | iframe_url = self.extractLinkHdPlayer(response) 40 | if not iframe_url: 41 | return None 42 | 43 | # Get HDPlayer page content 44 | response_hdplayer = self.client.get(iframe_url) 45 | if response_hdplayer.status_code != 200: 46 | return None 47 | 48 | sources_pattern = r'file:"([^"]+)"' 49 | match = re.search(sources_pattern, response_hdplayer.text) 50 | 51 | if match: 52 | return match.group(1) 53 | 54 | return None 55 | 56 | except Exception as e: 57 | print(f"Error in HDPlayer: {str(e)}") 58 | return None 59 | 60 | finally: 61 | self.client.close() 62 | -------------------------------------------------------------------------------- /.github/workflows/update_domain.yml: -------------------------------------------------------------------------------- 1 | name: Update domains (Amend Strategy) 2 | on: 3 | schedule: 4 | - cron: "0 7-21 * * *" 5 | workflow_dispatch: 6 | 7 | jobs: 8 | update-domains: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: write 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 # Serve per l'amend 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Setup Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.12' 25 | 26 | - name: Install dependencies 27 | run: | 28 | pip install httpx tldextract ua-generator dnspython 29 | pip install --upgrade pip setuptools wheel 30 | 31 | - name: Configure DNS 32 | run: | 33 | sudo sh -c 'echo "nameserver 208.67.220.220" > /etc/resolv.conf' 34 | sudo sh -c 'echo "nameserver 208.67.222.222" >> /etc/resolv.conf' 35 | sudo sh -c 'echo "nameserver 77.88.8.8" >> /etc/resolv.conf' 36 | 37 | - name: Execute domain update script 38 | run: python .github/.domain/domain_update.py 39 | 40 | - name: Always amend last commit 41 | run: | 42 | git config --global user.name 'github-actions[bot]' 43 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 44 | 45 | if ! git diff --quiet .github/.domain/domains.json; then 46 | echo "📝 Changes detected - amending last commit" 47 | git add .github/.domain/domains.json 48 | git commit --amend --no-edit 49 | git push --force-with-lease origin main 50 | else 51 | echo "✅ No changes to domains.json" 52 | fi 53 | 54 | - name: Verify repository state 55 | if: failure() 56 | run: | 57 | echo "❌ Something went wrong. Repository state:" 58 | git log --oneline -5 59 | git status -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/mediasetinfinity/util/fix_mpd.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | from urllib.parse import urlparse, urlunparse 4 | 5 | 6 | # Internal utilities 7 | from StreamingCommunity.Util.http_client import create_client 8 | 9 | 10 | def try_mpd(url, qualities): 11 | """ 12 | Given a url containing one of the qualities (hd/hr/sd), try to replace it with the others and check which manifest exists. 13 | """ 14 | parsed = urlparse(url) 15 | path_parts = parsed.path.rsplit('/', 1) 16 | 17 | if len(path_parts) != 2: 18 | return None 19 | 20 | dir_path, filename = path_parts 21 | 22 | # Find the current quality in the filename 23 | def replace_quality(filename, old_q, new_q): 24 | if f"{old_q}_" in filename: 25 | return filename.replace(f"{old_q}_", f"{new_q}_", 1) 26 | elif filename.startswith(f"{old_q}_"): 27 | return f"{new_q}_" + filename[len(f"{old_q}_") :] 28 | return filename 29 | 30 | for q in qualities: 31 | 32 | # Search for which quality is present in the filename 33 | for old_q in qualities: 34 | if f"{old_q}_" in filename or filename.startswith(f"{old_q}_"): 35 | new_filename = replace_quality(filename, old_q, q) 36 | break 37 | 38 | else: 39 | new_filename = filename # No quality found, use original filename 40 | 41 | new_path = f"{dir_path}/{new_filename}" 42 | mpd_url = urlunparse(parsed._replace(path=new_path)).strip() 43 | 44 | try: 45 | r = create_client().head(mpd_url) 46 | if r.status_code == 200: 47 | return mpd_url 48 | 49 | except Exception: 50 | pass 51 | 52 | return None 53 | 54 | def get_manifest(base): 55 | """ 56 | Try to get the manifest URL by checking different qualities. 57 | """ 58 | manifest_qualities = ["hd", "hr", "sd"] 59 | 60 | mpd_url = try_mpd(base, manifest_qualities) 61 | if not mpd_url: 62 | exit(1) 63 | 64 | return mpd_url -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/animeworld/film.py: -------------------------------------------------------------------------------- 1 | # 11.03.24 2 | 3 | import os 4 | 5 | 6 | # External library 7 | from rich.console import Console 8 | 9 | 10 | # Internal utilities 11 | from StreamingCommunity.Util.os import os_manager 12 | from StreamingCommunity.Util.message import start_message 13 | 14 | 15 | # Logic class 16 | from .util.ScrapeSerie import ScrapSerie 17 | from StreamingCommunity.Api.Template.config_loader import site_constant 18 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 19 | 20 | 21 | # Player 22 | from StreamingCommunity import MP4_downloader 23 | from StreamingCommunity.Api.Player.sweetpixel import VideoSource 24 | 25 | 26 | # Variable 27 | console = Console() 28 | 29 | 30 | def download_film(select_title: MediaItem): 31 | """ 32 | Function to download a film. 33 | 34 | Parameters: 35 | - id_film (int): The ID of the film. 36 | - title_name (str): The title of the film. 37 | """ 38 | start_message() 39 | 40 | scrape_serie = ScrapSerie(select_title.url, site_constant.FULL_URL) 41 | episodes = scrape_serie.get_episodes() 42 | 43 | # Get episode information 44 | episode_data = episodes[0] 45 | console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] ([cyan]{scrape_serie.get_name()}[/cyan]) \n") 46 | 47 | # Define filename and path for the downloaded video 48 | serie_name_with_year = os_manager.get_sanitize_file(scrape_serie.get_name(), select_title.date) 49 | mp4_name = f"{serie_name_with_year}.mp4" 50 | mp4_path = os.path.join(site_constant.ANIME_FOLDER, serie_name_with_year.replace('.mp4', '')) 51 | 52 | # Create output folder 53 | os_manager.create_path(mp4_path) 54 | 55 | # Get video source for the episode 56 | video_source = VideoSource(site_constant.FULL_URL, episode_data, scrape_serie.session_id, scrape_serie.csrf_token) 57 | mp4_link = video_source.get_playlist() 58 | 59 | # Start downloading 60 | path, kill_handler = MP4_downloader( 61 | url=str(mp4_link).strip(), 62 | path=os.path.join(mp4_path, mp4_name) 63 | ) 64 | 65 | return path, kill_handler -------------------------------------------------------------------------------- /StreamingCommunity/Api/Template/config_loader.py: -------------------------------------------------------------------------------- 1 | # 11.02.25 2 | 3 | import os 4 | import inspect 5 | 6 | 7 | # Internal utilities 8 | from StreamingCommunity.Util.config_json import config_manager 9 | 10 | 11 | def get_site_name_from_stack(): 12 | for frame_info in inspect.stack(): 13 | file_path = frame_info.filename 14 | 15 | if "__init__" in file_path: 16 | parts = file_path.split(f"Site{os.sep}") 17 | 18 | if len(parts) > 1: 19 | site_name = parts[1].split(os.sep)[0] 20 | return site_name 21 | 22 | return None 23 | 24 | 25 | class SiteConstant: 26 | @property 27 | def SITE_NAME(self): 28 | return get_site_name_from_stack() 29 | 30 | @property 31 | def ROOT_PATH(self): 32 | return config_manager.get('OUT_FOLDER', 'root_path') 33 | 34 | @property 35 | def FULL_URL(self): 36 | return config_manager.get_site(self.SITE_NAME, 'full_url').rstrip('/') 37 | 38 | @property 39 | def SERIES_FOLDER(self): 40 | base_path = self.ROOT_PATH 41 | if config_manager.get_bool("OUT_FOLDER", "add_siteName"): 42 | base_path = os.path.join(base_path, self.SITE_NAME) 43 | return os.path.join(base_path, config_manager.get('OUT_FOLDER', 'serie_folder_name')) 44 | 45 | @property 46 | def MOVIE_FOLDER(self): 47 | base_path = self.ROOT_PATH 48 | if config_manager.get_bool("OUT_FOLDER", "add_siteName"): 49 | base_path = os.path.join(base_path, self.SITE_NAME) 50 | return os.path.join(base_path, config_manager.get('OUT_FOLDER', 'movie_folder_name')) 51 | 52 | @property 53 | def ANIME_FOLDER(self): 54 | base_path = self.ROOT_PATH 55 | if config_manager.get_bool("OUT_FOLDER", "add_siteName"): 56 | base_path = os.path.join(base_path, self.SITE_NAME) 57 | return os.path.join(base_path, config_manager.get('OUT_FOLDER', 'anime_folder_name')) 58 | 59 | @property 60 | def TELEGRAM_BOT(self): 61 | return config_manager.get_bool('DEFAULT', 'telegram_bot') 62 | 63 | 64 | site_constant = SiteConstant() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/guardaserie/site.py: -------------------------------------------------------------------------------- 1 | # 09.06.24 2 | 3 | 4 | # External libraries 5 | from bs4 import BeautifulSoup 6 | from rich.console import Console 7 | 8 | 9 | # Internal utilities 10 | from StreamingCommunity.Util.headers import get_userAgent 11 | from StreamingCommunity.Util.http_client import create_client 12 | from StreamingCommunity.Util.table import TVShowManager 13 | 14 | 15 | # Logic class 16 | from StreamingCommunity.Api.Template.config_loader import site_constant 17 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 18 | 19 | 20 | # Variable 21 | console = Console() 22 | media_search_manager = MediaManager() 23 | table_show_manager = TVShowManager() 24 | 25 | 26 | def title_search(query: str) -> int: 27 | """ 28 | Search for titles based on a search query. 29 | 30 | Parameters: 31 | - query (str): The query to search for. 32 | 33 | Returns: 34 | - int: The number of titles found. 35 | """ 36 | media_search_manager.clear() 37 | table_show_manager.clear() 38 | 39 | search_url = f"{site_constant.FULL_URL}/?story={query}&do=search&subaction=search" 40 | console.print(f"[cyan]Search url: [yellow]{search_url}") 41 | 42 | try: 43 | response = create_client(headers={'user-agent': get_userAgent()}).get(search_url) 44 | response.raise_for_status() 45 | except Exception as e: 46 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 47 | return 0 48 | 49 | # Create soup and find table 50 | soup = BeautifulSoup(response.text, "html.parser") 51 | 52 | for serie_div in soup.find_all('div', class_='entry'): 53 | try: 54 | serie_info = { 55 | 'name': serie_div.find('a').get("title").replace("streaming guardaserie", ""), 56 | 'type': 'tv', 57 | 'url': serie_div.find('a').get("href"), 58 | 'image': f"{site_constant.FULL_URL}/{serie_div.find('img').get('src')}" 59 | } 60 | media_search_manager.add_media(serie_info) 61 | 62 | except Exception as e: 63 | print(f"Error parsing a film entry: {e}") 64 | 65 | # Return the number of titles found 66 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/hd4me/site.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | 4 | # External libraries 5 | from bs4 import BeautifulSoup 6 | from rich.console import Console 7 | 8 | 9 | # Internal utilities 10 | from StreamingCommunity.Util.headers import get_userAgent 11 | from StreamingCommunity.Util.http_client import create_client 12 | from StreamingCommunity.Util.table import TVShowManager 13 | 14 | 15 | # Logic class 16 | from StreamingCommunity.Api.Template.config_loader import site_constant 17 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 18 | 19 | 20 | # Variable 21 | console = Console() 22 | media_search_manager = MediaManager() 23 | table_show_manager = TVShowManager() 24 | 25 | 26 | def title_search(query: str) -> int: 27 | """ 28 | Search for titles based on a search query. 29 | 30 | Parameters: 31 | - query (str): The query to search for. 32 | 33 | Returns: 34 | int: The number of titles found. 35 | """ 36 | media_search_manager.clear() 37 | table_show_manager.clear() 38 | 39 | search_url = "https://hd4me.net/lista-film" 40 | console.print(f"[cyan]Search url: [yellow]{search_url}") 41 | 42 | try: 43 | response = create_client(headers={'user-agent': get_userAgent()}).get(search_url) 44 | response.raise_for_status() 45 | 46 | except Exception as e: 47 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 48 | return 0 49 | 50 | # Create soup instance 51 | soup = BeautifulSoup(response.text, "html.parser") 52 | 53 | # Collect data from new structure 54 | for li in soup.find_all("li"): 55 | 56 | a = li.find("a", href=True, id=True) 57 | if not a: 58 | continue 59 | 60 | href = a["href"].strip() 61 | title = a.get_text().split("–")[0].strip() 62 | id_attr = a.get("id") 63 | 64 | if query.lower() in title.lower(): 65 | media_dict = { 66 | 'id': id_attr, 67 | 'name': title, 68 | 'type': 'film', 69 | 'url': 'https://hd4me.net' + href, 70 | 'image': None 71 | } 72 | media_search_manager.add_media(media_dict) 73 | 74 | # Return the number of titles found 75 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /.github/.site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Streaming Directory 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 | Total Sites: 0 18 | 19 | 20 | 21 | Last Update: - 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /StreamingCommunity/Api/Player/mediapolisvod.py: -------------------------------------------------------------------------------- 1 | # 11.04.25 2 | 3 | 4 | # Internal utilities 5 | from StreamingCommunity.Util.http_client import create_client 6 | from StreamingCommunity.Util.headers import get_headers 7 | 8 | 9 | class VideoSource: 10 | @staticmethod 11 | def extract_m3u8_url(video_url: str) -> str: 12 | """Extract the m3u8 streaming URL from a RaiPlay video URL.""" 13 | if not video_url.endswith('.json'): 14 | if '/video/' in video_url: 15 | video_id = video_url.split('/')[-1].split('.')[0] 16 | video_path = '/'.join(video_url.split('/')[:-1]) 17 | video_url = f"{video_path}/{video_id}.json" 18 | 19 | else: 20 | return "Error: Unable to determine video JSON URL" 21 | 22 | try: 23 | response = create_client(headers=get_headers()).get(video_url) 24 | if response.status_code != 200: 25 | return f"Error: Failed to fetch video data (Status: {response.status_code})" 26 | 27 | video_data = response.json() 28 | content_url = video_data.get("video").get("content_url") 29 | 30 | if not content_url: 31 | return "Error: No content URL found in video data" 32 | 33 | # Extract the element key 34 | if "=" in content_url: 35 | element_key = content_url.split("=")[1] 36 | else: 37 | return "Error: Unable to extract element key" 38 | 39 | # Request the stream URL 40 | params = { 41 | 'cont': element_key, 42 | 'output': '62', 43 | } 44 | 45 | stream_response = create_client(headers=get_headers()).get('https://mediapolisvod.rai.it/relinker/relinkerServlet.htm', params=params) 46 | if stream_response.status_code != 200: 47 | return f"Error: Failed to fetch stream URL (Status: {stream_response.status_code})" 48 | 49 | # Extract the m3u8 URL 50 | stream_data = stream_response.json() 51 | m3u8_url = stream_data.get("video")[0] if "video" in stream_data else None 52 | return m3u8_url 53 | 54 | except Exception as e: 55 | return f"Error: {str(e)}" 56 | -------------------------------------------------------------------------------- /GUI/webgui/settings.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | import os 5 | import sys 6 | from pathlib import Path 7 | 8 | BASE_DIR = Path(__file__).resolve().parent.parent 9 | PROJECT_ROOT = BASE_DIR.parent # /Users/.../StreamingCommunity 10 | 11 | # Ensure project root is on sys.path to import top-level `StreamingCommunity` package 12 | if str(PROJECT_ROOT) not in sys.path: 13 | sys.path.insert(0, str(PROJECT_ROOT)) 14 | 15 | SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-secret-key") 16 | DEBUG = True 17 | ALLOWED_HOSTS = ["*"] 18 | 19 | INSTALLED_APPS = [ 20 | "django.contrib.admin", 21 | "django.contrib.auth", 22 | "django.contrib.contenttypes", 23 | "django.contrib.sessions", 24 | "django.contrib.messages", 25 | "django.contrib.staticfiles", 26 | "searchapp", 27 | ] 28 | 29 | MIDDLEWARE = [ 30 | "django.middleware.security.SecurityMiddleware", 31 | "django.contrib.sessions.middleware.SessionMiddleware", 32 | "django.middleware.common.CommonMiddleware", 33 | "django.middleware.csrf.CsrfViewMiddleware", 34 | "django.contrib.auth.middleware.AuthenticationMiddleware", 35 | "django.contrib.messages.middleware.MessageMiddleware", 36 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 37 | ] 38 | 39 | ROOT_URLCONF = "webgui.urls" 40 | 41 | TEMPLATES = [ 42 | { 43 | "BACKEND": "django.template.backends.django.DjangoTemplates", 44 | "DIRS": [BASE_DIR / "templates"], 45 | "APP_DIRS": True, 46 | "OPTIONS": { 47 | "context_processors": [ 48 | "django.template.context_processors.debug", 49 | "django.template.context_processors.request", 50 | "django.contrib.auth.context_processors.auth", 51 | "django.contrib.messages.context_processors.messages", 52 | ], 53 | }, 54 | }, 55 | ] 56 | 57 | WSGI_APPLICATION = "webgui.wsgi.application" 58 | 59 | DATABASES = { 60 | "default": { 61 | "ENGINE": "django.db.backends.sqlite3", 62 | "NAME": BASE_DIR / "db.sqlite3", 63 | } 64 | } 65 | 66 | LANGUAGE_CODE = "it-it" 67 | TIME_ZONE = "Europe/Rome" 68 | USE_I18N = True 69 | USE_TZ = True 70 | 71 | STATIC_URL = "static/" 72 | STATIC_ROOT = BASE_DIR / "static" 73 | STATICFILES_DIRS = [BASE_DIR / "assets"] if (BASE_DIR / "assets").exists() else [] 74 | 75 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 76 | 77 | CSRF_TRUSTED_ORIGINS = os.environ.get("CSRF_TRUSTED_ORIGINS", "").split() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/plutotv/site.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | 4 | # External libraries 5 | from rich.console import Console 6 | 7 | 8 | # Internal utilities 9 | from StreamingCommunity.Util.headers import get_userAgent 10 | from StreamingCommunity.Util.http_client import create_client 11 | from StreamingCommunity.Util.table import TVShowManager 12 | 13 | 14 | # Logic class 15 | from StreamingCommunity.Api.Template.config_loader import site_constant 16 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 17 | from .util.get_license import get_bearer_token 18 | 19 | 20 | # Variable 21 | console = Console() 22 | media_search_manager = MediaManager() 23 | table_show_manager = TVShowManager() 24 | 25 | 26 | def title_search(query: str) -> int: 27 | """ 28 | Search for titles based on a search query. 29 | 30 | Parameters: 31 | - query (str): The query to search for. 32 | 33 | Returns: 34 | int: The number of titles found. 35 | """ 36 | media_search_manager.clear() 37 | table_show_manager.clear() 38 | 39 | search_url = f"https://service-media-search.clusters.pluto.tv/v1/search?q={query}&limit=10" 40 | console.print(f"[cyan]Search url: [yellow]{search_url}") 41 | 42 | try: 43 | response = create_client(headers={'user-agent': get_userAgent(), 'Authorization': f"Bearer {get_bearer_token()}"}).get(search_url) 44 | response.raise_for_status() 45 | 46 | except Exception as e: 47 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 48 | return 0 49 | 50 | # Collect json data 51 | try: 52 | data = response.json().get('data') 53 | except Exception as e: 54 | console.log(f"Error parsing JSON response: {e}") 55 | return 0 56 | 57 | for dict_title in data: 58 | try: 59 | if dict_title.get('type') == 'channel': 60 | continue 61 | 62 | define_type = 'tv' if dict_title.get('type') == 'series' else dict_title.get('type') 63 | 64 | media_search_manager.add_media({ 65 | 'id': dict_title.get('id'), 66 | 'name': dict_title.get('name'), 67 | 'type': define_type, 68 | 'image': None, 69 | 'url': f"https://service-vod.clusters.pluto.tv/v4/vod/{dict_title.get('type')}/{dict_title.get('id')}" 70 | }) 71 | 72 | except Exception as e: 73 | print(f"Error parsing a film entry: {e}") 74 | 75 | # Return the number of titles found 76 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/realtime/site.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | 4 | # External libraries 5 | from rich.console import Console 6 | 7 | 8 | # Internal utilities 9 | from StreamingCommunity.Util.headers import get_userAgent 10 | from StreamingCommunity.Util.http_client import create_client 11 | from StreamingCommunity.Util.table import TVShowManager 12 | 13 | 14 | # Logic class 15 | from StreamingCommunity.Api.Template.config_loader import site_constant 16 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 17 | 18 | 19 | # Variable 20 | console = Console() 21 | media_search_manager = MediaManager() 22 | table_show_manager = TVShowManager() 23 | 24 | 25 | def title_search(query: str) -> int: 26 | """ 27 | Search for titles based on a search query. 28 | 29 | Parameters: 30 | - query (str): The query to search for. 31 | 32 | Returns: 33 | int: The number of titles found. 34 | """ 35 | media_search_manager.clear() 36 | table_show_manager.clear() 37 | 38 | search_url = f"https://public.aurora.enhanced.live/site/search/page/?include=default&filter[environment]=realtime&v=2&q={query}&page[number]=1&page[size]=20" 39 | console.print(f"[cyan]Search url: [yellow]{search_url}") 40 | 41 | try: 42 | response = create_client(headers={'user-agent': get_userAgent()}).get(search_url) 43 | response.raise_for_status() 44 | 45 | except Exception as e: 46 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 47 | return 0 48 | 49 | # Collect json data 50 | try: 51 | data = response.json().get('data') 52 | except Exception as e: 53 | console.log(f"Error parsing JSON response: {e}") 54 | return 0 55 | 56 | for dict_title in data: 57 | try: 58 | if dict_title.get('type') != 'showpage': 59 | continue 60 | 61 | media_search_manager.add_media({ 62 | 'name': dict_title.get('title'), 63 | 'type': 'tv', 64 | 'date': dict_title.get('dateLastModified').split('T')[0], 65 | 'image': dict_title.get('image').get('url'), 66 | 'url': f'https://public.aurora.enhanced.live/site/page/{str(dict_title.get("slug")).lower().replace(" ", "-")}/?include=default&filter[environment]=realtime&v=2&parent_slug={dict_title.get("parentSlug")}', 67 | }) 68 | 69 | except Exception as e: 70 | print(f"Error parsing a film entry: {e}") 71 | 72 | # Return the number of titles found 73 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/dmax/site.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | 4 | # External libraries 5 | from rich.console import Console 6 | 7 | 8 | # Internal utilities 9 | from StreamingCommunity.Util.headers import get_userAgent 10 | from StreamingCommunity.Util.http_client import create_client 11 | from StreamingCommunity.Util.table import TVShowManager 12 | 13 | 14 | # Logic class 15 | from StreamingCommunity.Api.Template.config_loader import site_constant 16 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 17 | 18 | 19 | # Variable 20 | console = Console() 21 | media_search_manager = MediaManager() 22 | table_show_manager = TVShowManager() 23 | 24 | 25 | def title_search(query: str) -> int: 26 | """ 27 | Search for titles based on a search query. 28 | 29 | Parameters: 30 | - query (str): The query to search for. 31 | 32 | Returns: 33 | int: The number of titles found. 34 | """ 35 | media_search_manager.clear() 36 | table_show_manager.clear() 37 | 38 | search_url = f"https://public.aurora.enhanced.live/site/search/page/?include=default&filter[environment]=dmaxit&v=2&q={query}&page[number]=1&page[size]=20" 39 | console.print(f"[cyan]Search url: [yellow]{search_url}") 40 | 41 | try: 42 | response = create_client(headers={'user-agent': get_userAgent()}).get(search_url) 43 | response.raise_for_status() 44 | 45 | except Exception as e: 46 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 47 | return 0 48 | 49 | # Collect json data 50 | try: 51 | data = response.json().get('data') 52 | except Exception as e: 53 | console.log(f"Error parsing JSON response: {e}") 54 | return 0 55 | 56 | for dict_title in data: 57 | try: 58 | # Skip non-showpage entries 59 | if dict_title.get('type') != 'showpage': 60 | continue 61 | 62 | media_search_manager.add_media({ 63 | 'name': dict_title.get('title'), 64 | 'type': 'tv', 65 | 'date': dict_title.get('dateLastModified').split('T')[0], 66 | 'image': dict_title.get('image').get('url'), 67 | 'url': f'https://public.aurora.enhanced.live/site/page/{str(dict_title.get("slug")).lower().replace(" ", "-")}/?include=default&filter[environment]=dmaxit&v=2&parent_slug={dict_title.get("parentSlug")}', 68 | }) 69 | 70 | except Exception as e: 71 | print(f"Error parsing a film entry: {e}") 72 | 73 | # Return the number of titles found 74 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /Test/Util/hooks.py: -------------------------------------------------------------------------------- 1 | # Simple manual test for pre/post hooks execution 2 | 3 | import os 4 | import sys 5 | import tempfile 6 | 7 | from StreamingCommunity.Util.config_json import config_manager 8 | from StreamingCommunity.run import execute_hooks 9 | 10 | 11 | def main(): 12 | # Prepare temp folder and python script 13 | with tempfile.TemporaryDirectory() as tmp: 14 | out_file = os.path.join(tmp, "hook_out.txt") 15 | script_path = os.path.join(tmp, "hook_script.py") 16 | 17 | with open(script_path, "w", encoding="utf-8") as f: 18 | f.write( 19 | "import os\n" 20 | "with open(os.environ.get('HOOK_OUT'), 'a', encoding='utf-8') as fp:\n" 21 | " fp.write('ran\\n')\n" 22 | ) 23 | 24 | original_hooks = ( 25 | config_manager.config.get("HOOKS", {}).copy() 26 | if config_manager.config.get("HOOKS") 27 | else {} 28 | ) 29 | 30 | try: 31 | # Configure hooks: run the python script pre and post 32 | config_manager.config.setdefault("HOOKS", {}) 33 | config_manager.config["HOOKS"]["pre_run"] = [ 34 | { 35 | "name": "test-pre", 36 | "type": "python", 37 | "path": script_path, 38 | "env": {"HOOK_OUT": out_file}, 39 | "enabled": True, 40 | "continue_on_error": False, 41 | } 42 | ] 43 | config_manager.config["HOOKS"]["post_run"] = [ 44 | { 45 | "name": "test-post", 46 | "type": "python", 47 | "path": script_path, 48 | "env": {"HOOK_OUT": out_file}, 49 | "enabled": True, 50 | "continue_on_error": False, 51 | } 52 | ] 53 | 54 | # Execute and assert 55 | execute_hooks("pre_run") 56 | execute_hooks("post_run") 57 | 58 | with open(out_file, "r", encoding="utf-8") as fp: 59 | content = fp.read().strip() 60 | assert content.splitlines() == ["ran", "ran"], ( 61 | f"Unexpected content: {content!r}" 62 | ) 63 | 64 | print("OK: hooks executed (pre + post)") 65 | 66 | finally: 67 | # Restore original hooks configuration 68 | if original_hooks: 69 | config_manager.config["HOOKS"] = original_hooks 70 | else: 71 | config_manager.config.pop("HOOKS", None) 72 | 73 | 74 | if __name__ == "__main__": 75 | sys.exit(main()) 76 | -------------------------------------------------------------------------------- /StreamingCommunity/Lib/M3U8/decryptor.py: -------------------------------------------------------------------------------- 1 | # 03.04.24 2 | 3 | import sys 4 | import logging 5 | import importlib.util 6 | 7 | 8 | # External library 9 | from rich.console import Console 10 | 11 | 12 | # Cryptodome imports 13 | from Cryptodome.Cipher import AES 14 | from Cryptodome.Util.Padding import unpad 15 | 16 | 17 | # Check if Cryptodome module is installed 18 | console = Console() 19 | crypto_spec = importlib.util.find_spec("Cryptodome") 20 | crypto_installed = crypto_spec is not None 21 | 22 | 23 | if not crypto_installed: 24 | console.log("[red]pycryptodomex non è installato. Per favore installalo. Leggi readme.md [Requirement].") 25 | sys.exit(0) 26 | 27 | 28 | logging.info("[cyan]Decryption use: Cryptodomex") 29 | 30 | 31 | 32 | class M3U8_Decryption: 33 | """ 34 | Class for decrypting M3U8 playlist content using AES with pycryptodomex. 35 | """ 36 | def __init__(self, key: bytes, iv: bytes, method: str) -> None: 37 | """ 38 | Initialize the M3U8_Decryption object. 39 | 40 | Parameters: 41 | key (bytes): The encryption key. 42 | iv (bytes): The initialization vector (IV). 43 | method (str): The encryption method. 44 | """ 45 | self.key = key 46 | self.iv = iv 47 | if "0x" in str(iv): 48 | self.iv = bytes.fromhex(iv.replace("0x", "")) 49 | self.method = method 50 | 51 | # Pre-create the cipher based on the encryption method 52 | if self.method == "AES": 53 | self.cipher = AES.new(self.key, AES.MODE_ECB) 54 | elif self.method == "AES-128": 55 | self.cipher = AES.new(self.key[:16], AES.MODE_CBC, iv=self.iv) 56 | elif self.method == "AES-128-CTR": 57 | self.cipher = AES.new(self.key[:16], AES.MODE_CTR, nonce=self.iv) 58 | else: 59 | raise ValueError("Invalid or unsupported method") 60 | 61 | def decrypt(self, ciphertext: bytes) -> bytes: 62 | """ 63 | Decrypt the ciphertext using the specified encryption method. 64 | 65 | Parameters: 66 | ciphertext (bytes): The encrypted content to decrypt. 67 | 68 | Returns: 69 | bytes: The decrypted content. 70 | """ 71 | #start = time.perf_counter_ns() 72 | 73 | if self.method in {"AES", "AES-128"}: 74 | decrypted_data = self.cipher.decrypt(ciphertext) 75 | decrypted_content = unpad(decrypted_data, AES.block_size) 76 | elif self.method == "AES-128-CTR": 77 | decrypted_content = self.cipher.decrypt(ciphertext) 78 | else: 79 | raise ValueError("Invalid or unsupported method") 80 | 81 | return decrypted_content -------------------------------------------------------------------------------- /StreamingCommunity/Lib/Downloader/MEGA/errors.py: -------------------------------------------------------------------------------- 1 | # 25-06-2020 By @rodwyer "https://pypi.org/project/mega.py/" 2 | 3 | 4 | _CODE_TO_DESCRIPTIONS = { 5 | -1: ('EINTERNAL', 6 | ('An internal error has occurred. Please submit a bug report, ' 7 | 'detailing the exact circumstances in which this error occurred')), 8 | -2: ('EARGS', 'You have passed invalid arguments to this command'), 9 | -3: ('EAGAIN', 10 | ('(always at the request level) A temporary congestion or server ' 11 | 'malfunction prevented your request from being processed. ' 12 | 'No data was altered. Retry. Retries must be spaced with ' 13 | 'exponential backoff')), 14 | -4: ('ERATELIMIT', 15 | ('You have exceeded your command weight per time quota. Please ' 16 | 'wait a few seconds, then try again (this should never happen ' 17 | 'in sane real-life applications)')), 18 | -5: ('EFAILED', 'The upload failed. Please restart it from scratch'), 19 | -6: 20 | ('ETOOMANY', 21 | 'Too many concurrent IP addresses are accessing this upload target URL'), 22 | -7: 23 | ('ERANGE', ('The upload file packet is out of range or not starting and ' 24 | 'ending on a chunk boundary')), 25 | -8: ('EEXPIRED', 26 | ('The upload target URL you are trying to access has expired. ' 27 | 'Please request a fresh one')), 28 | -9: ('ENOENT', 'Object (typically, node or user) not found'), 29 | -10: ('ECIRCULAR', 'Circular linkage attempted'), 30 | -11: ('EACCESS', 31 | 'Access violation (e.g., trying to write to a read-only share)'), 32 | -12: ('EEXIST', 'Trying to create an object that already exists'), 33 | -13: ('EINCOMPLETE', 'Trying to access an incomplete resource'), 34 | -14: ('EKEY', 'A decryption operation failed (never returned by the API)'), 35 | -15: ('ESID', 'Invalid or expired user session, please relogin'), 36 | -16: ('EBLOCKED', 'User blocked'), 37 | -17: ('EOVERQUOTA', 'Request over quota'), 38 | -18: ('ETEMPUNAVAIL', 39 | 'Resource temporarily not available, please try again later'), 40 | -19: ('ETOOMANYCONNECTIONS', 'many connections on this resource'), 41 | -20: ('EWRITE', 'Write failed'), 42 | -21: ('EREAD', 'Read failed'), 43 | -22: ('EAPPKEY', 'Invalid application key; request not processed'), 44 | } 45 | 46 | 47 | class RequestError(Exception): 48 | """ 49 | Error in API request 50 | """ 51 | def __init__(self, message): 52 | code = message 53 | self.code = code 54 | code_desc, long_desc = _CODE_TO_DESCRIPTIONS[code] 55 | self.message = f'{code_desc}, {long_desc}' 56 | 57 | def __str__(self): 58 | return self.message -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/mediasetinfinity/film.py: -------------------------------------------------------------------------------- 1 | # 21.05.24 2 | 3 | import os 4 | from typing import Tuple 5 | 6 | 7 | # External library 8 | from rich.console import Console 9 | 10 | 11 | # Internal utilities 12 | from StreamingCommunity.Util.config_json import config_manager 13 | from StreamingCommunity.Util.os import os_manager 14 | from StreamingCommunity.Util.message import start_message 15 | from StreamingCommunity.Util.headers import get_headers 16 | 17 | 18 | # Logic class 19 | from StreamingCommunity.Api.Template.config_loader import site_constant 20 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 21 | 22 | 23 | # Player 24 | from .util.fix_mpd import get_manifest 25 | from StreamingCommunity import DASH_Downloader 26 | from .util.get_license import get_playback_url, get_tracking_info, generate_license_url 27 | 28 | 29 | # Variable 30 | console = Console() 31 | extension_output = config_manager.get("M3U8_CONVERSION", "extension") 32 | 33 | 34 | def download_film(select_title: MediaItem) -> Tuple[str, bool]: 35 | """ 36 | Downloads a film using the provided film ID, title name, and domain. 37 | 38 | Parameters: 39 | - select_title (MediaItem): The selected media item. 40 | 41 | Return: 42 | - str: output path if successful, otherwise None 43 | """ 44 | start_message() 45 | console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") 46 | 47 | # Define the filename and path for the downloaded film 48 | title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output 49 | mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) 50 | 51 | # Get playback URL and tracking info 52 | playback_json = get_playback_url(select_title.id) 53 | tracking_info = get_tracking_info(playback_json)['videos'][0] 54 | 55 | license_url, license_params = generate_license_url(tracking_info) 56 | mpd_url = get_manifest(tracking_info['url']) 57 | 58 | # Download the episode 59 | dash_process = DASH_Downloader( 60 | license_url=license_url, 61 | mpd_url=mpd_url, 62 | output_path=os.path.join(mp4_path, title_name), 63 | ) 64 | dash_process.parse_manifest(custom_headers=get_headers()) 65 | 66 | if dash_process.download_and_decrypt(query_params=license_params): 67 | dash_process.finalize_output() 68 | 69 | # Get final output path and status 70 | status = dash_process.get_status() 71 | 72 | if status['error'] is not None and status['path']: 73 | try: 74 | os.remove(status['path']) 75 | except Exception: 76 | pass 77 | 78 | return status['path'], status['stopped'] -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/streamingcommunity/film.py: -------------------------------------------------------------------------------- 1 | # 3.12.23 2 | 3 | import os 4 | 5 | 6 | # External library 7 | from rich.console import Console 8 | 9 | 10 | # Internal utilities 11 | from StreamingCommunity.Util.os import os_manager 12 | from StreamingCommunity.Util.config_json import config_manager 13 | from StreamingCommunity.Util.message import start_message 14 | from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession 15 | 16 | 17 | # Logic class 18 | from StreamingCommunity.Api.Template.config_loader import site_constant 19 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 20 | 21 | 22 | # Player 23 | from StreamingCommunity import HLS_Downloader 24 | from StreamingCommunity.Api.Player.vixcloud import VideoSource 25 | 26 | 27 | # Variable 28 | console = Console() 29 | extension_output = config_manager.get("M3U8_CONVERSION", "extension") 30 | 31 | 32 | def download_film(select_title: MediaItem) -> str: 33 | """ 34 | Downloads a film using the provided film ID, title name, and domain. 35 | 36 | Parameters: 37 | - domain (str): The domain of the site 38 | - version (str): Version of site. 39 | 40 | Return: 41 | - str: output path 42 | """ 43 | start_message() 44 | console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") 45 | 46 | # Init class 47 | video_source = VideoSource(f"{site_constant.FULL_URL}/it", False, select_title.id) 48 | 49 | # Retrieve scws and if available master playlist 50 | video_source.get_iframe(select_title.id) 51 | video_source.get_content() 52 | master_playlist = video_source.get_playlist() 53 | 54 | if master_playlist is None: 55 | console.print(f"[red]Site: {site_constant.SITE_NAME}, error: No master playlist found[/red]") 56 | return None 57 | 58 | # Define the filename and path for the downloaded film 59 | title_name = f"{os_manager.get_sanitize_file(select_title.name, select_title.date)}.{extension_output}" 60 | mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) 61 | 62 | # Download the film using the m3u8 playlist, and output filename 63 | hls_process = HLS_Downloader( 64 | m3u8_url=master_playlist, 65 | output_path=os.path.join(mp4_path, title_name) 66 | ).start() 67 | 68 | if site_constant.TELEGRAM_BOT: 69 | 70 | # Delete script_id 71 | script_id = TelegramSession.get_session() 72 | if script_id != "unknown": 73 | TelegramSession.deleteScriptId(script_id) 74 | 75 | if hls_process['error'] is not None: 76 | try: 77 | os.remove(hls_process['path']) 78 | except Exception: 79 | pass 80 | 81 | return hls_process['path'] -------------------------------------------------------------------------------- /StreamingCommunity/Util/installer/binary_paths.py: -------------------------------------------------------------------------------- 1 | # 19.09.25 2 | 3 | import os 4 | import platform 5 | 6 | 7 | class BinaryPaths: 8 | def __init__(self): 9 | self.system = self._detect_system() 10 | self.arch = self._detect_arch() 11 | self.home_dir = os.path.expanduser('~') 12 | 13 | def _detect_system(self) -> str: 14 | """ 15 | Detect and normalize the operating system name. 16 | 17 | Returns: 18 | str: Normalized operating system name ('windows', 'darwin', or 'linux') 19 | 20 | Raises: 21 | ValueError: If the operating system is not supported 22 | """ 23 | system = platform.system().lower() 24 | supported_systems = ['windows', 'darwin', 'linux'] 25 | 26 | if system not in supported_systems: 27 | raise ValueError(f"Unsupported operating system: {system}. Supported: {supported_systems}") 28 | 29 | return system 30 | 31 | def _detect_arch(self) -> str: 32 | """ 33 | Detect and normalize the system architecture. 34 | 35 | Returns: 36 | str: Normalized architecture name 37 | """ 38 | machine = platform.machine().lower() 39 | arch_map = { 40 | 'amd64': 'x64', 41 | 'x86_64': 'x64', 42 | 'x64': 'x64', 43 | 'arm64': 'arm64', 44 | 'aarch64': 'arm64', 45 | 'armv7l': 'arm', 46 | 'i386': 'ia32', 47 | 'i686': 'ia32', 48 | 'x86': 'x86' 49 | } 50 | return arch_map.get(machine, machine) 51 | 52 | def get_binary_directory(self) -> str: 53 | """ 54 | Get the binary directory path based on the operating system. 55 | 56 | Returns: 57 | str: Path to the binary directory 58 | """ 59 | if self.system == 'windows': 60 | return os.path.join(os.path.splitdrive(self.home_dir)[0] + os.path.sep, 'binary') 61 | 62 | elif self.system == 'darwin': 63 | return os.path.join(self.home_dir, 'Applications', 'binary') 64 | 65 | else: # linux 66 | return os.path.join(self.home_dir, '.local', 'bin', 'binary') 67 | 68 | def ensure_binary_directory(self, mode: int = 0o755) -> str: 69 | """ 70 | Create the binary directory if it doesn't exist and return its path. 71 | 72 | Args: 73 | mode (int, optional): Directory permissions. Defaults to 0o755. 74 | 75 | Returns: 76 | str: Path to the binary directory 77 | """ 78 | binary_dir = self.get_binary_directory() 79 | os.makedirs(binary_dir, mode=mode, exist_ok=True) 80 | return binary_dir 81 | 82 | 83 | binary_paths = BinaryPaths() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/hd4me/film.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | import os 4 | 5 | 6 | # External library 7 | from bs4 import BeautifulSoup 8 | from rich.console import Console 9 | 10 | 11 | # Internal utilities 12 | from StreamingCommunity.Util.os import os_manager 13 | from StreamingCommunity.Util.headers import get_headers 14 | from StreamingCommunity.Util.http_client import create_client_curl 15 | from StreamingCommunity.Util.message import start_message 16 | from StreamingCommunity.Util.config_json import config_manager 17 | 18 | 19 | # Logic class 20 | from StreamingCommunity.Api.Template.config_loader import site_constant 21 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 22 | 23 | 24 | # Player 25 | from StreamingCommunity import Mega_Downloader 26 | 27 | 28 | # Variable 29 | console = Console() 30 | extension_output = config_manager.get("M3U8_CONVERSION", "extension") 31 | 32 | 33 | def download_film(select_title: MediaItem) -> str: 34 | """ 35 | Downloads a film using the provided film ID, title name, and domain. 36 | 37 | Parameters: 38 | - select_title (MediaItem): The selected media item. 39 | 40 | Return: 41 | - str: output path if successful, otherwise None 42 | """ 43 | start_message() 44 | console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") 45 | 46 | mega_link = None 47 | try: 48 | response = create_client_curl(headers=get_headers()).get(select_title.url) 49 | response.raise_for_status() 50 | 51 | # Parse HTML to find mega link 52 | soup = BeautifulSoup(response.text, 'html.parser') 53 | for a in soup.find_all("a", href=True): 54 | 55 | if "?!" in a["href"].lower().strip(): 56 | mega_link = "https://mega.nz/file/" + a["href"].split("/")[-1].replace('?!', '') 57 | break 58 | 59 | if "/?file/" in a["href"].lower().strip(): 60 | mega_link = "https://mega.nz/file/" + a["href"].split("/")[-1].replace('/?file/', '') 61 | break 62 | 63 | except Exception as e: 64 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get mostraguarda") 65 | return None 66 | 67 | # Define the filename and path for the downloaded film 68 | title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output 69 | mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) 70 | 71 | # Download the film using the mega downloader 72 | mega = Mega_Downloader() 73 | m = mega.login() 74 | 75 | if mega_link is None: 76 | console.print(f"[red]Site: {site_constant.SITE_NAME}, error: Mega link not found for url: {select_title.url}[/red]") 77 | return None 78 | 79 | output_path = m.download_url( 80 | url=mega_link, 81 | dest_path=os.path.join(mp4_path, title_name) 82 | ) 83 | return output_path -------------------------------------------------------------------------------- /StreamingCommunity/Api/Template/Class/SearchType.py: -------------------------------------------------------------------------------- 1 | # 07.07.24 2 | 3 | from typing import List, TypedDict 4 | 5 | 6 | class MediaItemData(TypedDict, total=False): 7 | id: int # GENERAL 8 | name: str # GENERAL 9 | type: str # GENERAL 10 | url: str # GENERAL 11 | size: str # GENERAL 12 | score: str # GENERAL 13 | date: str # GENERAL 14 | desc: str # GENERAL 15 | 16 | seeder: int # TOR 17 | leecher: int # TOR 18 | 19 | slug: str # SC 20 | 21 | 22 | class MediaItemMeta(type): 23 | def __new__(cls, name, bases, dct): 24 | def init(self, **kwargs): 25 | for key, value in kwargs.items(): 26 | setattr(self, key, value) 27 | 28 | dct['__init__'] = init 29 | 30 | def get_attr(self, item): 31 | return self.__dict__.get(item, None) 32 | 33 | dct['__getattr__'] = get_attr 34 | 35 | def set_attr(self, key, value): 36 | self.__dict__[key] = value 37 | 38 | dct['__setattr__'] = set_attr 39 | 40 | return super().__new__(cls, name, bases, dct) 41 | 42 | 43 | class MediaItem(metaclass=MediaItemMeta): 44 | id: int # GENERAL 45 | name: str # GENERAL 46 | type: str # GENERAL 47 | url: str # GENERAL 48 | size: str # GENERAL 49 | score: str # GENERAL 50 | date: str # GENERAL 51 | desc: str # GENERAL 52 | 53 | seeder: int # TOR 54 | leecher: int # TOR 55 | 56 | slug: str # SC 57 | 58 | 59 | class MediaManager: 60 | def __init__(self): 61 | self.media_list: List[MediaItem] = [] 62 | 63 | def add_media(self, data: dict) -> None: 64 | """ 65 | Add media to the list. 66 | 67 | Args: 68 | data (dict): Media data to add. 69 | """ 70 | self.media_list.append(MediaItem(**data)) 71 | 72 | def get(self, index: int) -> MediaItem: 73 | """ 74 | Get a media item from the list by index. 75 | 76 | Args: 77 | index (int): The index of the media item to retrieve. 78 | 79 | Returns: 80 | MediaItem: The media item at the specified index. 81 | """ 82 | return self.media_list[index] 83 | 84 | def get_length(self) -> int: 85 | """ 86 | Get the number of media items in the list. 87 | 88 | Returns: 89 | int: Number of media items. 90 | """ 91 | return len(self.media_list) 92 | 93 | def clear(self) -> None: 94 | """ 95 | This method clears the media list. 96 | """ 97 | self.media_list.clear() 98 | 99 | def __str__(self): 100 | return f"MediaManager(num_media={len(self.media_list)})" -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/raiplay/film.py: -------------------------------------------------------------------------------- 1 | # 21.05.24 2 | 3 | import os 4 | from typing import Tuple 5 | 6 | 7 | # External library 8 | from rich.console import Console 9 | 10 | 11 | # Internal utilities 12 | from StreamingCommunity.Util.os import os_manager 13 | from StreamingCommunity.Util.config_json import config_manager 14 | from StreamingCommunity.Util.headers import get_headers 15 | from StreamingCommunity.Util.http_client import create_client 16 | from StreamingCommunity.Util.message import start_message 17 | 18 | # Logic class 19 | from .util.get_license import generate_license_url 20 | from StreamingCommunity.Api.Template.config_loader import site_constant 21 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 22 | 23 | 24 | # Player 25 | from StreamingCommunity import HLS_Downloader, DASH_Downloader 26 | from StreamingCommunity.Api.Player.mediapolisvod import VideoSource 27 | 28 | 29 | # Variable 30 | console = Console() 31 | extension_output = config_manager.get("M3U8_CONVERSION", "extension") 32 | 33 | 34 | def download_film(select_title: MediaItem) -> Tuple[str, bool]: 35 | """ 36 | Downloads a film using the provided MediaItem information. 37 | 38 | Parameters: 39 | - select_title (MediaItem): The media item containing film information 40 | 41 | Return: 42 | - str: Path to downloaded file 43 | - bool: Whether download was stopped 44 | """ 45 | start_message() 46 | console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") 47 | 48 | # Extract m3u8 URL from the film's URL 49 | response = create_client(headers=get_headers()).get(select_title.url + ".json") 50 | first_item_path = "https://www.raiplay.it" + response.json().get("first_item_path") 51 | master_playlist = VideoSource.extract_m3u8_url(first_item_path) 52 | 53 | # Define the filename and path for the downloaded film 54 | mp4_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output 55 | mp4_path = os.path.join(site_constant.MOVIE_FOLDER, mp4_name.replace(extension_output, "")) 56 | 57 | # HLS 58 | if ".mpd" not in master_playlist: 59 | r_proc = HLS_Downloader( 60 | m3u8_url=master_playlist, 61 | output_path=os.path.join(mp4_path, mp4_name) 62 | ).start() 63 | 64 | # MPD 65 | else: 66 | license_url = generate_license_url(select_title.mpd_id) 67 | 68 | dash_process = DASH_Downloader( 69 | license_url=license_url, 70 | mpd_url=master_playlist, 71 | output_path=os.path.join(mp4_path, mp4_name), 72 | ) 73 | dash_process.parse_manifest(custom_headers=get_headers()) 74 | 75 | if dash_process.download_and_decrypt(): 76 | dash_process.finalize_output() 77 | 78 | # Get final output path and status 79 | r_proc = dash_process.get_status() 80 | 81 | if r_proc['error'] is not None: 82 | try: 83 | os.remove(r_proc['path']) 84 | except Exception: 85 | pass 86 | 87 | return r_proc['path'], r_proc['stopped'] -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/mediasetinfinity/site.py: -------------------------------------------------------------------------------- 1 | # 25.07.25 2 | 3 | from datetime import datetime 4 | 5 | 6 | # External libraries 7 | from rich.console import Console 8 | 9 | 10 | # Internal utilities 11 | from StreamingCommunity.Util.http_client import create_client 12 | from StreamingCommunity.Util.table import TVShowManager 13 | from StreamingCommunity.Api.Template.config_loader import site_constant 14 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 15 | 16 | 17 | # Logic class 18 | from .util.get_license import get_bearer_token 19 | 20 | 21 | # Variable 22 | console = Console() 23 | media_search_manager = MediaManager() 24 | table_show_manager = TVShowManager() 25 | 26 | 27 | def title_search(query: str) -> int: 28 | """ 29 | Search for titles based on a search query. 30 | 31 | Parameters: 32 | - query (str): The query to search for. 33 | 34 | Returns: 35 | int: The number of titles found. 36 | """ 37 | media_search_manager.clear() 38 | table_show_manager.clear() 39 | class_mediaset_api = get_bearer_token() 40 | search_url = 'https://mediasetplay.api-graph.mediaset.it/' 41 | console.print(f"[cyan]Search url: [yellow]{search_url}") 42 | 43 | params = { 44 | 'extensions': f'{{"persistedQuery":{{"version":1,"sha256Hash":"{class_mediaset_api.getHash256()}"}}}}', 45 | 'variables': f'{{"first":10,"property":"search","query":"{query}","uxReference":"filteredSearch"}}', 46 | } 47 | 48 | try: 49 | response = create_client(headers=class_mediaset_api.generate_request_headers()).get(search_url, params=params) 50 | response.raise_for_status() 51 | except Exception as e: 52 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 53 | return 0 54 | 55 | # Parse response 56 | resp_json = response.json() 57 | items = resp_json.get("data", {}).get("getSearchPage", {}).get("areaContainersConnection", {}).get("areaContainers", [])[0].get("areas", [])[0].get("sections", [])[0].get("collections", [])[0].get("itemsConnection", {}).get("items", []) 58 | 59 | if len(items) == 1: 60 | return 0 61 | 62 | # Process items 63 | for item in items: 64 | is_series = ( 65 | item.get("__typename") == "SeriesItem" 66 | or item.get("cardLink", {}).get("referenceType") == "series" 67 | or bool(item.get("seasons")) 68 | ) 69 | item_type = "tv" if is_series else "film" 70 | 71 | # Get date 72 | date = item.get("year") or '' 73 | if not date: 74 | updated = item.get("updated") or item.get("r") or '' 75 | if updated: 76 | try: 77 | date = datetime.fromisoformat(str(updated).replace("Z", "+00:00")).year 78 | except Exception: 79 | date = '' 80 | 81 | media_search_manager.add_media({ 82 | "id": item.get("guid", ""), 83 | "name": item.get("cardTitle", "No Title"), 84 | "type": item_type, 85 | "image": None, 86 | "date": date, 87 | "url": item.get("cardLink", {}).get("value", "") 88 | }) 89 | 90 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Util/logger.py: -------------------------------------------------------------------------------- 1 | # 26.03.24 2 | 3 | import os 4 | import logging 5 | from logging.handlers import RotatingFileHandler 6 | 7 | 8 | # Internal utilities 9 | from StreamingCommunity.Util.config_json import config_manager 10 | 11 | 12 | class Logger: 13 | _instance = None 14 | 15 | def __new__(cls): 16 | if cls._instance is None: 17 | cls._instance = super(Logger, cls).__new__(cls) 18 | cls._instance._initialized = False 19 | return cls._instance 20 | 21 | def __init__(self): 22 | # Initialize only once 23 | if getattr(self, '_initialized', False): 24 | return 25 | 26 | # Configure root logger 27 | self.debug_mode = config_manager.get_bool('DEFAULT', "debug") 28 | self.logger = logging.getLogger('') 29 | 30 | # Remove any existing handlers to avoid duplication 31 | for handler in self.logger.handlers[:]: 32 | self.logger.removeHandler(handler) 33 | 34 | # Reduce logging level for external libraries 35 | logging.getLogger("httpx").setLevel(logging.WARNING) 36 | logging.getLogger("httpcore").setLevel(logging.WARNING) 37 | 38 | # Set logging level based on debug_mode 39 | if self.debug_mode: 40 | self.logger.setLevel(logging.DEBUG) 41 | self._configure_console_log_file() 42 | 43 | else: 44 | self.logger.setLevel(logging.ERROR) 45 | self._configure_console_logging() 46 | 47 | self._initialized = True 48 | 49 | def _configure_console_logging(self): 50 | """Configure console logging output to terminal.""" 51 | console_handler = logging.StreamHandler() 52 | console_handler.setLevel(logging.ERROR) 53 | formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s') 54 | console_handler.setFormatter(formatter) 55 | self.logger.addHandler(console_handler) 56 | 57 | def _configure_console_log_file(self): 58 | """Create a console.log file only when debug mode is enabled.""" 59 | console_log_path = "console.log" 60 | try: 61 | # Remove existing file if present 62 | if os.path.exists(console_log_path): 63 | os.remove(console_log_path) 64 | 65 | # Create handler for console.log 66 | console_file_handler = RotatingFileHandler( 67 | console_log_path, 68 | maxBytes=5*1024*1024, # 5 MB 69 | backupCount=3 70 | ) 71 | console_file_handler.setLevel(logging.DEBUG) 72 | formatter = logging.Formatter('[%(filename)s:%(lineno)s - %(funcName)20s() ] %(asctime)s - %(levelname)s - %(message)s') 73 | console_file_handler.setFormatter(formatter) 74 | self.logger.addHandler(console_file_handler) 75 | 76 | except Exception as e: 77 | print(f"Error creating console.log: {e}") 78 | 79 | @staticmethod 80 | def get_logger(name=None): 81 | """ 82 | Get a specific logger for a module/component. 83 | If name is None, returns the root logger. 84 | """ 85 | # Ensure Logger instance is initialized 86 | Logger() 87 | return logging.getLogger(name) -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/altadefinizione/site.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | 4 | # External libraries 5 | from bs4 import BeautifulSoup 6 | from rich.console import Console 7 | 8 | 9 | # Internal utilities 10 | from StreamingCommunity.Util.headers import get_userAgent 11 | from StreamingCommunity.Util.http_client import create_client 12 | from StreamingCommunity.Util.table import TVShowManager 13 | from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance 14 | 15 | 16 | # Logic class 17 | from StreamingCommunity.Api.Template.config_loader import site_constant 18 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 19 | 20 | 21 | # Variable 22 | console = Console() 23 | media_search_manager = MediaManager() 24 | table_show_manager = TVShowManager() 25 | 26 | 27 | def title_search(query: str) -> int: 28 | """ 29 | Search for titles based on a search query. 30 | 31 | Parameters: 32 | - query (str): The query to search for. 33 | 34 | Returns: 35 | int: The number of titles found. 36 | """ 37 | if site_constant.TELEGRAM_BOT: 38 | bot = get_bot_instance() 39 | 40 | media_search_manager.clear() 41 | table_show_manager.clear() 42 | 43 | search_url = f"{site_constant.FULL_URL}/?story={query}&do=search&subaction=search" 44 | console.print(f"[cyan]Search url: [yellow]{search_url}") 45 | 46 | try: 47 | response = create_client(headers={'user-agent': get_userAgent()}).get(search_url) 48 | response.raise_for_status() 49 | except Exception as e: 50 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 51 | if site_constant.TELEGRAM_BOT: 52 | bot.send_message(f"ERRORE\n\nErrore nella richiesta di ricerca:\n\n{e}", None) 53 | return 0 54 | 55 | # Prepara le scelte per l'utente 56 | if site_constant.TELEGRAM_BOT: 57 | choices = [] 58 | 59 | # Create soup instance 60 | soup = BeautifulSoup(response.text, "html.parser") 61 | 62 | # Collect data from new structure 63 | boxes = soup.find("div", id="dle-content").find_all("div", class_="box") 64 | for i, box in enumerate(boxes): 65 | 66 | title_tag = box.find("h2", class_="titleFilm") 67 | a_tag = title_tag.find("a") 68 | title = a_tag.get_text(strip=True) 69 | url = a_tag.get("href") 70 | 71 | # Image 72 | img_tag = box.find("img", class_="attachment-loc-film") 73 | image_url = None 74 | if img_tag: 75 | img_src = img_tag.get("src") 76 | if img_src and img_src.startswith("/"): 77 | image_url = f"{site_constant.FULL_URL}{img_src}" 78 | else: 79 | image_url = img_src 80 | 81 | # Type 82 | tipo = "tv" if "/serie-tv/" in url else "film" 83 | 84 | media_dict = { 85 | 'url': url, 86 | 'name': title, 87 | 'type': tipo, 88 | 'image': image_url 89 | } 90 | media_search_manager.add_media(media_dict) 91 | 92 | if site_constant.TELEGRAM_BOT: 93 | choice_text = f"{i} - {title} ({tipo})" 94 | choices.append(choice_text) 95 | 96 | if site_constant.TELEGRAM_BOT: 97 | if choices: 98 | bot.send_message("Lista dei risultati:", choices) 99 | 100 | # Return the number of titles found 101 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Lib/Downloader/MEGA/crypto.py: -------------------------------------------------------------------------------- 1 | # 25-06-2020 By @rodwyer "https://pypi.org/project/mega.py/" 2 | 3 | 4 | import json 5 | import base64 6 | import struct 7 | import binascii 8 | import random 9 | import codecs 10 | 11 | 12 | # External libraries 13 | from Crypto.Cipher import AES 14 | 15 | 16 | def makebyte(x): 17 | return codecs.latin_1_encode(x)[0] 18 | 19 | def makestring(x): 20 | return codecs.latin_1_decode(x)[0] 21 | 22 | def aes_cbc_encrypt(data, key): 23 | aes_cipher = AES.new(key, AES.MODE_CBC, makebyte('\0' * 16)) 24 | return aes_cipher.encrypt(data) 25 | 26 | def aes_cbc_decrypt(data, key): 27 | aes_cipher = AES.new(key, AES.MODE_CBC, makebyte('\0' * 16)) 28 | return aes_cipher.decrypt(data) 29 | 30 | def aes_cbc_encrypt_a32(data, key): 31 | return str_to_a32(aes_cbc_encrypt(a32_to_str(data), a32_to_str(key))) 32 | 33 | def aes_cbc_decrypt_a32(data, key): 34 | return str_to_a32(aes_cbc_decrypt(a32_to_str(data), a32_to_str(key))) 35 | 36 | def encrypt_key(a, key): 37 | return sum((aes_cbc_encrypt_a32(a[i:i + 4], key) 38 | for i in range(0, len(a), 4)), ()) 39 | 40 | def decrypt_key(a, key): 41 | return sum((aes_cbc_decrypt_a32(a[i:i + 4], key) 42 | for i in range(0, len(a), 4)), ()) 43 | 44 | def decrypt_attr(attr, key): 45 | attr = aes_cbc_decrypt(attr, a32_to_str(key)) 46 | attr = makestring(attr) 47 | attr = attr.rstrip('\0') 48 | 49 | if attr[:6] == 'MEGA{"': 50 | json_start = attr.index('{') 51 | json_end = attr.rfind('}') + 1 52 | return json.loads(attr[json_start:json_end]) 53 | 54 | def a32_to_str(a): 55 | return struct.pack('>%dI' % len(a), *a) 56 | 57 | def str_to_a32(b): 58 | if isinstance(b, str): 59 | b = makebyte(b) 60 | if len(b) % 4: 61 | b += b'\0' * (4 - len(b) % 4) 62 | return struct.unpack('>%dI' % (len(b) / 4), b) 63 | 64 | 65 | def mpi_to_int(s): 66 | return int(binascii.hexlify(s[2:]), 16) 67 | 68 | def extended_gcd(a, b): 69 | if a == 0: 70 | return (b, 0, 1) 71 | else: 72 | g, y, x = extended_gcd(b % a, a) 73 | return (g, x - (b // a) * y, y) 74 | 75 | def modular_inverse(a, m): 76 | g, x, y = extended_gcd(a, m) 77 | if g != 1: 78 | raise Exception('modular inverse does not exist') 79 | else: 80 | return x % m 81 | 82 | def base64_url_decode(data): 83 | data += '=='[(2 - len(data) * 3) % 4:] 84 | for search, replace in (('-', '+'), ('_', '/'), (',', '')): 85 | data = data.replace(search, replace) 86 | return base64.b64decode(data) 87 | 88 | def base64_to_a32(s): 89 | return str_to_a32(base64_url_decode(s)) 90 | 91 | def base64_url_encode(data): 92 | data = base64.b64encode(data) 93 | data = makestring(data) 94 | for search, replace in (('+', '-'), ('/', '_'), ('=', '')): 95 | data = data.replace(search, replace) 96 | 97 | return data 98 | 99 | def a32_to_base64(a): 100 | return base64_url_encode(a32_to_str(a)) 101 | 102 | def get_chunks(size): 103 | p = 0 104 | s = 0x20000 105 | while p + s < size: 106 | yield (p, s) 107 | p += s 108 | if s < 0x100000: 109 | s += 0x20000 110 | 111 | yield (p, size - p) 112 | 113 | def make_id(length): 114 | text = '' 115 | possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 116 | for i in range(length): 117 | text += random.choice(possible) 118 | return text -------------------------------------------------------------------------------- /GUI/searchapp/tests.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | from django.test import TestCase, Client 5 | from django.urls import reverse 6 | from unittest.mock import patch 7 | import json 8 | 9 | 10 | class SeriesMetadataViewTests(TestCase): 11 | def setUp(self): 12 | self.client = Client() 13 | self.url = reverse("series_metadata") 14 | 15 | def post_json(self, data): 16 | return self.client.post( 17 | self.url, data=json.dumps(data), content_type="application/json" 18 | ) 19 | 20 | def test_film_returns_no_series(self): 21 | payload = {"type": "Film"} 22 | resp = self.post_json( 23 | {"source_alias": "streamingcommunity", "item_payload": payload} 24 | ) 25 | self.assertEqual(resp.status_code, 200) 26 | data = resp.json() 27 | self.assertFalse(data.get("isSeries")) 28 | self.assertEqual(data.get("seasonsCount"), 0) 29 | self.assertEqual(data.get("episodesPerSeason"), {}) 30 | 31 | def test_ova_returns_no_series(self): 32 | payload = {"type": "OVA"} 33 | resp = self.post_json({"source_alias": "animeunity", "item_payload": payload}) 34 | self.assertEqual(resp.status_code, 200) 35 | data = resp.json() 36 | self.assertFalse(data.get("isSeries")) 37 | self.assertEqual(data.get("seasonsCount"), 0) 38 | self.assertEqual(data.get("episodesPerSeason"), {}) 39 | 40 | def test_unknown_site_returns_default(self): 41 | payload = {"type": "series"} 42 | resp = self.post_json({"source_alias": "unknown", "item_payload": payload}) 43 | self.assertEqual(resp.status_code, 200) 44 | data = resp.json() 45 | self.assertFalse(data.get("isSeries")) 46 | 47 | @patch( 48 | "StreamingCommunity.Api.Site.streamingcommunity.util.ScrapeSerie.GetSerieInfo" 49 | ) 50 | @patch( 51 | "StreamingCommunity.Util.config_json.config_manager.get_site", 52 | return_value="https://example.com", 53 | ) 54 | def test_streamingcommunity_happy_path(self, _cfg_mock, getserieinfo_mock): 55 | instance = getserieinfo_mock.return_value 56 | instance.getNumberSeason.return_value = 2 57 | 58 | def _get_eps(season): 59 | return [object()] * (10 if season == 1 else 8) 60 | 61 | instance.getEpisodeSeasons.side_effect = _get_eps 62 | 63 | payload = {"type": "series", "id": 123, "slug": "my-show"} 64 | resp = self.post_json( 65 | {"source_alias": "streamingcommunity", "item_payload": payload} 66 | ) 67 | self.assertEqual(resp.status_code, 200) 68 | data = resp.json() 69 | self.assertTrue(data.get("isSeries")) 70 | self.assertEqual(data.get("seasonsCount"), 2) 71 | self.assertEqual(data.get("episodesPerSeason"), {1: 10, 2: 8}) 72 | 73 | @patch("StreamingCommunity.Api.Site.animeunity.util.ScrapeSerie.ScrapeSerieAnime") 74 | @patch( 75 | "StreamingCommunity.Util.config_json.config_manager.get_site", 76 | return_value="https://example.com", 77 | ) 78 | def test_animeunity_single_season(self, _cfg_mock, scrape_mock): 79 | instance = scrape_mock.return_value 80 | instance.get_count_episodes.return_value = 24 81 | 82 | payload = {"type": "series", "id": 55, "slug": "anime-x"} 83 | resp = self.post_json({"source_alias": "animeunity", "item_payload": payload}) 84 | self.assertEqual(resp.status_code, 200) 85 | data = resp.json() 86 | self.assertTrue(data.get("isSeries")) 87 | self.assertEqual(data.get("seasonsCount"), 1) 88 | self.assertEqual(data.get("episodesPerSeason"), {1: 24}) -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/raiplay/site.py: -------------------------------------------------------------------------------- 1 | # 21.05.24 2 | 3 | # External libraries 4 | from rich.console import Console 5 | 6 | 7 | # Internal utilities 8 | from StreamingCommunity.Util.headers import get_headers 9 | from StreamingCommunity.Util.http_client import create_client 10 | from StreamingCommunity.Util.table import TVShowManager 11 | from StreamingCommunity.Api.Template.config_loader import site_constant 12 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 13 | 14 | 15 | # Variable 16 | console = Console() 17 | media_search_manager = MediaManager() 18 | table_show_manager = TVShowManager() 19 | 20 | 21 | def title_search(query: str) -> int: 22 | """ 23 | Search for titles based on a search query. 24 | 25 | Parameters: 26 | - query (str): The query to search for. 27 | 28 | Returns: 29 | int: The number of titles found. 30 | """ 31 | media_search_manager.clear() 32 | table_show_manager.clear() 33 | 34 | search_url = "https://www.raiplay.it/atomatic/raiplay-search-service/api/v1/msearch" 35 | console.print(f"[cyan]Search url: [yellow]{search_url}") 36 | 37 | json_data = { 38 | 'templateIn': '6470a982e4e0301afe1f81f1', 39 | 'templateOut': '6516ac5d40da6c377b151642', 40 | 'params': { 41 | 'param': query, 42 | 'from': None, 43 | 'sort': 'relevance', 44 | 'onlyVideoQuery': False, 45 | }, 46 | } 47 | 48 | try: 49 | response = create_client(headers=get_headers()).post(search_url, json=json_data) 50 | response.raise_for_status() 51 | 52 | except Exception as e: 53 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 54 | return 0 55 | 56 | try: 57 | response_data = response.json() 58 | cards = response_data.get('agg', {}).get('titoli', {}).get('cards', []) 59 | 60 | # Limit to only 15 results for performance 61 | data = cards[:15] 62 | console.print(f"[cyan]Found {len(cards)} results, processing first {len(data)}...[/cyan]") 63 | 64 | except Exception as e: 65 | console.print(f"[red]Error parsing search results: {e}[/red]") 66 | return 0 67 | 68 | # Process each item and add to media manager 69 | for idx, item in enumerate(data, 1): 70 | try: 71 | # Get path_id 72 | path_id = item.get('path_id', '') 73 | if not path_id: 74 | console.print("[yellow]Skipping item due to missing path_id[/yellow]") 75 | continue 76 | 77 | # Get image URL - handle both relative and absolute URLs 78 | image = item.get('immagine', '') 79 | if image and not image.startswith('http'): 80 | image = f"https://www.raiplay.it{image}" 81 | 82 | # Get URL - handle both relative and absolute URLs 83 | url = item.get('url', '') 84 | if url and not url.startswith('http'): 85 | url = f"https://www.raiplay.it{url}" 86 | 87 | media_search_manager.add_media({ 88 | 'id': item.get('id', ''), 89 | 'path_id': path_id, 90 | 'name': item.get('titolo', 'Unknown'), 91 | 'type': 'tv', 92 | 'url': url, 93 | 'image': image, 94 | 'year': image.split("/")[5] 95 | }) 96 | 97 | except Exception as e: 98 | console.print(f"[red]Error processing item '{item.get('titolo', 'Unknown')}': {e}[/red]") 99 | continue 100 | 101 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/hd4me/__init__.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | 4 | # External library 5 | from rich.console import Console 6 | from rich.prompt import Prompt 7 | 8 | 9 | # Internal utilities 10 | from StreamingCommunity.Api.Template import get_select_title 11 | from StreamingCommunity.Api.Template.config_loader import site_constant 12 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 13 | 14 | 15 | # Logic class 16 | from .site import title_search, table_show_manager, media_search_manager 17 | from .film import download_film 18 | 19 | 20 | # Variable 21 | indice = 10 22 | _useFor = "Film" 23 | _priority = 0 24 | _engineDownload = "hls" 25 | _deprecate = False 26 | 27 | msg = Prompt() 28 | console = Console() 29 | 30 | 31 | def get_user_input(string_to_search: str = None): 32 | """ 33 | Asks the user to input a search term. 34 | Handles both Telegram bot input and direct input. 35 | If string_to_search is provided, it's returned directly (after stripping). 36 | """ 37 | if string_to_search is not None: 38 | return string_to_search.strip() 39 | 40 | else: 41 | return msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() 42 | 43 | 44 | def process_search_result(select_title, selections=None): 45 | """ 46 | Handles the search result and initiates the download for either a film or series. 47 | 48 | Parameters: 49 | select_title (MediaItem): The selected media item 50 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 51 | {'season': season_selection, 'episode': episode_selection} 52 | 53 | Returns: 54 | bool: True if processing was successful, False otherwise 55 | """ 56 | if not select_title: 57 | return False 58 | 59 | if select_title.type == 'film': 60 | download_film(select_title) 61 | table_show_manager.clear() 62 | return True 63 | 64 | 65 | # search("Game of Thrones", selections={"season": "1", "episode": "1-3"}) 66 | def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): 67 | """ 68 | Main function of the application for search. 69 | 70 | Parameters: 71 | string_to_search (str, optional): String to search for 72 | get_onlyDatabase (bool, optional): If True, return only the database object 73 | direct_item (dict, optional): Direct item to process (bypass search) 74 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 75 | {'season': season_selection, 'episode': episode_selection} 76 | """ 77 | if direct_item: 78 | select_title = MediaItem(**direct_item) 79 | result = process_search_result(select_title, selections) 80 | return result 81 | 82 | # Get the user input for the search term 83 | actual_search_query = get_user_input(string_to_search) 84 | 85 | # Handle empty input 86 | if not actual_search_query: 87 | return False 88 | 89 | # Search on database 90 | len_database = title_search(actual_search_query) 91 | 92 | # If only the database is needed, return the manager 93 | if get_onlyDatabase: 94 | return media_search_manager 95 | 96 | if len_database > 0: 97 | select_title = get_select_title(table_show_manager, media_search_manager, len_database) 98 | result = process_search_result(select_title, selections) 99 | return result 100 | 101 | else: 102 | console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") 103 | return False -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/altadefinizione/film.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | import os 4 | import re 5 | 6 | 7 | # External library 8 | from bs4 import BeautifulSoup 9 | from rich.console import Console 10 | 11 | 12 | # Internal utilities 13 | from StreamingCommunity.Util.os import os_manager 14 | from StreamingCommunity.Util.headers import get_headers 15 | from StreamingCommunity.Util.http_client import create_client 16 | from StreamingCommunity.Util.message import start_message 17 | from StreamingCommunity.Util.config_json import config_manager 18 | from StreamingCommunity.TelegramHelp.telegram_bot import TelegramSession 19 | 20 | 21 | # Logic class 22 | from StreamingCommunity.Api.Template.config_loader import site_constant 23 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 24 | 25 | 26 | # Player 27 | from StreamingCommunity import HLS_Downloader 28 | from StreamingCommunity.Api.Player.supervideo import VideoSource 29 | 30 | 31 | # Variable 32 | console = Console() 33 | extension_output = config_manager.get("M3U8_CONVERSION", "extension") 34 | 35 | 36 | def download_film(select_title: MediaItem) -> str: 37 | """ 38 | Downloads a film using the provided film ID, title name, and domain. 39 | 40 | Parameters: 41 | - select_title (MediaItem): The selected media item. 42 | 43 | Return: 44 | - str: output path if successful, otherwise None 45 | """ 46 | start_message() 47 | console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") 48 | 49 | # Extract mostraguarda URL 50 | try: 51 | response = create_client(headers=get_headers()).get(select_title.url) 52 | response.raise_for_status() 53 | 54 | soup = BeautifulSoup(response.text, 'html.parser') 55 | iframes = soup.find_all('iframe') 56 | mostraguarda = iframes[0]['src'] 57 | 58 | except Exception as e: 59 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get mostraguarda") 60 | return None 61 | 62 | # Extract supervideo URL 63 | supervideo_url = None 64 | try: 65 | response = create_client(headers=get_headers()).get(mostraguarda) 66 | response.raise_for_status() 67 | 68 | soup = BeautifulSoup(response.text, 'html.parser') 69 | pattern = r'//supervideo\.[^/]+/[a-z]/[a-zA-Z0-9]+' 70 | supervideo_match = re.search(pattern, response.text) 71 | supervideo_url = 'https:' + supervideo_match.group(0) 72 | 73 | except Exception as e: 74 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request error: {e}, get supervideo URL") 75 | console.print("[yellow]This content will be available soon![/yellow]") 76 | return None 77 | 78 | # Init class 79 | video_source = VideoSource(supervideo_url) 80 | master_playlist = video_source.get_playlist() 81 | 82 | # Define the filename and path for the downloaded film 83 | title_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output 84 | mp4_path = os.path.join(site_constant.MOVIE_FOLDER, title_name.replace(extension_output, "")) 85 | 86 | # Download the film using the m3u8 playlist, and output filename 87 | hls_process = HLS_Downloader( 88 | m3u8_url=master_playlist, 89 | output_path=os.path.join(mp4_path, title_name) 90 | ).start() 91 | 92 | if site_constant.TELEGRAM_BOT: 93 | 94 | # Delete script_id 95 | script_id = TelegramSession.get_session() 96 | if script_id != "unknown": 97 | TelegramSession.deleteScriptId(script_id) 98 | 99 | if hls_process['error'] is not None: 100 | try: 101 | os.remove(hls_process['path']) 102 | except Exception: 103 | pass 104 | 105 | return hls_process['path'] -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/crunchyroll/site.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | # External libraries 4 | from rich.console import Console 5 | 6 | 7 | # Internal utilities 8 | from StreamingCommunity.Util.config_json import config_manager 9 | from StreamingCommunity.Util.table import TVShowManager 10 | 11 | 12 | # Logic class 13 | from StreamingCommunity.Api.Template.config_loader import site_constant 14 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 15 | from .util.get_license import CrunchyrollClient 16 | 17 | 18 | # Variable 19 | console = Console() 20 | media_search_manager = MediaManager() 21 | table_show_manager = TVShowManager() 22 | 23 | 24 | def title_search(query: str) -> int: 25 | """ 26 | Search for titles based on a search query. 27 | 28 | Parameters: 29 | - query (str): The query to search for. 30 | 31 | Returns: 32 | int: The number of titles found. 33 | """ 34 | media_search_manager.clear() 35 | table_show_manager.clear() 36 | 37 | config = config_manager.get_dict("SITE_LOGIN", "crunchyroll") 38 | if not config.get('device_id') or not config.get('etp_rt'): 39 | console.print("[bold red] device_id or etp_rt is missing or empty in config.json.[/bold red]") 40 | raise Exception("device_id or etp_rt is missing or empty in config.json.") 41 | 42 | client = CrunchyrollClient() 43 | if not client.start(): 44 | console.print("[bold red] Failed to authenticate with Crunchyroll.[/bold red]") 45 | raise Exception("Failed to authenticate with Crunchyroll.") 46 | 47 | api_url = "https://www.crunchyroll.com/content/v2/discover/search" 48 | 49 | params = { 50 | "q": query, 51 | "n": 20, 52 | "type": "series,movie_listing", 53 | "ratings": "true", 54 | "preferred_audio_language": "it-IT", 55 | "locale": "it-IT" 56 | } 57 | 58 | console.print(f"[cyan]Search url: [yellow]{api_url}") 59 | 60 | try: 61 | response = client._request_with_retry('GET', api_url, params=params) 62 | response.raise_for_status() 63 | 64 | except Exception as e: 65 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 66 | return 0 67 | 68 | data = response.json() 69 | found = 0 70 | 71 | # Parse results 72 | for block in data.get("data", []): 73 | if block.get("type") not in ("series", "movie_listing", "top_results"): 74 | continue 75 | 76 | for item in block.get("items", []): 77 | tipo = None 78 | 79 | if item.get("type") == "movie_listing": 80 | tipo = "film" 81 | elif item.get("type") == "series": 82 | meta = item.get("series_metadata", {}) 83 | 84 | # Heuristic: single episode series might be films 85 | if meta.get("episode_count") == 1 and meta.get("season_count", 1) == 1 and meta.get("series_launch_year"): 86 | description = item.get("description", "").lower() 87 | if "film" in description or "movie" in description: 88 | tipo = "film" 89 | else: 90 | tipo = "tv" 91 | else: 92 | tipo = "tv" 93 | else: 94 | continue 95 | 96 | url = "" 97 | if tipo in ("tv", "film"): 98 | url = f"https://www.crunchyroll.com/series/{item.get('id')}" 99 | else: 100 | continue 101 | 102 | title = item.get("title", "") 103 | 104 | media_search_manager.add_media({ 105 | 'name': title, 106 | 'type': tipo, 107 | 'url': url 108 | }) 109 | found += 1 110 | 111 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/animeworld/util/ScrapeSerie.py: -------------------------------------------------------------------------------- 1 | # 21.03.25 2 | 3 | import logging 4 | 5 | 6 | # External libraries 7 | from bs4 import BeautifulSoup 8 | 9 | 10 | # Internal utilities 11 | from StreamingCommunity.Util.headers import get_userAgent 12 | from StreamingCommunity.Util.http_client import create_client 13 | from StreamingCommunity.Util.os import os_manager 14 | 15 | 16 | # Player 17 | from ..site import get_session_and_csrf 18 | from StreamingCommunity.Api.Player.sweetpixel import VideoSource 19 | 20 | 21 | 22 | 23 | class ScrapSerie: 24 | def __init__(self, url, site_url): 25 | """Initialize the ScrapSerie object with the provided URL and setup the HTTP client.""" 26 | self.url = url 27 | self.session_id, self.csrf_token = get_session_and_csrf() 28 | self.client = create_client( 29 | cookies={"sessionId": self.session_id}, 30 | headers={"User-Agent": get_userAgent(), "csrf-token": self.csrf_token} 31 | ) 32 | 33 | try: 34 | self.response = self.client.get(self.url) 35 | self.response.raise_for_status() 36 | 37 | except Exception as e: 38 | raise Exception(f"Failed to retrieve anime page: {str(e)}") 39 | 40 | def get_name(self): 41 | """Extract and return the name of the anime series.""" 42 | soup = BeautifulSoup(self.response.content, "html.parser") 43 | return os_manager.get_sanitize_file(soup.find("h1", {"id": "anime-title"}).get_text(strip=True)) 44 | 45 | def get_episodes(self, nums=None): 46 | """Fetch and return the list of episodes, optionally filtering by specific episode numbers.""" 47 | soup = BeautifulSoup(self.response.content, "html.parser") 48 | 49 | raw_eps = {} 50 | for data in soup.select('li.episode > a'): 51 | epNum = data.get('data-episode-num') 52 | epID = data.get('data-episode-id') 53 | 54 | if nums and epNum not in nums: 55 | continue 56 | 57 | if epID not in raw_eps: 58 | raw_eps[epID] = { 59 | 'number': epNum, 60 | 'link': f"/api/download/{epID}" 61 | } 62 | 63 | episodes = [episode_data for episode_data in raw_eps.values()] 64 | return episodes 65 | 66 | def get_episode(self, index): 67 | """Fetch a specific episode based on the index, and return an VideoSource instance.""" 68 | episodes = self.get_episodes() 69 | 70 | if 0 <= index < len(episodes): 71 | episode_data = episodes[index] 72 | return VideoSource(episode_data, self.session_id, self.csrf_token) 73 | 74 | else: 75 | raise IndexError("Episode index out of range") 76 | 77 | 78 | # ------------- FOR GUI ------------- 79 | def getNumberSeason(self) -> int: 80 | """ 81 | Get the total number of seasons available for the anime. 82 | Note: AnimeWorld typically doesn't have seasons, so returns 1. 83 | """ 84 | return 1 85 | 86 | def getEpisodeSeasons(self, season_number: int = 1) -> list: 87 | """ 88 | Get all episodes for a specific season. 89 | Note: For AnimeWorld, this returns all episodes as they're typically in one season. 90 | """ 91 | return self.get_episodes() 92 | 93 | def selectEpisode(self, season_number: int = 1, episode_index: int = 0) -> dict: 94 | """ 95 | Get information for a specific episode. 96 | """ 97 | episodes = self.get_episodes() 98 | if not episodes or episode_index < 0 or episode_index >= len(episodes): 99 | logging.error(f"Episode index {episode_index} is out of range") 100 | return None 101 | 102 | return episodes[episode_index] -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/animeworld/site.py: -------------------------------------------------------------------------------- 1 | # 21.03.25 2 | 3 | import logging 4 | 5 | 6 | # External libraries 7 | from bs4 import BeautifulSoup 8 | from rich.console import Console 9 | 10 | 11 | # Internal utilities 12 | from StreamingCommunity.Util.headers import get_headers 13 | from StreamingCommunity.Util.http_client import create_client 14 | from StreamingCommunity.Util.table import TVShowManager 15 | 16 | 17 | # Logic class 18 | from StreamingCommunity.Api.Template.config_loader import site_constant 19 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 20 | 21 | 22 | # Variable 23 | console = Console() 24 | media_search_manager = MediaManager() 25 | table_show_manager = TVShowManager() 26 | 27 | 28 | def get_session_and_csrf() -> dict: 29 | """ 30 | Get the session ID and CSRF token from the website's cookies and HTML meta data. 31 | """ 32 | # Send an initial GET request to the website 33 | client = create_client(headers=get_headers()) 34 | response = client.get(site_constant.FULL_URL) 35 | 36 | # Extract the sessionId from the cookies 37 | session_id = response.cookies.get('sessionId') 38 | logging.info(f"Session ID: {session_id}") 39 | 40 | # Use BeautifulSoup to parse the HTML and extract the CSRF-Token 41 | soup = BeautifulSoup(response.text, 'html.parser') 42 | 43 | # Try to find the CSRF token in a meta tag or hidden input 44 | csrf_token = None 45 | meta_tag = soup.find('meta', {'name': 'csrf-token'}) 46 | if meta_tag: 47 | csrf_token = meta_tag.get('content') 48 | 49 | # If it's not in the meta tag, check for hidden input fields 50 | if not csrf_token: 51 | input_tag = soup.find('input', {'name': '_csrf'}) 52 | if input_tag: 53 | csrf_token = input_tag.get('value') 54 | 55 | logging.info(f"CSRF Token: {csrf_token}") 56 | return session_id, csrf_token 57 | 58 | def title_search(query: str) -> int: 59 | """ 60 | Function to perform an anime search using a provided title. 61 | 62 | Parameters: 63 | - query (str): The query to search for. 64 | 65 | Returns: 66 | - int: A number containing the length of media search manager. 67 | """ 68 | search_url = f"{site_constant.FULL_URL}/search?keyword={query}" 69 | console.print(f"[cyan]Search url: [yellow]{search_url}") 70 | 71 | # Make the GET request 72 | try: 73 | response = create_client(headers=get_headers()).get(search_url) 74 | except Exception as e: 75 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 76 | return 0 77 | 78 | # Create soup istance 79 | soup = BeautifulSoup(response.text, 'html.parser') 80 | 81 | # Collect data from soup 82 | for element in soup.find_all('a', class_='poster'): 83 | try: 84 | title = element.find('img').get('alt') 85 | url = f"{site_constant.FULL_URL}{element.get('href')}" 86 | status_div = element.find('div', class_='status') 87 | is_dubbed = False 88 | anime_type = 'TV' 89 | 90 | if status_div: 91 | if status_div.find('div', class_='dub'): 92 | is_dubbed = True 93 | 94 | if status_div.find('div', class_='movie'): 95 | anime_type = 'Movie' 96 | elif status_div.find('div', class_='ona'): 97 | anime_type = 'ONA' 98 | 99 | media_search_manager.add_media({ 100 | 'name': title, 101 | 'type': anime_type, 102 | 'DUB': is_dubbed, 103 | 'url': url, 104 | 'image': element.find('img').get('src') 105 | }) 106 | 107 | except Exception as e: 108 | print(f"Error parsing a film entry: {e}") 109 | 110 | # Return the length of media search manager 111 | return media_search_manager.get_length() 112 | -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/animeworld/serie.py: -------------------------------------------------------------------------------- 1 | # 11.03.24 2 | 3 | import os 4 | from typing import Tuple 5 | 6 | 7 | # External library 8 | from rich.console import Console 9 | from rich.prompt import Prompt 10 | 11 | 12 | # Internal utilities 13 | from StreamingCommunity.Util.os import os_manager 14 | from StreamingCommunity.Util.message import start_message 15 | 16 | 17 | # Logic class 18 | from .util.ScrapeSerie import ScrapSerie 19 | from StreamingCommunity.Api.Template.config_loader import site_constant 20 | from StreamingCommunity.Api.Template.Util import manage_selection, dynamic_format_number 21 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 22 | 23 | 24 | # Player 25 | from StreamingCommunity import MP4_downloader 26 | from StreamingCommunity.Api.Player.sweetpixel import VideoSource 27 | 28 | 29 | # Variable 30 | console = Console() 31 | msg = Prompt() 32 | KILL_HANDLER = bool(False) 33 | 34 | 35 | def download_episode(index_select: int, scrape_serie: ScrapSerie) -> Tuple[str,bool]: 36 | """ 37 | Downloads the selected episode. 38 | 39 | Parameters: 40 | - index_select (int): Index of the episode to download. 41 | 42 | Return: 43 | - str: output path 44 | - bool: kill handler status 45 | """ 46 | start_message() 47 | 48 | # Get episode information 49 | episode_data = scrape_serie.selectEpisode(1, index_select) 50 | 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") 51 | 52 | # Define filename and path for the downloaded video 53 | mp4_name = f"{scrape_serie.get_name()}_EP_{dynamic_format_number(str(index_select+1))}.mp4" 54 | mp4_path = os.path.join(site_constant.ANIME_FOLDER, scrape_serie.get_name()) 55 | 56 | # Create output folder 57 | os_manager.create_path(mp4_path) 58 | 59 | # Get video source for the episode 60 | video_source = VideoSource(site_constant.FULL_URL, episode_data, scrape_serie.session_id, scrape_serie.csrf_token) 61 | mp4_link = video_source.get_playlist() 62 | 63 | # Start downloading 64 | path, kill_handler = MP4_downloader( 65 | url=str(mp4_link).strip(), 66 | path=os.path.join(mp4_path, mp4_name) 67 | ) 68 | 69 | return path, kill_handler 70 | 71 | 72 | def download_series(select_title: MediaItem, episode_selection: str = None): 73 | """ 74 | Function to download episodes of a TV series. 75 | 76 | Parameters: 77 | - select_title (MediaItem): The selected media item 78 | - episode_selection (str, optional): Episode selection input that bypasses manual input 79 | """ 80 | start_message() 81 | 82 | # Create scrap instance 83 | scrape_serie = ScrapSerie(select_title.url, site_constant.FULL_URL) 84 | episodes = scrape_serie.get_episodes() 85 | 86 | # Get episode count 87 | console.print(f"\n[green]Episodes count:[/green] [red]{len(episodes)}[/red]") 88 | 89 | # Display episodes list and get user selection 90 | if episode_selection is None: 91 | 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") 92 | else: 93 | last_command = episode_selection 94 | console.print(f"\n[cyan]Using provided episode selection: [yellow]{episode_selection}") 95 | 96 | list_episode_select = manage_selection(last_command, len(episodes)) 97 | 98 | # Download selected episodes 99 | if len(list_episode_select) == 1 and last_command != "*": 100 | path, _ = download_episode(list_episode_select[0]-1, scrape_serie) 101 | return path 102 | 103 | # Download all selected episodes 104 | else: 105 | kill_handler = False 106 | for i_episode in list_episode_select: 107 | if kill_handler: 108 | break 109 | _, kill_handler = download_episode(i_episode-1, scrape_serie) -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/animeunity/util/ScrapeSerie.py: -------------------------------------------------------------------------------- 1 | # 01.03.24 2 | 3 | import logging 4 | 5 | 6 | # Internal utilities 7 | from StreamingCommunity.Util.headers import get_headers 8 | from StreamingCommunity.Util.http_client import create_client_curl 9 | from StreamingCommunity.Api.Player.Helper.Vixcloud.util import EpisodeManager, Episode 10 | 11 | 12 | class ScrapeSerieAnime: 13 | def __init__(self, url: str): 14 | """ 15 | Initialize the media scraper for a specific website. 16 | 17 | Args: 18 | url (str): Url of the streaming site 19 | """ 20 | self.is_series = False 21 | self.headers = get_headers() 22 | self.url = url 23 | self.episodes_cache = None 24 | 25 | def setup(self, version: str = None, media_id: int = None, series_name: str = None): 26 | self.version = version 27 | self.media_id = media_id 28 | 29 | if series_name is not None: 30 | self.is_series = True 31 | self.series_name = series_name 32 | self.obj_episode_manager: EpisodeManager = EpisodeManager() 33 | 34 | def get_count_episodes(self): 35 | """ 36 | Retrieve total number of episodes for the selected media. 37 | This includes partial episodes (like episode 6.5). 38 | 39 | Returns: 40 | int: Total episode count including partial episodes 41 | """ 42 | if self.episodes_cache is None: 43 | self._fetch_all_episodes() 44 | 45 | if self.episodes_cache: 46 | return len(self.episodes_cache) 47 | return None 48 | 49 | def _fetch_all_episodes(self): 50 | """ 51 | Fetch all episodes data at once and cache it 52 | """ 53 | try: 54 | # Get initial episode count 55 | response = create_client_curl(headers=self.headers).get(f"{self.url}/info_api/{self.media_id}/") 56 | response.raise_for_status() 57 | initial_count = response.json()["episodes_count"] 58 | 59 | all_episodes = [] 60 | start_range = 1 61 | 62 | # Fetch episodes in chunks 63 | while start_range <= initial_count: 64 | end_range = min(start_range + 119, initial_count) 65 | 66 | params={ 67 | "start_range": start_range, 68 | "end_range": end_range 69 | } 70 | 71 | response = create_client_curl(headers=self.headers).get(f"{self.url}/info_api/{self.media_id}/1", params=params) 72 | response.raise_for_status() 73 | 74 | chunk_episodes = response.json().get("episodes", []) 75 | all_episodes.extend(chunk_episodes) 76 | start_range = end_range + 1 77 | 78 | self.episodes_cache = all_episodes 79 | except Exception as e: 80 | logging.error(f"Error fetching all episodes: {e}") 81 | self.episodes_cache = None 82 | 83 | def get_info_episode(self, index_ep: int) -> Episode: 84 | """ 85 | Get episode info from cache 86 | """ 87 | if self.episodes_cache is None: 88 | self._fetch_all_episodes() 89 | 90 | if self.episodes_cache and 0 <= index_ep < len(self.episodes_cache): 91 | return Episode(self.episodes_cache[index_ep]) 92 | return None 93 | 94 | 95 | # ------------- FOR GUI ------------- 96 | def getNumberSeason(self) -> int: 97 | """ 98 | Get the total number of seasons available for the anime. 99 | Note: AnimeUnity typically doesn't have seasons, so returns 1. 100 | """ 101 | return 1 102 | 103 | def selectEpisode(self, season_number: int = 1, episode_index: int = 0) -> Episode: 104 | """ 105 | Get information for a specific episode. 106 | """ 107 | return self.get_info_episode(episode_index) 108 | -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/crunchyroll/film.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | import os 4 | from urllib.parse import urlparse, parse_qs 5 | 6 | 7 | # External library 8 | from rich.console import Console 9 | 10 | 11 | # Internal utilities 12 | from StreamingCommunity.Util.message import start_message 13 | from StreamingCommunity.Util.config_json import config_manager 14 | from StreamingCommunity.Util.os import os_manager 15 | 16 | 17 | # Logic class 18 | from StreamingCommunity.Api.Template.config_loader import site_constant 19 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 20 | 21 | 22 | # Player 23 | from StreamingCommunity import DASH_Downloader 24 | from .util.get_license import get_playback_session, CrunchyrollClient 25 | 26 | 27 | # Variable 28 | console = Console() 29 | extension_output = config_manager.get("M3U8_CONVERSION", "extension") 30 | 31 | 32 | def download_film(select_title: MediaItem) -> str: 33 | """ 34 | Downloads a film using the provided film ID, title name, and domain. 35 | 36 | Parameters: 37 | - select_title (MediaItem): The selected media item. 38 | 39 | Return: 40 | - str: output path if successful, otherwise None 41 | """ 42 | start_message() 43 | console.print(f"\n[bold yellow]Download:[/bold yellow] [red]{site_constant.SITE_NAME}[/red] → [cyan]{select_title.name}[/cyan] \n") 44 | 45 | # Initialize Crunchyroll client 46 | client = CrunchyrollClient() 47 | if not client.start(): 48 | console.print("[bold red]Failed to authenticate with Crunchyroll.[/bold red]") 49 | return None, True 50 | 51 | # Define filename and path for the downloaded video 52 | mp4_name = os_manager.get_sanitize_file(select_title.name, select_title.date) + extension_output 53 | mp4_path = os.path.join(site_constant.MOVIE_FOLDER, mp4_name.replace(extension_output, "")) 54 | 55 | # Generate mpd and license URLs 56 | url_id = select_title.get('url').split('/')[-1] 57 | 58 | # Get playback session 59 | try: 60 | playback_result = get_playback_session(client, url_id) 61 | 62 | # Check if access was denied (403) 63 | if playback_result is None: 64 | console.print("[bold red]✗ Access denied:[/bold red] This content requires a premium subscription") 65 | return None, False 66 | 67 | mpd_url, mpd_headers, mpd_list_sub, token, audio_locale = playback_result 68 | 69 | except Exception as e: 70 | console.print(f"[bold red]✗ Error getting playback session:[/bold red] {str(e)}") 71 | return None, False 72 | 73 | # Parse playback token from mpd_url 74 | parsed_url = urlparse(mpd_url) 75 | query_params = parse_qs(parsed_url.query) 76 | 77 | # Download the film 78 | dash_process = DASH_Downloader( 79 | license_url='https://www.crunchyroll.com/license/v1/license/widevine', 80 | mpd_url=mpd_url, 81 | mpd_sub_list=mpd_list_sub, 82 | output_path=os.path.join(mp4_path, mp4_name), 83 | ) 84 | dash_process.parse_manifest(custom_headers=mpd_headers) 85 | 86 | # Create headers for license request 87 | license_headers = mpd_headers.copy() 88 | license_headers.update({ 89 | "x-cr-content-id": url_id, 90 | "x-cr-video-token": query_params['playbackGuid'][0], 91 | }) 92 | 93 | if dash_process.download_and_decrypt(custom_headers=license_headers): 94 | dash_process.finalize_output() 95 | 96 | # Get final output path and status 97 | status = dash_process.get_status() 98 | 99 | if status['error'] is not None and status['path']: 100 | try: 101 | os.remove(status['path']) 102 | except Exception: 103 | pass 104 | 105 | # Delete stream after download to avoid TOO_MANY_ACTIVE_STREAMS 106 | playback_token = token or query_params.get('playbackGuid', [None])[0] 107 | if playback_token: 108 | client.delete_active_stream(url_id, playback_token) 109 | console.print("[dim]✓ Playback session closed[/dim]") 110 | 111 | return status['path'], status['stopped'] -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/dmax/__init__.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | # External library 4 | from rich.console import Console 5 | from rich.prompt import Prompt 6 | 7 | 8 | # Internal utilities 9 | from StreamingCommunity.Api.Template import get_select_title 10 | from StreamingCommunity.Api.Template.config_loader import site_constant 11 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 12 | 13 | 14 | # Logic class 15 | from .site import title_search, table_show_manager, media_search_manager 16 | from .series import download_series 17 | 18 | 19 | # Variable 20 | indice = 9 21 | _useFor = "Serie" 22 | _priority = 0 23 | _engineDownload = "hls" 24 | _deprecate = False 25 | 26 | msg = Prompt() 27 | console = Console() 28 | 29 | 30 | def get_user_input(string_to_search: str = None): 31 | """ 32 | Asks the user to input a search term. 33 | Handles both Telegram bot input and direct input. 34 | If string_to_search is provided, it's returned directly (after stripping). 35 | """ 36 | if string_to_search is not None: 37 | return string_to_search.strip() 38 | else: 39 | return msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() 40 | 41 | 42 | def process_search_result(select_title, selections=None): 43 | """ 44 | Handles the search result and initiates the download for either a film or series. 45 | 46 | Parameters: 47 | select_title (MediaItem): The selected media item. Can be None if selection fails. 48 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 49 | e.g., {'season': season_selection, 'episode': episode_selection} 50 | Returns: 51 | bool: True if processing was successful, False otherwise 52 | """ 53 | if not select_title: 54 | return False 55 | 56 | if select_title.type == 'tv': 57 | season_selection = None 58 | episode_selection = None 59 | 60 | if selections: 61 | season_selection = selections.get('season') 62 | episode_selection = selections.get('episode') 63 | 64 | download_series(select_title, season_selection, episode_selection) 65 | media_search_manager.clear() 66 | table_show_manager.clear() 67 | return True 68 | 69 | 70 | def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): 71 | """ 72 | Main function of the application for search. 73 | 74 | Parameters: 75 | string_to_search (str, optional): String to search for. Can be passed from run.py. 76 | If 'back', special handling might occur in get_user_input. 77 | get_onlyDatabase (bool, optional): If True, return only the database search manager object. 78 | direct_item (dict, optional): Direct item to process (bypasses search). 79 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 80 | for series (season/episode). 81 | """ 82 | if direct_item: 83 | select_title = MediaItem(**direct_item) 84 | result = process_search_result(select_title, selections) 85 | return result 86 | 87 | # Get the user input for the search term 88 | actual_search_query = get_user_input(string_to_search) 89 | 90 | # Handle empty input 91 | if not actual_search_query: 92 | return False 93 | 94 | # Search on database 95 | len_database = title_search(actual_search_query) 96 | 97 | # If only the database is needed, return the manager 98 | if get_onlyDatabase: 99 | return media_search_manager 100 | 101 | if len_database > 0: 102 | select_title = get_select_title(table_show_manager, media_search_manager, len_database) 103 | result = process_search_result(select_title, selections) 104 | return result 105 | 106 | else: 107 | console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") 108 | return False -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/plutotv/__init__.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | 4 | # External library 5 | from rich.console import Console 6 | from rich.prompt import Prompt 7 | 8 | 9 | # Internal utilities 10 | from StreamingCommunity.Api.Template import get_select_title 11 | from StreamingCommunity.Api.Template.config_loader import site_constant 12 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 13 | 14 | 15 | # Logic class 16 | from .site import title_search, table_show_manager, media_search_manager 17 | from .series import download_series 18 | 19 | 20 | # Variable 21 | indice = 11 22 | _useFor = "Serie" 23 | _priority = 0 24 | _engineDownload = "dash" 25 | _deprecate = True 26 | 27 | msg = Prompt() 28 | console = Console() 29 | 30 | 31 | def get_user_input(string_to_search: str = None): 32 | """ 33 | Asks the user to input a search term. 34 | Handles both Telegram bot input and direct input. 35 | If string_to_search is provided, it's returned directly (after stripping). 36 | """ 37 | if string_to_search is not None: 38 | return string_to_search.strip() 39 | else: 40 | return msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() 41 | 42 | def process_search_result(select_title, selections=None): 43 | """ 44 | Handles the search result and initiates the download for either a film or series. 45 | 46 | Parameters: 47 | select_title (MediaItem): The selected media item. Can be None if selection fails. 48 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 49 | e.g., {'season': season_selection, 'episode': episode_selection} 50 | Returns: 51 | bool: True if processing was successful, False otherwise 52 | """ 53 | if not select_title: 54 | return False 55 | 56 | if select_title.type == 'tv': 57 | season_selection = None 58 | episode_selection = None 59 | 60 | if selections: 61 | season_selection = selections.get('season') 62 | episode_selection = selections.get('episode') 63 | 64 | download_series(select_title, season_selection, episode_selection) 65 | media_search_manager.clear() 66 | table_show_manager.clear() 67 | return True 68 | 69 | 70 | def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): 71 | """ 72 | Main function of the application for search. 73 | 74 | Parameters: 75 | string_to_search (str, optional): String to search for. Can be passed from run.py. 76 | If 'back', special handling might occur in get_user_input. 77 | get_onlyDatabase (bool, optional): If True, return only the database search manager object. 78 | direct_item (dict, optional): Direct item to process (bypasses search). 79 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 80 | for series (season/episode). 81 | """ 82 | if direct_item: 83 | select_title = MediaItem(**direct_item) 84 | result = process_search_result(select_title, selections) 85 | return result 86 | 87 | # Get the user input for the search term 88 | actual_search_query = get_user_input(string_to_search) 89 | 90 | # Handle empty input 91 | if not actual_search_query: 92 | return False 93 | 94 | # Search on database 95 | len_database = title_search(actual_search_query) 96 | 97 | # If only the database is needed, return the manager 98 | if get_onlyDatabase: 99 | return media_search_manager 100 | 101 | if len_database > 0: 102 | select_title = get_select_title(table_show_manager, media_search_manager, len_database) 103 | result = process_search_result(select_title, selections) 104 | return result 105 | 106 | else: 107 | console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") 108 | return False -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/realtime/__init__.py: -------------------------------------------------------------------------------- 1 | # 26.11.2025 2 | 3 | 4 | # External library 5 | from rich.console import Console 6 | from rich.prompt import Prompt 7 | 8 | 9 | # Internal utilities 10 | from StreamingCommunity.Api.Template import get_select_title 11 | from StreamingCommunity.Api.Template.config_loader import site_constant 12 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 13 | 14 | 15 | # Logic class 16 | from .site import title_search, table_show_manager, media_search_manager 17 | from .series import download_series 18 | 19 | 20 | # Variable 21 | indice = 8 22 | _useFor = "Serie" 23 | _priority = 0 24 | _engineDownload = "hls" 25 | _deprecate = False 26 | 27 | msg = Prompt() 28 | console = Console() 29 | 30 | 31 | def get_user_input(string_to_search: str = None): 32 | """ 33 | Asks the user to input a search term. 34 | Handles both Telegram bot input and direct input. 35 | If string_to_search is provided, it's returned directly (after stripping). 36 | """ 37 | if string_to_search is not None: 38 | return string_to_search.strip() 39 | else: 40 | return msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() 41 | 42 | def process_search_result(select_title, selections=None): 43 | """ 44 | Handles the search result and initiates the download for either a film or series. 45 | 46 | Parameters: 47 | select_title (MediaItem): The selected media item. Can be None if selection fails. 48 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 49 | e.g., {'season': season_selection, 'episode': episode_selection} 50 | Returns: 51 | bool: True if processing was successful, False otherwise 52 | """ 53 | if not select_title: 54 | return False 55 | 56 | if select_title.type == 'tv': 57 | season_selection = None 58 | episode_selection = None 59 | 60 | if selections: 61 | season_selection = selections.get('season') 62 | episode_selection = selections.get('episode') 63 | 64 | download_series(select_title, season_selection, episode_selection) 65 | media_search_manager.clear() 66 | table_show_manager.clear() 67 | return True 68 | 69 | 70 | def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): 71 | """ 72 | Main function of the application for search. 73 | 74 | Parameters: 75 | string_to_search (str, optional): String to search for. Can be passed from run.py. 76 | If 'back', special handling might occur in get_user_input. 77 | get_onlyDatabase (bool, optional): If True, return only the database search manager object. 78 | direct_item (dict, optional): Direct item to process (bypasses search). 79 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 80 | for series (season/episode). 81 | """ 82 | if direct_item: 83 | select_title = MediaItem(**direct_item) 84 | result = process_search_result(select_title, selections) 85 | return result 86 | 87 | # Get the user input for the search term 88 | actual_search_query = get_user_input(string_to_search) 89 | 90 | # Handle empty input 91 | if not actual_search_query: 92 | return False 93 | 94 | # Search on database 95 | len_database = title_search(actual_search_query) 96 | 97 | # If only the database is needed, return the manager 98 | if get_onlyDatabase: 99 | return media_search_manager 100 | 101 | if len_database > 0: 102 | select_title = get_select_title(table_show_manager, media_search_manager, len_database) 103 | result = process_search_result(select_title, selections) 104 | return result 105 | 106 | else: 107 | console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") 108 | return False -------------------------------------------------------------------------------- /StreamingCommunity/Upload/update.py: -------------------------------------------------------------------------------- 1 | # 01.03.2023 2 | 3 | import os 4 | import sys 5 | import time 6 | import asyncio 7 | import importlib.metadata 8 | 9 | 10 | # External library 11 | import httpx 12 | from rich.console import Console 13 | 14 | 15 | # Internal utilities 16 | from .version import __version__ as source_code_version, __author__, __title__ 17 | from StreamingCommunity.Util.config_json import config_manager 18 | from StreamingCommunity.Util.headers import get_userAgent 19 | 20 | 21 | # Variable 22 | if getattr(sys, 'frozen', False): # Modalità PyInstaller 23 | base_path = os.path.join(sys._MEIPASS, "StreamingCommunity") 24 | else: 25 | base_path = os.path.dirname(__file__) 26 | console = Console() 27 | 28 | 29 | async def fetch_github_data(client, url): 30 | """Helper function to fetch data from GitHub API""" 31 | response = await client.get( 32 | url=url, 33 | headers={'user-agent': get_userAgent()}, 34 | timeout=config_manager.get_int("REQUESTS", "timeout"), 35 | follow_redirects=True 36 | ) 37 | return response.json() 38 | 39 | async def async_github_requests(): 40 | """Make concurrent GitHub API requests""" 41 | async with httpx.AsyncClient() as client: 42 | tasks = [ 43 | fetch_github_data(client, f"https://api.github.com/repos/{__author__}/{__title__}/releases"), 44 | fetch_github_data(client, f"https://api.github.com/repos/{__author__}/{__title__}/commits") 45 | ] 46 | return await asyncio.gather(*tasks) 47 | 48 | def get_execution_mode(): 49 | """Get the execution mode of the application""" 50 | if getattr(sys, 'frozen', False): 51 | return "installer" 52 | 53 | try: 54 | package_location = importlib.metadata.files(__title__) 55 | if any("site-packages" in str(path) for path in package_location): 56 | return "pip" 57 | 58 | except importlib.metadata.PackageNotFoundError: 59 | pass 60 | 61 | return "python" 62 | 63 | def update(): 64 | """Check for updates on GitHub and display relevant information.""" 65 | try: 66 | # Run async requests concurrently 67 | response_releases, response_commits = asyncio.run(async_github_requests()) 68 | 69 | except Exception as e: 70 | console.print(f"[red]Error accessing GitHub API: {e}") 71 | return 72 | 73 | # Calculate total download count from all releases 74 | total_download_count = sum(asset['download_count'] for release in response_releases for asset in release.get('assets', [])) 75 | 76 | # Get latest version name 77 | if response_releases: 78 | last_version = response_releases[0].get('name', 'Unknown') 79 | else: 80 | last_version = 'Unknown' 81 | 82 | # Get the current version (installed version) 83 | try: 84 | current_version = importlib.metadata.version(__title__) 85 | except importlib.metadata.PackageNotFoundError: 86 | current_version = source_code_version 87 | 88 | # Get commit details 89 | latest_commit = response_commits[0] if response_commits else None 90 | if latest_commit: 91 | latest_commit_message = latest_commit.get('commit', {}).get('message', 'No commit message') 92 | else: 93 | latest_commit_message = 'No commit history available' 94 | 95 | if str(current_version).replace('v', '') != str(last_version).replace('v', ''): 96 | console.print(f"\n[cyan]New version available: [yellow]{last_version}") 97 | 98 | console.print( 99 | f"\n[red]{__title__} has been downloaded [yellow]{total_download_count}" 100 | f"\n[yellow]{get_execution_mode()} - [green]Current installed version: [yellow]{current_version} " 101 | f"[green]last commit: [white]'[yellow]{latest_commit_message.splitlines()[0]}[white]'\n" 102 | f" [cyan]Help the repository grow today by leaving a [yellow]star [cyan]and [yellow]sharing " 103 | f"[cyan]it with others online!\n" 104 | f" [magenta]If you'd like to support development and keep the program updated, consider leaving a " 105 | f"[yellow]donation[magenta]. Thank you!" 106 | ) 107 | 108 | time.sleep(1) -------------------------------------------------------------------------------- /StreamingCommunity/Api/Player/Helper/Vixcloud/js_parser.py: -------------------------------------------------------------------------------- 1 | # 26.11.24 2 | # !!! DIO CANErino 3 | 4 | import re 5 | 6 | 7 | class JavaScriptParser: 8 | @staticmethod 9 | def fix_string(ss): 10 | if ss is None: 11 | return None 12 | 13 | ss = str(ss) 14 | ss = ss.encode('utf-8').decode('unicode-escape') 15 | ss = ss.strip("\"'") 16 | ss = ss.strip() 17 | 18 | return ss 19 | 20 | @staticmethod 21 | def fix_url(url): 22 | if url is None: 23 | return None 24 | 25 | url = url.replace('\\/', '/') 26 | return url 27 | 28 | @staticmethod 29 | def parse_value(value): 30 | value = JavaScriptParser.fix_string(value) 31 | 32 | if 'http' in str(value) or 'https' in str(value): 33 | return JavaScriptParser.fix_url(value) 34 | 35 | if value is None or str(value).lower() == 'null': 36 | return None 37 | if str(value).lower() == 'true': 38 | return True 39 | if str(value).lower() == 'false': 40 | return False 41 | 42 | try: 43 | return int(value) 44 | except ValueError: 45 | try: 46 | return float(value) 47 | except ValueError: 48 | pass 49 | 50 | return value 51 | 52 | @staticmethod 53 | def parse_object(obj_str): 54 | obj_str = obj_str.strip('{}').strip() 55 | 56 | result = {} 57 | key_value_pairs = re.findall(r'([\'"]?[\w]+[\'"]?)\s*:\s*([^,{}]+|{[^}]*}|\[[^\]]*\]|\'[^\']*\'|"[^"]*")', obj_str) 58 | 59 | for key, value in key_value_pairs: 60 | key = JavaScriptParser.fix_string(key) 61 | value = value.strip() 62 | 63 | if value.startswith('{'): 64 | result[key] = JavaScriptParser.parse_object(value) 65 | elif value.startswith('['): 66 | result[key] = JavaScriptParser.parse_array(value) 67 | else: 68 | result[key] = JavaScriptParser.parse_value(value) 69 | 70 | return result 71 | 72 | @staticmethod 73 | def parse_array(arr_str): 74 | arr_str = arr_str.strip('[]').strip() 75 | result = [] 76 | 77 | elements = [] 78 | current_elem = "" 79 | brace_count = 0 80 | in_string = False 81 | quote_type = None 82 | 83 | for char in arr_str: 84 | if char in ['"', "'"]: 85 | if not in_string: 86 | in_string = True 87 | quote_type = char 88 | elif quote_type == char: 89 | in_string = False 90 | quote_type = None 91 | 92 | if not in_string: 93 | if char == '{': 94 | brace_count += 1 95 | elif char == '}': 96 | brace_count -= 1 97 | elif char == ',' and brace_count == 0: 98 | elements.append(current_elem.strip()) 99 | current_elem = "" 100 | continue 101 | 102 | current_elem += char 103 | 104 | if current_elem.strip(): 105 | elements.append(current_elem.strip()) 106 | 107 | for elem in elements: 108 | elem = elem.strip() 109 | 110 | if elem.startswith('{'): 111 | result.append(JavaScriptParser.parse_object(elem)) 112 | elif 'active' in elem or 'url' in elem: 113 | key_value_match = re.search(r'([\w]+)\":([^,}]+)', elem) 114 | 115 | if key_value_match: 116 | key = key_value_match.group(1) 117 | value = key_value_match.group(2) 118 | result[-1][key] = JavaScriptParser.parse_value(value.strip('"\\')) 119 | else: 120 | result.append(JavaScriptParser.parse_value(elem)) 121 | 122 | return result 123 | 124 | @classmethod 125 | def parse(cls, js_string): 126 | assignments = re.findall(r'window\.(\w+)\s*=\s*([^;]+);?', js_string, re.DOTALL) 127 | result = {} 128 | 129 | for var_name, value in assignments: 130 | value = value.strip() 131 | 132 | if value.startswith('{'): 133 | result[var_name] = cls.parse_object(value) 134 | elif value.startswith('['): 135 | result[var_name] = cls.parse_array(value) 136 | else: 137 | result[var_name] = cls.parse_value(value) 138 | 139 | can_play_fhd_match = re.search(r'window\.canPlayFHD\s*=\s*(\w+);?', js_string) 140 | if can_play_fhd_match: 141 | result['canPlayFHD'] = cls.parse_value(can_play_fhd_match.group(1)) 142 | 143 | return result 144 | -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/streamingcommunity/site.py: -------------------------------------------------------------------------------- 1 | # 10.12.23 2 | 3 | import json 4 | 5 | 6 | # External libraries 7 | from bs4 import BeautifulSoup 8 | from rich.console import Console 9 | 10 | 11 | # Internal utilities 12 | from StreamingCommunity.Util.headers import get_userAgent 13 | from StreamingCommunity.Util.http_client import create_client 14 | from StreamingCommunity.Util.table import TVShowManager 15 | from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance 16 | 17 | 18 | # Logic class 19 | from StreamingCommunity.Api.Template.config_loader import site_constant 20 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 21 | 22 | 23 | # Variable 24 | console = Console() 25 | media_search_manager = MediaManager() 26 | table_show_manager = TVShowManager() 27 | 28 | 29 | def title_search(query: str) -> int: 30 | """ 31 | Search for titles based on a search query. 32 | 33 | Parameters: 34 | - query (str): The query to search for. 35 | 36 | Returns: 37 | int: The number of titles found. 38 | """ 39 | if site_constant.TELEGRAM_BOT: 40 | bot = get_bot_instance() 41 | 42 | media_search_manager.clear() 43 | table_show_manager.clear() 44 | 45 | try: 46 | response = create_client(headers={'user-agent': get_userAgent()}).get(f"{site_constant.FULL_URL}/it") 47 | response.raise_for_status() 48 | 49 | soup = BeautifulSoup(response.text, 'html.parser') 50 | version = json.loads(soup.find('div', {'id': "app"}).get("data-page"))['version'] 51 | 52 | except Exception as e: 53 | console.print(f"[red]Site: {site_constant.SITE_NAME} version, request error: {e}") 54 | return 0 55 | 56 | search_url = f"{site_constant.FULL_URL}/it/search?q={query}" 57 | console.print(f"[cyan]Search url: [yellow]{search_url}") 58 | 59 | try: 60 | response = create_client(headers={'user-agent': get_userAgent(), 'x-inertia': 'true', 'x-inertia-version': version}).get(search_url) 61 | response.raise_for_status() 62 | 63 | except Exception as e: 64 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 65 | if site_constant.TELEGRAM_BOT: 66 | bot.send_message(f"ERRORE\n\nErrore nella richiesta di ricerca:\n\n{e}", None) 67 | return 0 68 | 69 | # Prepara le scelte per l'utente 70 | if site_constant.TELEGRAM_BOT: 71 | choices = [] 72 | 73 | # Collect json data 74 | try: 75 | data = response.json().get('props').get('titles') 76 | except Exception as e: 77 | console.log(f"Error parsing JSON response: {e}") 78 | return 0 79 | 80 | for i, dict_title in enumerate(data): 81 | try: 82 | images = dict_title.get('images') or [] 83 | filename = None 84 | preferred_types = ['poster', 'cover', 'cover_mobile', 'background'] 85 | for ptype in preferred_types: 86 | for img in images: 87 | if img.get('type') == ptype and img.get('filename'): 88 | filename = img.get('filename') 89 | break 90 | 91 | if filename: 92 | break 93 | 94 | if not filename and images: 95 | filename = images[0].get('filename') 96 | 97 | image_url = None 98 | if filename: 99 | image_url = f"{site_constant.FULL_URL.replace('stream', 'cdn.stream')}/images/{filename}" 100 | 101 | # Extract date: prefer last_air_date, otherwise try translations (last_air_date or release_date) 102 | date = dict_title.get('last_air_date') 103 | if not date: 104 | for trans in dict_title.get('translations') or []: 105 | if trans.get('key') in ('last_air_date', 'release_date') and trans.get('value'): 106 | date = trans.get('value') 107 | break 108 | 109 | media_search_manager.add_media({ 110 | 'id': dict_title.get('id'), 111 | 'slug': dict_title.get('slug'), 112 | 'name': dict_title.get('name'), 113 | 'type': dict_title.get('type'), 114 | 'date': date, 115 | 'image': image_url 116 | }) 117 | 118 | if site_constant.TELEGRAM_BOT: 119 | choice_date = date if date else "N/A" 120 | choice_text = f"{i} - {dict_title.get('name')} ({dict_title.get('type')}) - {choice_date}" 121 | choices.append(choice_text) 122 | 123 | except Exception as e: 124 | print(f"Error parsing a film entry: {e}") 125 | if site_constant.TELEGRAM_BOT: 126 | bot.send_message(f"ERRORE\n\nErrore nell'analisi del film:\n\n{e}", None) 127 | 128 | if site_constant.TELEGRAM_BOT: 129 | if choices: 130 | bot.send_message("Lista dei risultati:", choices) 131 | 132 | # Return the number of titles found 133 | return media_search_manager.get_length() -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/altadefinizione/util/ScrapeSerie.py: -------------------------------------------------------------------------------- 1 | # 16.03.25 2 | 3 | import logging 4 | 5 | 6 | # External libraries 7 | from bs4 import BeautifulSoup 8 | 9 | 10 | # Internal utilities 11 | from StreamingCommunity.Util.headers import get_userAgent 12 | from StreamingCommunity.Util.http_client import create_client 13 | from StreamingCommunity.Api.Player.Helper.Vixcloud.util import SeasonManager 14 | 15 | 16 | class GetSerieInfo: 17 | def __init__(self, url): 18 | """ 19 | Initialize the GetSerieInfo class for scraping TV series information. 20 | 21 | Args: 22 | - url (str): The URL of the streaming site. 23 | """ 24 | self.headers = {'user-agent': get_userAgent()} 25 | self.url = url 26 | self.seasons_manager = SeasonManager() 27 | 28 | def collect_season(self) -> None: 29 | """ 30 | Retrieve all episodes for all seasons. 31 | """ 32 | response = create_client(headers=self.headers).get(self.url) 33 | soup = BeautifulSoup(response.text, "html.parser") 34 | self.series_name = soup.find("title").get_text(strip=True).split(" - ")[0] 35 | 36 | tt_holder = soup.find('div', id='tt_holder') 37 | 38 | # Find all seasons 39 | seasons_div = tt_holder.find('div', class_='tt_season') 40 | if not seasons_div: 41 | return 42 | 43 | season_list_items = seasons_div.find_all('li') 44 | for season_li in season_list_items: 45 | season_anchor = season_li.find('a') 46 | if not season_anchor: 47 | continue 48 | 49 | season_num = int(season_anchor.get_text(strip=True)) 50 | season_name = f"Stagione {season_num}" 51 | 52 | # Create a new season 53 | current_season = self.seasons_manager.add_season({ 54 | 'number': season_num, 55 | 'name': season_name 56 | }) 57 | 58 | # Find episodes for this season 59 | tt_series_div = tt_holder.find('div', class_='tt_series') 60 | tab_content = tt_series_div.find('div', class_='tab-content') 61 | tab_pane = tab_content.find('div', id=f'season-{season_num}') 62 | 63 | episode_list_items = tab_pane.find_all('li') 64 | for ep_li in episode_list_items: 65 | ep_anchor = ep_li.find('a', id=lambda x: x and x.startswith(f'serie-{season_num}_')) 66 | if not ep_anchor: 67 | continue 68 | 69 | ep_num_str = ep_anchor.get('data-num', '') 70 | try: 71 | ep_num = int(ep_num_str.split('x')[1]) 72 | except (IndexError, ValueError): 73 | ep_num = int(ep_anchor.get_text(strip=True)) 74 | 75 | ep_title = ep_anchor.get('data-title', '').strip() 76 | ep_url = ep_anchor.get('data-link', '').strip() 77 | 78 | # Prefer supervideo link from mirrors if available 79 | mirrors_div = ep_li.find('div', class_='mirrors') 80 | supervideo_url = None 81 | if mirrors_div: 82 | supervideo_a = mirrors_div.find('a', class_='mr', text=lambda t: t and 'Supervideo' in t) 83 | if supervideo_a: 84 | supervideo_url = supervideo_a.get('data-link', '').strip() 85 | 86 | if supervideo_url: 87 | ep_url = supervideo_url 88 | 89 | if current_season: 90 | current_season.episodes.add({ 91 | 'number': ep_num, 92 | 'name': ep_title if ep_title else f"Episodio {ep_num}", 93 | 'url': ep_url 94 | }) 95 | 96 | 97 | # ------------- FOR GUI ------------- 98 | def getNumberSeason(self) -> int: 99 | """ 100 | Get the total number of seasons available for the series. 101 | """ 102 | if not self.seasons_manager.seasons: 103 | self.collect_season() 104 | 105 | return len(self.seasons_manager.seasons) 106 | 107 | def getEpisodeSeasons(self, season_number: int) -> list: 108 | """ 109 | Get all episodes for a specific season. 110 | """ 111 | if not self.seasons_manager.seasons: 112 | self.collect_season() 113 | 114 | # Get season directly by its number 115 | season = self.seasons_manager.get_season_by_number(season_number) 116 | return season.episodes.episodes if season else [] 117 | 118 | def selectEpisode(self, season_number: int, episode_index: int) -> dict: 119 | """ 120 | Get information for a specific episode in a specific season. 121 | """ 122 | episodes = self.getEpisodeSeasons(season_number) 123 | if not episodes or episode_index < 0 or episode_index >= len(episodes): 124 | logging.error(f"Episode index {episode_index} is out of range for season {season_number}") 125 | return None 126 | 127 | return episodes[episode_index] -------------------------------------------------------------------------------- /StreamingCommunity/Lib/Downloader/DASH/cdm_helpher.py: -------------------------------------------------------------------------------- 1 | # 25.07.25 2 | 3 | import base64 4 | from urllib.parse import urlencode 5 | 6 | 7 | # External libraries 8 | from curl_cffi import requests 9 | from rich.console import Console 10 | from pywidevine.cdm import Cdm 11 | from pywidevine.device import Device 12 | from pywidevine.pssh import PSSH 13 | 14 | 15 | # Variable 16 | console = Console() 17 | 18 | 19 | def get_widevine_keys(pssh, license_url, cdm_device_path, headers=None, query_params=None): 20 | """ 21 | Extract Widevine CONTENT keys (KID/KEY) from a license using pywidevine. 22 | 23 | Args: 24 | pssh (str): PSSH base64. 25 | license_url (str): Widevine license URL. 26 | cdm_device_path (str): Path to CDM file (device.wvd). 27 | headers (dict): Optional HTTP headers for the license request (from fetch). 28 | query_params (dict): Optional query parameters to append to the URL. 29 | 30 | Returns: 31 | list: List of dicts {'kid': ..., 'key': ...} (only CONTENT keys) or None if error. 32 | """ 33 | if not cdm_device_path: 34 | console.print("[bold red]Invalid CDM device path.[/bold red]") 35 | return None 36 | 37 | try: 38 | device = Device.load(cdm_device_path) 39 | cdm = Cdm.from_device(device) 40 | session_id = cdm.open() 41 | 42 | try: 43 | challenge = cdm.get_license_challenge(session_id, PSSH(pssh)) 44 | 45 | # Build request URL with query params 46 | request_url = license_url 47 | if query_params: 48 | request_url = f"{license_url}?{urlencode(query_params)}" 49 | 50 | # Prepare headers (use original headers from fetch) 51 | req_headers = headers.copy() if headers else {} 52 | request_kwargs = {} 53 | request_kwargs['data'] = challenge 54 | 55 | # Keep original Content-Type or default to octet-stream 56 | if 'Content-Type' not in req_headers: 57 | req_headers['Content-Type'] = 'application/octet-stream' 58 | 59 | # Send license request 60 | try: 61 | # response = httpx.post(license_url, data=challenge, headers=req_headers, content=payload) 62 | response = requests.post(request_url, headers=req_headers, impersonate="chrome124", **request_kwargs) 63 | 64 | except Exception as e: 65 | console.print(f"[bold red]Request error:[/bold red] {e}") 66 | return None 67 | 68 | if response.status_code != 200: 69 | console.print(f"[bold red]License error:[/bold red] {response.status_code}, {response.text}") 70 | console.print({ 71 | "url": license_url, 72 | "headers": req_headers, 73 | "session_id": session_id.hex(), 74 | "pssh": pssh 75 | }) 76 | return None 77 | 78 | # Parse license response 79 | license_bytes = response.content 80 | content_type = response.headers.get("Content-Type", "") 81 | 82 | # Handle JSON response 83 | if "application/json" in content_type: 84 | try: 85 | data = response.json() 86 | if "license" in data: 87 | license_bytes = base64.b64decode(data["license"]) 88 | else: 89 | console.print(f"[bold red]'license' field not found in JSON response: {data}.[/bold red]") 90 | return None 91 | except Exception as e: 92 | console.print(f"[bold red]Error parsing JSON license:[/bold red] {e}") 93 | return None 94 | 95 | if not license_bytes: 96 | console.print("[bold red]License data is empty.[/bold red]") 97 | return None 98 | 99 | # Parse license 100 | try: 101 | cdm.parse_license(session_id, license_bytes) 102 | except Exception as e: 103 | console.print(f"[bold red]Error parsing license:[/bold red] {e}") 104 | return None 105 | 106 | # Extract CONTENT keys 107 | content_keys = [] 108 | for key in cdm.get_keys(session_id): 109 | if key.type == "CONTENT": 110 | kid = key.kid.hex() if isinstance(key.kid, bytes) else str(key.kid) 111 | key_val = key.key.hex() if isinstance(key.key, bytes) else str(key.key) 112 | 113 | content_keys.append({ 114 | 'kid': kid.replace('-', '').strip(), 115 | 'key': key_val.replace('-', '').strip() 116 | }) 117 | 118 | if not content_keys: 119 | console.print("[bold yellow]⚠️ No CONTENT keys found in license.[/bold yellow]") 120 | return None 121 | 122 | return content_keys 123 | 124 | finally: 125 | cdm.close(session_id) 126 | 127 | except Exception as e: 128 | console.print(f"[bold red]CDM error:[/bold red] {e}") 129 | return None -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/animeunity/site.py: -------------------------------------------------------------------------------- 1 | # 10.12.23 2 | 3 | import urllib.parse 4 | 5 | 6 | # External libraries 7 | from rich.console import Console 8 | 9 | 10 | # Internal utilities 11 | from StreamingCommunity.Util.headers import get_userAgent 12 | from StreamingCommunity.Util.http_client import create_client_curl 13 | from StreamingCommunity.Util.table import TVShowManager 14 | from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance 15 | 16 | 17 | # Logic class 18 | from StreamingCommunity.Api.Template.config_loader import site_constant 19 | from StreamingCommunity.Api.Template.Class.SearchType import MediaManager 20 | 21 | 22 | # Variable 23 | console = Console() 24 | media_search_manager = MediaManager() 25 | table_show_manager = TVShowManager() 26 | 27 | 28 | def get_token(user_agent: str) -> dict: 29 | """ 30 | Retrieve session cookies from the site. 31 | """ 32 | response = create_client_curl(headers={'user-agent': user_agent}).get(site_constant.FULL_URL) 33 | response.raise_for_status() 34 | all_cookies = {name: value for name, value in response.cookies.items()} 35 | 36 | return {k: urllib.parse.unquote(v) for k, v in all_cookies.items()} 37 | 38 | 39 | def get_real_title(record: dict) -> str: 40 | """ 41 | Return the most appropriate title from the record. 42 | """ 43 | if record.get('title_eng'): 44 | return record['title_eng'] 45 | elif record.get('title'): 46 | return record['title'] 47 | else: 48 | return record.get('title_it', '') 49 | 50 | 51 | def title_search(query: str) -> int: 52 | """ 53 | Perform anime search on animeunity.so. 54 | """ 55 | if site_constant.TELEGRAM_BOT: 56 | bot = get_bot_instance() 57 | 58 | media_search_manager.clear() 59 | table_show_manager.clear() 60 | seen_titles = set() 61 | choices = [] if site_constant.TELEGRAM_BOT else None 62 | 63 | user_agent = get_userAgent() 64 | data = get_token(user_agent) 65 | 66 | cookies = { 67 | 'XSRF-TOKEN': data.get('XSRF-TOKEN', ''), 68 | 'animeunity_session': data.get('animeunity_session', ''), 69 | } 70 | 71 | headers = { 72 | 'origin': site_constant.FULL_URL, 73 | 'referer': f"{site_constant.FULL_URL}/", 74 | 'user-agent': user_agent, 75 | 'x-xsrf-token': data.get('XSRF-TOKEN', ''), 76 | } 77 | 78 | # First call: /livesearch 79 | try: 80 | response1 = create_client_curl(headers=headers).post(f'{site_constant.FULL_URL}/livesearch', cookies=cookies, data={'title': query}) 81 | response1.raise_for_status() 82 | process_results(response1.json().get('records', []), seen_titles, media_search_manager, choices) 83 | 84 | except Exception as e: 85 | console.print(f"[red]Site: {site_constant.SITE_NAME}, request search error: {e}") 86 | return 0 87 | 88 | # Second call: /archivio/get-animes 89 | try: 90 | json_data = { 91 | 'title': query, 92 | 'type': False, 93 | 'year': False, 94 | 'order': False, 95 | 'status': False, 96 | 'genres': False, 97 | 'offset': 0, 98 | 'dubbed': False, 99 | 'season': False, 100 | } 101 | response2 = create_client_curl(headers=headers).post(f'{site_constant.FULL_URL}/archivio/get-animes', cookies=cookies, json=json_data) 102 | response2.raise_for_status() 103 | process_results(response2.json().get('records', []), seen_titles, media_search_manager, choices) 104 | 105 | except Exception as e: 106 | console.print(f"Site: {site_constant.SITE_NAME}, archivio search error: {e}") 107 | 108 | if site_constant.TELEGRAM_BOT and choices and len(choices) > 0: 109 | bot.send_message("List of results:", choices) 110 | 111 | result_count = media_search_manager.get_length() 112 | if result_count == 0: 113 | console.print(f"Nothing matching was found for: {query}") 114 | 115 | return result_count 116 | 117 | 118 | def process_results(records: list, seen_titles: set, media_manager: MediaManager, choices: list = None) -> None: 119 | """ 120 | Add unique results to the media manager and to choices. 121 | """ 122 | for dict_title in records: 123 | try: 124 | title_id = dict_title.get('id') 125 | if title_id in seen_titles: 126 | continue 127 | 128 | seen_titles.add(title_id) 129 | dict_title['name'] = get_real_title(dict_title) 130 | 131 | media_manager.add_media({ 132 | 'id': title_id, 133 | 'slug': dict_title.get('slug'), 134 | 'name': dict_title.get('name'), 135 | 'type': dict_title.get('type'), 136 | 'status': dict_title.get('status'), 137 | 'episodes_count': dict_title.get('episodes_count'), 138 | 'image': dict_title.get('imageurl') 139 | }) 140 | 141 | if choices is not None: 142 | choice_text = f"{len(choices)} - {dict_title.get('name')} ({dict_title.get('type')}) - Episodes: {dict_title.get('episodes_count')}" 143 | choices.append(choice_text) 144 | except Exception as e: 145 | print(f"Error parsing a title entry: {e}") -------------------------------------------------------------------------------- /StreamingCommunity/Lib/Downloader/DASH/decrypt.py: -------------------------------------------------------------------------------- 1 | # 25.07.25 2 | 3 | import os 4 | import subprocess 5 | import logging 6 | import threading 7 | import time 8 | 9 | 10 | # External libraries 11 | from rich.console import Console 12 | from tqdm import tqdm 13 | 14 | 15 | # Internal utilities 16 | from StreamingCommunity.Util.config_json import config_manager 17 | from StreamingCommunity.Util.os import get_mp4decrypt_path 18 | from StreamingCommunity.Util.color import Colors 19 | 20 | 21 | # Variable 22 | console = Console() 23 | extension_output = config_manager.get("M3U8_CONVERSION", "extension") 24 | CLEANUP_TMP = config_manager.get_bool('M3U8_DOWNLOAD', 'cleanup_tmp_folder') 25 | SHOW_DECRYPT_PROGRESS = False 26 | 27 | 28 | def decrypt_with_mp4decrypt(type, encrypted_path, kid, key, output_path=None, cleanup=True): 29 | """ 30 | Decrypt an mp4/m4s file using mp4decrypt. 31 | 32 | Args: 33 | type (str): Type of file ('video' or 'audio'). 34 | encrypted_path (str): Path to encrypted file. 35 | kid (str): Hexadecimal KID. 36 | key (str): Hexadecimal key. 37 | output_path (str): Output decrypted file path (optional). 38 | cleanup (bool): If True, remove temporary files after decryption. 39 | 40 | Returns: 41 | str: Path to decrypted file, or None if error. 42 | """ 43 | 44 | # Check if input file exists 45 | if not os.path.isfile(encrypted_path): 46 | console.print(f"[bold red] Encrypted file not found: {encrypted_path}[/bold red]") 47 | return None 48 | 49 | # Check if kid and key are valid hex 50 | try: 51 | bytes.fromhex(kid) 52 | bytes.fromhex(key) 53 | except Exception: 54 | console.print("[bold red] Invalid KID or KEY (not hex).[/bold red]") 55 | return None 56 | 57 | if not output_path: 58 | output_path = os.path.splitext(encrypted_path)[0] + f"_decrypted.{extension_output}" 59 | 60 | # Get file size for progress tracking 61 | file_size = os.path.getsize(encrypted_path) 62 | 63 | key_format = f"{kid.lower()}:{key.lower()}" 64 | cmd = [get_mp4decrypt_path(), "--key", key_format, encrypted_path, output_path] 65 | logging.info(f"Running command: {' '.join(cmd)}") 66 | 67 | progress_bar = None 68 | monitor_thread = None 69 | 70 | if SHOW_DECRYPT_PROGRESS: 71 | bar_format = ( 72 | f"{Colors.YELLOW}DECRYPT{Colors.CYAN} {type}{Colors.WHITE}: " 73 | f"{Colors.MAGENTA}{{bar:40}} " 74 | f"{Colors.LIGHT_GREEN}{{n_fmt}}{Colors.WHITE}/{Colors.CYAN}{{total_fmt}} " 75 | f"{Colors.DARK_GRAY}[{Colors.YELLOW}{{elapsed}}{Colors.WHITE} < {Colors.CYAN}{{remaining}}{Colors.DARK_GRAY}] " 76 | f"{Colors.WHITE}{{postfix}}" 77 | ) 78 | 79 | progress_bar = tqdm( 80 | total=100, 81 | bar_format=bar_format, 82 | unit="", 83 | ncols=150 84 | ) 85 | 86 | def monitor_output_file(): 87 | """Monitor output file growth and update progress bar.""" 88 | last_size = 0 89 | while True: 90 | if os.path.exists(output_path): 91 | current_size = os.path.getsize(output_path) 92 | if current_size > 0: 93 | progress_percent = min(int((current_size / file_size) * 100), 100) 94 | progress_bar.n = progress_percent 95 | progress_bar.refresh() 96 | 97 | if current_size == last_size and current_size > 0: 98 | # File stopped growing, likely finished 99 | break 100 | 101 | last_size = current_size 102 | 103 | time.sleep(0.1) 104 | 105 | # Start monitoring thread 106 | monitor_thread = threading.Thread(target=monitor_output_file, daemon=True) 107 | monitor_thread.start() 108 | 109 | try: 110 | result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) 111 | except Exception as e: 112 | if progress_bar: 113 | progress_bar.close() 114 | console.print(f"[bold red] mp4decrypt execution failed: {e}[/bold red]") 115 | return None 116 | 117 | if progress_bar: 118 | progress_bar.n = 100 119 | progress_bar.refresh() 120 | progress_bar.close() 121 | 122 | if result.returncode == 0 and os.path.exists(output_path): 123 | 124 | # Cleanup temporary files 125 | if cleanup and CLEANUP_TMP: 126 | if os.path.exists(encrypted_path): 127 | os.remove(encrypted_path) 128 | 129 | temp_dec = os.path.splitext(encrypted_path)[0] + f"_decrypted.{extension_output}" 130 | 131 | # Do not delete the final output! 132 | if temp_dec != output_path and os.path.exists(temp_dec): 133 | os.remove(temp_dec) 134 | 135 | # Check if output file is not empty 136 | if os.path.getsize(output_path) == 0: 137 | console.print(f"[bold red] Decrypted file is empty: {output_path}[/bold red]") 138 | return None 139 | 140 | return output_path 141 | 142 | else: 143 | console.print(f"[bold red] mp4decrypt failed:[/bold red] {result.stderr}") 144 | return None -------------------------------------------------------------------------------- /GUI/searchapp/api/animeunity.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | import importlib 5 | from typing import List, Optional 6 | 7 | 8 | # Internal utilities 9 | from .base import BaseStreamingAPI, MediaItem, Season, Episode 10 | 11 | 12 | # External utilities 13 | from StreamingCommunity.Util.config_json import config_manager 14 | from StreamingCommunity.Api.Site.animeunity.util.ScrapeSerie import ScrapeSerieAnime 15 | 16 | 17 | 18 | class AnimeUnityAPI(BaseStreamingAPI): 19 | def __init__(self): 20 | super().__init__() 21 | self.site_name = "animeunity" 22 | self._load_config() 23 | self._search_fn = None 24 | 25 | def _load_config(self): 26 | """Load site configuration.""" 27 | self.base_url = (config_manager.get_site("animeunity", "full_url") or "").rstrip("/") 28 | 29 | def _get_search_fn(self): 30 | """Lazy load the search function.""" 31 | if self._search_fn is None: 32 | module = importlib.import_module("StreamingCommunity.Api.Site.animeunity") 33 | self._search_fn = getattr(module, "search") 34 | return self._search_fn 35 | 36 | def search(self, query: str) -> List[MediaItem]: 37 | """ 38 | Search for content on AnimeUnity. 39 | 40 | Args: 41 | query: Search term 42 | 43 | Returns: 44 | List of MediaItem objects 45 | """ 46 | try: 47 | search_fn = self._get_search_fn() 48 | database = search_fn(query, get_onlyDatabase=True) 49 | 50 | results = [] 51 | if database and hasattr(database, 'media_list'): 52 | for element in database.media_list: 53 | item_dict = element.__dict__.copy() if hasattr(element, '__dict__') else {} 54 | 55 | media_item = MediaItem( 56 | id=item_dict.get('id'), 57 | title=item_dict.get('name'), 58 | slug=item_dict.get('slug', ''), 59 | type=item_dict.get('type'), 60 | url=item_dict.get('url'), 61 | poster=item_dict.get('image'), 62 | raw_data=item_dict 63 | ) 64 | results.append(media_item) 65 | 66 | return results 67 | 68 | except Exception as e: 69 | raise Exception(f"AnimeUnity search error: {e}") 70 | 71 | def get_series_metadata(self, media_item: MediaItem) -> Optional[List[Season]]: 72 | """ 73 | Get seasons and episodes for an AnimeUnity series. 74 | Note: AnimeUnity typically has single season anime. 75 | 76 | Args: 77 | media_item: MediaItem to get metadata for 78 | 79 | Returns: 80 | List of Season objects (usually one season), or None if not a series 81 | """ 82 | # Check if it's a movie or OVA 83 | if media_item.is_movie: 84 | return None 85 | 86 | try: 87 | scraper = ScrapeSerieAnime(self.base_url) 88 | scraper.setup(series_name=media_item.slug, media_id=media_item.id) 89 | 90 | episodes_count = scraper.get_count_episodes() 91 | if not episodes_count: 92 | return None 93 | 94 | # AnimeUnity typically has single season 95 | episodes = [] 96 | for ep_num in range(1, episodes_count + 1): 97 | episode = Episode( 98 | number=ep_num, 99 | name=f"Episodio {ep_num}", 100 | id=ep_num 101 | ) 102 | episodes.append(episode) 103 | 104 | season = Season(number=1, episodes=episodes) 105 | return [season] 106 | 107 | except Exception as e: 108 | raise Exception(f"Error getting series metadata: {e}") 109 | 110 | def start_download(self, media_item: MediaItem, season: Optional[str] = None, episodes: Optional[str] = None) -> bool: 111 | """ 112 | Start downloading from AnimeUnity. 113 | 114 | Args: 115 | media_item: MediaItem to download 116 | season: Season number (typically 1 for anime) 117 | episodes: Episode selection 118 | 119 | Returns: 120 | True if download started successfully 121 | """ 122 | try: 123 | search_fn = self._get_search_fn() 124 | 125 | # Prepare direct_item from MediaItem 126 | direct_item = media_item.raw_data or media_item.to_dict() 127 | 128 | # For AnimeUnity, we only use episode selection 129 | selections = None 130 | if episodes: 131 | selections = {'episode': episodes} 132 | 133 | elif not media_item.is_movie: 134 | # Default: download all episodes 135 | selections = {'episode': '*'} 136 | 137 | # Execute download 138 | search_fn(direct_item=direct_item, selections=selections) 139 | return True 140 | 141 | except Exception as e: 142 | raise Exception(f"Download error: {e}") -------------------------------------------------------------------------------- /StreamingCommunity/Api/Template/site.py: -------------------------------------------------------------------------------- 1 | # 19.06.24 2 | 3 | # External library 4 | from rich.console import Console 5 | 6 | 7 | # Internal utilities 8 | from StreamingCommunity.Api.Template.config_loader import site_constant 9 | from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance 10 | 11 | 12 | # Variable 13 | console = Console() 14 | available_colors = ['red', 'magenta', 'yellow', 'cyan', 'green', 'blue', 'white'] 15 | column_to_hide = ['Slug', 'Sub_ita', 'Last_air_date', 'Seasons_count', 'Url', 'Image', 'Path_id'] 16 | 17 | 18 | def get_select_title(table_show_manager, media_search_manager, num_results_available): 19 | """ 20 | Display a selection of titles and prompt the user to choose one. 21 | Handles both console and Telegram bot input. 22 | 23 | Parameters: 24 | table_show_manager: Manager for console table display. 25 | media_search_manager: Manager holding the list of media items. 26 | num_results_available (int): The number of media items available for selection. 27 | 28 | Returns: 29 | MediaItem: The selected media item, or None if no selection is made or an error occurs. 30 | """ 31 | if not media_search_manager.media_list: 32 | 33 | # console.print("\n[red]No media items available.") 34 | return None 35 | 36 | if site_constant.TELEGRAM_BOT: 37 | bot = get_bot_instance() 38 | prompt_message = f"Inserisci il numero del titolo che vuoi selezionare (da 0 a {num_results_available - 1}):" 39 | 40 | user_input_str = bot.ask( 41 | "select_title_from_list_number", 42 | prompt_message, 43 | None 44 | ) 45 | 46 | if user_input_str is None: 47 | bot.send_message("Timeout: nessuna selezione ricevuta.", None) 48 | return None 49 | 50 | try: 51 | chosen_index = int(user_input_str) 52 | if 0 <= chosen_index < num_results_available: 53 | selected_item = media_search_manager.get(chosen_index) 54 | if selected_item: 55 | return selected_item 56 | 57 | else: 58 | bot.send_message(f"Errore interno: Impossibile recuperare il titolo con indice {chosen_index}.", None) 59 | return None 60 | else: 61 | bot.send_message(f"Selezione '{chosen_index}' non valida. Inserisci un numero compreso tra 0 e {num_results_available - 1}.", None) 62 | return None 63 | 64 | except ValueError: 65 | bot.send_message(f"Input '{user_input_str}' non valido. Devi inserire un numero.", None) 66 | return None 67 | 68 | except Exception as e: 69 | bot.send_message(f"Si è verificato un errore durante la selezione: {e}", None) 70 | return None 71 | 72 | else: 73 | 74 | # Logica originale per la console 75 | if not media_search_manager.media_list: 76 | console.print("\n[red]No media items available.") 77 | return None 78 | 79 | first_media_item = media_search_manager.media_list[0] 80 | column_info = {"Index": {'color': available_colors[0]}} 81 | 82 | color_index = 1 83 | for key in first_media_item.__dict__.keys(): 84 | 85 | if key.capitalize() in column_to_hide: 86 | continue 87 | 88 | if key in ('id', 'type', 'name', 'score'): 89 | if key == 'type': 90 | column_info["Type"] = {'color': 'yellow'} 91 | 92 | elif key == 'name': 93 | column_info["Name"] = {'color': 'magenta'} 94 | elif key == 'score': 95 | column_info["Score"] = {'color': 'cyan'} 96 | 97 | else: 98 | column_info[key.capitalize()] = {'color': available_colors[color_index % len(available_colors)]} 99 | color_index += 1 100 | 101 | table_show_manager.clear() 102 | table_show_manager.add_column(column_info) 103 | 104 | for i, media in enumerate(media_search_manager.media_list): 105 | media_dict = {'Index': str(i)} 106 | for key in first_media_item.__dict__.keys(): 107 | if key.capitalize() in column_to_hide: 108 | continue 109 | media_dict[key.capitalize()] = str(getattr(media, key)) 110 | table_show_manager.add_tv_show(media_dict) 111 | 112 | last_command_str = table_show_manager.run(force_int_input=True, max_int_input=len(media_search_manager.media_list)) 113 | table_show_manager.clear() 114 | 115 | if last_command_str is None or last_command_str.lower() in ["q", "quit"]: 116 | console.print("\n[red]Selezione annullata o uscita.") 117 | return None 118 | 119 | try: 120 | 121 | selected_index = int(last_command_str) 122 | 123 | if 0 <= selected_index < len(media_search_manager.media_list): 124 | return media_search_manager.get(selected_index) 125 | 126 | else: 127 | console.print("\n[red]Indice errato o non valido.") 128 | return None 129 | 130 | except ValueError: 131 | console.print("\n[red]Input non numerico ricevuto dalla tabella.") 132 | return None -------------------------------------------------------------------------------- /GUI/searchapp/api/base.py: -------------------------------------------------------------------------------- 1 | # 06-06-2025 By @FrancescoGrazioso -> "https://github.com/FrancescoGrazioso" 2 | 3 | 4 | from abc import ABC, abstractmethod 5 | from typing import Any, Dict, List, Optional 6 | from dataclasses import dataclass 7 | 8 | 9 | @dataclass 10 | class MediaItem: 11 | """Standardized media item representation.""" 12 | id: Any 13 | title: str 14 | slug: str 15 | type: str # 'film', 'series', 'ova', etc. 16 | url: Optional[str] = None 17 | poster: Optional[str] = None 18 | release_date: Optional[str] = None 19 | year: Optional[int] = None 20 | raw_data: Optional[Dict[str, Any]] = None 21 | 22 | @property 23 | def is_movie(self) -> bool: 24 | return self.type.lower() in ['film', 'movie', 'ova'] 25 | 26 | def to_dict(self) -> Dict[str, Any]: 27 | return { 28 | 'id': self.id, 29 | 'title': self.title, 30 | 'slug': self.slug, 31 | 'type': self.type, 32 | 'url': self.url, 33 | 'poster': self.poster, 34 | 'release_date': self.release_date, 35 | 'year': self.year, 36 | 'raw_data': self.raw_data, 37 | 'is_movie': self.is_movie 38 | } 39 | 40 | 41 | @dataclass 42 | class Episode: 43 | """Episode information.""" 44 | number: int 45 | name: str 46 | id: Optional[Any] = None 47 | 48 | def to_dict(self) -> Dict[str, Any]: 49 | return { 50 | 'number': self.number, 51 | 'name': self.name, 52 | 'id': self.id 53 | } 54 | 55 | 56 | @dataclass 57 | class Season: 58 | """Season information.""" 59 | number: int 60 | episodes: List[Episode] 61 | 62 | @property 63 | def episode_count(self) -> int: 64 | return len(self.episodes) 65 | 66 | def to_dict(self) -> Dict[str, Any]: 67 | return { 68 | 'number': self.number, 69 | 'episodes': [ep.to_dict() for ep in self.episodes], 70 | 'episode_count': self.episode_count 71 | } 72 | 73 | 74 | class BaseStreamingAPI(ABC): 75 | """Base class for all streaming site APIs.""" 76 | 77 | def __init__(self): 78 | self.site_name: str = "" 79 | self.base_url: str = "" 80 | 81 | @abstractmethod 82 | def search(self, query: str) -> List[MediaItem]: 83 | """ 84 | Search for content on the streaming site. 85 | 86 | Args: 87 | query: Search term 88 | 89 | Returns: 90 | List of MediaItem objects 91 | """ 92 | pass 93 | 94 | @abstractmethod 95 | def get_series_metadata(self, media_item: MediaItem) -> Optional[List[Season]]: 96 | """ 97 | Get seasons and episodes for a series. 98 | 99 | Args: 100 | media_item: MediaItem to get metadata for 101 | 102 | Returns: 103 | List of Season objects, or None if not a series 104 | """ 105 | pass 106 | 107 | @abstractmethod 108 | def start_download(self, media_item: MediaItem, season: Optional[str] = None, episodes: Optional[str] = None) -> bool: 109 | """ 110 | Start downloading content. 111 | 112 | Args: 113 | media_item: MediaItem to download 114 | season: Season number (for series) 115 | episodes: Episode selection (e.g., "1-5" or "1,3,5" or "*" for all) 116 | 117 | Returns: 118 | True if download started successfully 119 | """ 120 | pass 121 | 122 | def ensure_complete_item(self, partial_item: Dict[str, Any]) -> MediaItem: 123 | """ 124 | Ensure a media item has all required fields by searching the database. 125 | 126 | Args: 127 | partial_item: Dictionary with partial item data 128 | 129 | Returns: 130 | Complete MediaItem object 131 | """ 132 | # If already complete, convert to MediaItem 133 | if partial_item.get('id') and (partial_item.get('slug') or partial_item.get('url')): 134 | return self._dict_to_media_item(partial_item) 135 | 136 | # Try to find in database 137 | query = (partial_item.get('title') or partial_item.get('name') or partial_item.get('slug') or partial_item.get('display_title')) 138 | 139 | if query: 140 | results = self.search(query) 141 | if results: 142 | wanted_slug = partial_item.get('slug') 143 | if wanted_slug: 144 | for item in results: 145 | if item.slug == wanted_slug: 146 | return item 147 | 148 | return results[0] 149 | 150 | # Fallback: return partial item 151 | return self._dict_to_media_item(partial_item) 152 | 153 | def _dict_to_media_item(self, data: Dict[str, Any]) -> MediaItem: 154 | """Convert dictionary to MediaItem.""" 155 | return MediaItem( 156 | id=data.get('id'), 157 | title=data.get('title') or data.get('name') or 'Unknown', 158 | slug=data.get('slug') or '', 159 | type=data.get('type') or data.get('media_type') or 'unknown', 160 | url=data.get('url'), 161 | poster=data.get('poster') or data.get('poster_url') or data.get('image'), 162 | release_date=data.get('release_date') or data.get('first_air_date'), 163 | year=data.get('year'), 164 | raw_data=data 165 | ) -------------------------------------------------------------------------------- /StreamingCommunity/Api/Site/guardaserie/__init__.py: -------------------------------------------------------------------------------- 1 | # 09.06.24 2 | 3 | import sys 4 | import subprocess 5 | from urllib.parse import quote_plus 6 | 7 | 8 | # External library 9 | from rich.console import Console 10 | from rich.prompt import Prompt 11 | 12 | 13 | # Internal utilities 14 | from StreamingCommunity.Api.Template import get_select_title 15 | from StreamingCommunity.Api.Template.config_loader import site_constant 16 | from StreamingCommunity.Api.Template.Class.SearchType import MediaItem 17 | from StreamingCommunity.TelegramHelp.telegram_bot import get_bot_instance 18 | 19 | 20 | # Logic class 21 | from .site import title_search, media_search_manager, table_show_manager 22 | from .series import download_series 23 | 24 | 25 | # Variable 26 | indice = 4 27 | _useFor = "Serie" 28 | _priority = 0 29 | _engineDownload = "hls" 30 | _deprecate = False 31 | 32 | msg = Prompt() 33 | console = Console() 34 | 35 | 36 | def get_user_input(string_to_search: str = None): 37 | """ 38 | Asks the user to input a search term. 39 | Handles both Telegram bot input and direct input. 40 | If string_to_search is provided, it's returned directly (after stripping). 41 | """ 42 | if string_to_search is not None: 43 | return string_to_search.strip() 44 | 45 | if site_constant.TELEGRAM_BOT: 46 | bot = get_bot_instance() 47 | user_response = bot.ask( 48 | "key_search", # Request type 49 | "Enter the search term\nor type 'back' to return to the menu: ", 50 | None 51 | ) 52 | 53 | if user_response is None: 54 | bot.send_message("Timeout: No search term entered.", None) 55 | return None 56 | 57 | if user_response.lower() == 'back': 58 | bot.send_message("Returning to the main menu...", None) 59 | 60 | try: 61 | # Restart the script 62 | subprocess.Popen([sys.executable] + sys.argv) 63 | sys.exit() 64 | 65 | except Exception as e: 66 | bot.send_message(f"Error during restart attempt: {e}", None) 67 | return None # Return None if restart fails 68 | 69 | return user_response.strip() 70 | 71 | else: 72 | return msg.ask(f"\n[purple]Insert a word to search in [green]{site_constant.SITE_NAME}").strip() 73 | 74 | 75 | def process_search_result(select_title, selections=None): 76 | """ 77 | Handles the search result and initiates the download for either a film or series. 78 | 79 | Parameters: 80 | select_title (MediaItem): The selected media item 81 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 82 | {'season': season_selection, 'episode': episode_selection} 83 | 84 | Returns: 85 | bool: True if processing was successful, False otherwise 86 | """ 87 | if not select_title: 88 | if site_constant.TELEGRAM_BOT: 89 | bot = get_bot_instance() 90 | bot.send_message("No title selected or selection cancelled.", None) 91 | else: 92 | console.print("[yellow]No title selected or selection cancelled.") 93 | return False 94 | 95 | season_selection = None 96 | episode_selection = None 97 | 98 | if selections: 99 | season_selection = selections.get('season') 100 | episode_selection = selections.get('episode') 101 | 102 | download_series(select_title, season_selection, episode_selection) 103 | media_search_manager.clear() 104 | table_show_manager.clear() 105 | return True 106 | 107 | 108 | def search(string_to_search: str = None, get_onlyDatabase: bool = False, direct_item: dict = None, selections: dict = None): 109 | """ 110 | Main function of the application for search. 111 | 112 | Parameters: 113 | string_to_search (str, optional): String to search for 114 | get_onlyDatabase (bool, optional): If True, return only the database object 115 | direct_item (dict, optional): Direct item to process (bypass search) 116 | selections (dict, optional): Dictionary containing selection inputs that bypass manual input 117 | {'season': season_selection, 'episode': episode_selection} 118 | """ 119 | bot = None 120 | if site_constant.TELEGRAM_BOT: 121 | bot = get_bot_instance() 122 | 123 | if direct_item: 124 | select_title = MediaItem(**direct_item) 125 | result = process_search_result(select_title, selections) 126 | return result 127 | 128 | # Get the user input for the search term 129 | actual_search_query = get_user_input(string_to_search) 130 | 131 | # Handle empty input 132 | if not actual_search_query: 133 | if bot: 134 | if actual_search_query is None: 135 | bot.send_message("Search term not provided or operation cancelled. Returning.", None) 136 | return False 137 | 138 | # Search on database 139 | len_database = title_search(quote_plus(actual_search_query)) 140 | 141 | # If only the database is needed, return the manager 142 | if get_onlyDatabase: 143 | return media_search_manager 144 | 145 | if len_database > 0: 146 | select_title = get_select_title(table_show_manager, media_search_manager, len_database) 147 | result = process_search_result(select_title, selections) 148 | return result 149 | 150 | else: 151 | if bot: 152 | bot.send_message(f"No results found for: '{actual_search_query}'", None) 153 | else: 154 | console.print(f"\n[red]Nothing matching was found for[white]: [purple]{actual_search_query}") 155 | return False --------------------------------------------------------------------------------