├── 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 | - 
--------------------------------------------------------------------------------
/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 |
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
--------------------------------------------------------------------------------