From f498daf6feea25e5fa2a37aba767263967cb01a9 Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Sat, 27 Jan 2024 00:08:49 +0530 Subject: [PATCH] Add Premiumize Streaming provider --- api/main.py | 7 + db/config.py | 2 + db/schemas.py | 9 +- deployment/deployment.yaml | 18 ++- resources/html/configure.html | 3 +- resources/js/config_script.js | 4 +- streaming_providers/premiumize/__init__.py | 0 streaming_providers/premiumize/api.py | 34 +++++ streaming_providers/premiumize/client.py | 149 ++++++++++++++++++++ streaming_providers/premiumize/utils.py | 155 +++++++++++++++++++++ utils/parser.py | 6 + 11 files changed, 380 insertions(+), 7 deletions(-) create mode 100644 streaming_providers/premiumize/__init__.py create mode 100644 streaming_providers/premiumize/api.py create mode 100644 streaming_providers/premiumize/client.py create mode 100644 streaming_providers/premiumize/utils.py diff --git a/api/main.py b/api/main.py index 9c10be5..f2d03f3 100644 --- a/api/main.py +++ b/api/main.py @@ -28,6 +28,8 @@ from streaming_providers.debridlink.utils import get_direct_link_from_debridlink from streaming_providers.exceptions import ProviderException from streaming_providers.offcloud.utils import get_direct_link_from_offcloud from streaming_providers.pikpak.utils import get_direct_link_from_pikpak +from streaming_providers.premiumize.api import router as premiumize_router +from streaming_providers.premiumize.utils import get_direct_link_from_premiumize from streaming_providers.realdebrid.api import router as realdebrid_router from streaming_providers.realdebrid.utils import get_direct_link_from_realdebrid from streaming_providers.seedr.api import router as seedr_router @@ -424,6 +426,10 @@ async def streaming_provider_endpoint( video_url = get_direct_link_from_torbox( info_hash, magnet_link, user_data, filename, 1, 0 ) + elif user_data.streaming_provider.service == "premiumize": + video_url = get_direct_link_from_premiumize( + info_hash, magnet_link, user_data, stream.torrent_name, filename, 1, 0 + ) else: video_url = get_direct_link_from_debridlink( info_hash, magnet_link, user_data, stream, episode_data, 1, 0 @@ -507,3 +513,4 @@ async def add_tv_metadata(metadata: schemas.TVMetaData): app.include_router(seedr_router, prefix="/seedr", tags=["seedr"]) app.include_router(realdebrid_router, prefix="/realdebrid", tags=["realdebrid"]) app.include_router(debridlink_router, prefix="/debridlink", tags=["debridlink"]) +app.include_router(premiumize_router, prefix="/premiumize", tags=["premiumize"]) diff --git a/db/config.py b/db/config.py index fa95fe0..107f64e 100644 --- a/db/config.py +++ b/db/config.py @@ -10,6 +10,8 @@ class Settings(BaseSettings): enable_scrapper: bool = False poster_cache_path: str = "resources/poster_cache" torrentio_url: str = "https://torrentio.strem.fun" + premiumize_oauth_client_id: str | None = None + premiumize_oauth_client_secret: str | None = None class Config: env_file = ".env" diff --git a/db/schemas.py b/db/schemas.py index 36078ee..9b25801 100644 --- a/db/schemas.py +++ b/db/schemas.py @@ -62,7 +62,14 @@ class Streams(BaseModel): class StreamingProvider(BaseModel): service: Literal[ - "realdebrid", "seedr", "debridlink", "alldebrid", "offcloud", "pikpak", "torbox" + "realdebrid", + "seedr", + "debridlink", + "alldebrid", + "offcloud", + "pikpak", + "torbox", + "premiumize", ] token: str | None = None username: str | None = None diff --git a/deployment/deployment.yaml b/deployment/deployment.yaml index a6ae78d..7769beb 100644 --- a/deployment/deployment.yaml +++ b/deployment/deployment.yaml @@ -37,6 +37,16 @@ spec: secretKeyRef: name: mediafusion-secrets key: SECRET_KEY + - name: PREMIUMIZE_OAUTH_CLIENT_ID + valueFrom: + secretKeyRef: + name: mediafusion-secrets + key: PREMIUMIZE_OAUTH_CLIENT_ID + - name: PREMIUMIZE_OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: mediafusion-secrets + key: PREMIUMIZE_OAUTH_CLIENT_SECRET - name: PROWLARR_API_KEY valueFrom: secretKeyRef: @@ -120,10 +130,10 @@ spec: image: ghcr.io/hotio/prowlarr:latest resources: requests: - memory: "512Mi" + memory: "256Mi" cpu: "250m" limits: - memory: "1Gi" + memory: "512Mi" cpu: "500m" env: - name: PUID @@ -231,7 +241,7 @@ kind: CronJob metadata: name: tamil-blasters-scraper spec: - schedule: "0 */3 * * *" # At minute 0 past every 3rd hour + schedule: "0 */6 * * *" # At minute 0 past every 6th hour jobTemplate: spec: template: @@ -263,7 +273,7 @@ kind: CronJob metadata: name: tamilmv-scraper spec: - schedule: "30 */3 * * *" # At minute 30 past every 3rd hour + schedule: "30 */6 * * *" # At minute 30 past every 6th hour jobTemplate: spec: template: diff --git a/resources/html/configure.html b/resources/html/configure.html index 814f9ce..715708a 100644 --- a/resources/html/configure.html +++ b/resources/html/configure.html @@ -36,6 +36,7 @@ + @@ -116,7 +117,7 @@ user_data.selected_catalogs %}checked{% endif %}> diff --git a/resources/js/config_script.js b/resources/js/config_script.js index e0a2ad7..681444b 100644 --- a/resources/js/config_script.js +++ b/resources/js/config_script.js @@ -103,7 +103,7 @@ function adjustOAuthSectionDisplay() { const providerToken = document.getElementById('provider_token').value; setOAuthBtnTextContent(provider) - if ((provider === 'seedr' || provider === 'realdebrid' || provider === 'debridlink') && !providerToken) { + if ((provider === 'seedr' || provider === 'realdebrid' || provider === 'debridlink' || provider === "premiumize") && !providerToken) { setElementDisplay('oauth_section', 'block'); } else { setElementDisplay('oauth_section', 'none'); @@ -181,6 +181,8 @@ oAuthBtn.addEventListener('click', async function () { await initiateOAuthFlow('/realdebrid/get-device-code', '/realdebrid/authorize'); } else if (provider === 'debridlink') { await initiateOAuthFlow('/debridlink/get-device-code', '/debridlink/authorize') + } else if (provider === 'premiumize') { + return window.location.href = "/premiumize/authorize"; } }); diff --git a/streaming_providers/premiumize/__init__.py b/streaming_providers/premiumize/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streaming_providers/premiumize/api.py b/streaming_providers/premiumize/api.py new file mode 100644 index 0000000..1edca85 --- /dev/null +++ b/streaming_providers/premiumize/api.py @@ -0,0 +1,34 @@ +from fastapi import APIRouter +from fastapi.responses import RedirectResponse + +from db import schemas +from streaming_providers.premiumize.client import Premiumize +from utils import crypto + +router = APIRouter() +headers = { + "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", + "Pragma": "no-cache", + "Expires": "0", +} + + +@router.get("/authorize") +async def authorize(): + if not Premiumize.OAUTH_CLIENT_ID or not Premiumize.OAUTH_CLIENT_SECRET: + return {"error": "Premiumize OAuth not configured"} + + premiumize_client = Premiumize() + return RedirectResponse(premiumize_client.get_authorization_url(), headers=headers) + + +@router.get("/oauth2_redirect") +async def oauth2_redirect(code: str): + premiumize_client = Premiumize() + token_data = premiumize_client.get_token(code) + token = premiumize_client.encode_token_data(token_data["access_token"]) + user_data = schemas.UserData( + streaming_provider=schemas.StreamingProvider(service="premiumize", token=token) + ) + encrypted_str = crypto.encrypt_user_data(user_data) + return RedirectResponse(f"/{encrypted_str}/configure", headers=headers) diff --git a/streaming_providers/premiumize/client.py b/streaming_providers/premiumize/client.py new file mode 100644 index 0000000..fbc34c9 --- /dev/null +++ b/streaming_providers/premiumize/client.py @@ -0,0 +1,149 @@ +from base64 import b64encode, b64decode +from typing import Any +from urllib.parse import quote_plus +from uuid import uuid4 + +from db.config import settings +from streaming_providers.debrid_client import DebridClient +from streaming_providers.exceptions import ProviderException + + +class Premiumize(DebridClient): + BASE_URL = "https://www.premiumize.me/api" + OAUTH_TOKEN_URL = "https://www.premiumize.me/token" + OAUTH_URL = "https://www.premiumize.me/authorize" + REDIRECT_URI = f"{settings.host_url}/premiumize/oauth2_redirect" + + OAUTH_CLIENT_ID = settings.premiumize_oauth_client_id + OAUTH_CLIENT_SECRET = settings.premiumize_oauth_client_secret + + def _handle_service_specific_errors(self, error): + pass + + def initialize_headers(self): + if self.token: + token_data = self.decode_token_str(self.token) + self.headers = {"Authorization": f"Bearer {token_data['access_token']}"} + + @staticmethod + def encode_token_data(access_token: str): + """ + Premiumize support grant_type device_code which has 10years of token expiration. + """ + return b64encode(str(access_token).encode()).decode() + + @staticmethod + def decode_token_str(token: str) -> dict[str, str]: + try: + access_token = b64decode(token).decode() + except ValueError: + raise ProviderException("Invalid token", "invalid_token.mp4") + return {"access_token": access_token} + + def get_authorization_url(self) -> str: + state = uuid4().hex + return f"{self.OAUTH_URL}?client_id={self.OAUTH_CLIENT_ID}&response_type=code&redirect_uri={quote_plus(self.REDIRECT_URI)}&state={state}" + + def get_token(self, code): + return self._make_request( + "POST", + self.OAUTH_TOKEN_URL, + data={ + "client_id": self.OAUTH_CLIENT_ID, + "client_secret": self.OAUTH_CLIENT_SECRET, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": self.REDIRECT_URI, + }, + ) + + def add_magent_link(self, magnet_link: str, folder_id: str = None): + return self._make_request( + "POST", + f"{self.BASE_URL}/transfer/create", + data={"src": magnet_link, "folder_id": folder_id}, + ) + + def create_folder(self, name, parent_id=None): + return self._make_request( + "POST", + f"{self.BASE_URL}/folder/create", + data={"name": name, "parent_id": parent_id}, + ) + + def get_transfer_list(self): + return self._make_request("GET", f"{self.BASE_URL}/transfer/list") + + def get_user_torrent_list(self): + return self._make_request("GET", f"{self.BASE_URL}/item/listall") + + def get_torrent_info(self, torrent_id): + transfer_list = self.get_transfer_list() + torrent_info = next( + ( + torrent + for torrent in transfer_list["transfers"] + if torrent["id"] == torrent_id + ), + None, + ) + return torrent_info + + def get_folder_list(self, folder_id: str = None): + return self._make_request( + "GET", + f"{self.BASE_URL}/folder/list", + params={"id": folder_id} if folder_id else None, + ) + + def delete_torrent(self, torrent_id): + return self._make_request( + "POST", f"{self.BASE_URL}/transfer/delete", data={"id": torrent_id} + ) + + def get_torrent_instant_availability(self, torrent_hashes: list[str]): + return self._make_request( + "GET", f"{self.BASE_URL}/cache/check", params={"items[]": torrent_hashes} + ) + + def create_download_link(self, link): + response = self._make_request( + "POST", + f"{self.BASE_URL}/transfer/directdl", + data={"link": link}, + is_expected_to_fail=True, + ) + if "download" in response: + return response + + if "error_code" in response: + if response["error_code"] == 23: + raise ProviderException( + "Exceed remote traffic limit", "exceed_remote_traffic_limit.mp4" + ) + raise ProviderException( + f"Failed to create download link. response: {response}", "api_error.mp4" + ) + + def disable_access_token(self): + pass + + def get_available_torrent( + self, info_hash: str, torrent_name + ) -> dict[str, Any] | None: + torrent_list_response = self.get_transfer_list() + if torrent_list_response.get("status") != "success": + if torrent_list_response.get("message") == "Not logged in.": + raise ProviderException("Premiumize is not logged in.", "ap.mp4") + raise ProviderException( + "Failed to get torrent info from Premiumize", "transfer_error.mp4" + ) + + available_torrents = torrent_list_response["transfers"] + for torrent in available_torrents: + if ( + info_hash in torrent["src"] + or info_hash == torrent["name"] + or torrent_name == torrent["name"] + ): + return torrent diff --git a/streaming_providers/premiumize/utils.py b/streaming_providers/premiumize/utils.py new file mode 100644 index 0000000..9b31208 --- /dev/null +++ b/streaming_providers/premiumize/utils.py @@ -0,0 +1,155 @@ +from datetime import datetime +from typing import Any + +from thefuzz import fuzz + +from db.models import TorrentStreams +from db.schemas import UserData +from streaming_providers.exceptions import ProviderException +from streaming_providers.premiumize.client import Premiumize + + +def create_or_get_folder_id(pm_client: Premiumize, info_hash: str): + folder_data = pm_client.get_folder_list() + for folder in folder_data["content"]: + if folder["name"] == info_hash: + return folder["id"] + + folder_data = pm_client.create_folder(info_hash) + if folder_data.get("status") != "success": + raise ProviderException( + f"Failed to create folder with name {info_hash}", "api_error.mp4" + ) + return folder_data.get("id") + + +def get_direct_link_from_premiumize( + info_hash: str, + magnet_link: str, + user_data: UserData, + torrent_name: str, + filename: str, + max_retries=5, + retry_interval=5, +) -> str: + pm_client = Premiumize(token=user_data.streaming_provider.token) + + # Check if the torrent already exists + torrent_info = pm_client.get_available_torrent(info_hash, torrent_name) + if torrent_info: + torrent_id = torrent_info.get("id") + if torrent_info["status"] == "error": + pm_client.delete_torrent(torrent_id) + raise ProviderException( + "Not enough seeders available for parse magnet link", + "transfer_error.mp4", + ) + else: + # If torrent doesn't exist, add it + folder_id = create_or_get_folder_id(pm_client, info_hash) + response_data = pm_client.add_magent_link(magnet_link, folder_id) + if "id" not in response_data: + raise ProviderException( + "Failed to add magnet link to Real-Debrid", "transfer_error.mp4" + ) + torrent_id = response_data["id"] + + # Wait for file selection and then start torrent download + torrent_info = pm_client.wait_for_status( + torrent_id, "finished", max_retries, retry_interval + ) + return get_stream_link(pm_client, torrent_info, filename, info_hash) + + +def get_stream_link( + pm_client: Premiumize, torrent_info: dict[str, Any], filename: str, info_hash: str +) -> str: + """Get the stream link from the torrent info.""" + if torrent_info["folder_id"] is None: + torrent_folder_data = pm_client.get_folder_list( + create_or_get_folder_id(pm_client, info_hash) + ) + else: + torrent_folder_data = pm_client.get_folder_list(torrent_info["folder_id"]) + exact_match = next( + (f for f in torrent_folder_data["content"] if f["name"] == filename), None + ) + if exact_match: + return exact_match["link"] + + # Fuzzy matching as a fallback + for file in torrent_folder_data["content"]: + file["fuzzy_ratio"] = fuzz.ratio(filename, file["name"]) + selected_file = max(torrent_folder_data["content"], key=lambda x: x["fuzzy_ratio"]) + + # If the fuzzy ratio is less than 50, then select the largest file + if selected_file["fuzzy_ratio"] < 50: + selected_file = max(torrent_folder_data["content"], key=lambda x: x["size"]) + + if "video" not in selected_file["mime_type"]: + raise ProviderException( + "No matching file available for this torrent", "no_matching_file.mp4" + ) + + return selected_file["link"] + + +def free_up_space(pm_client: Premiumize, required_space: int): + """Frees up space in the Premiumize account by deleting torrents until the required space is available.""" + contents = pm_client.get_folder_list() + available_space = contents["space_max"] - contents["space_used"] + + if available_space >= required_space: + return # There's enough space, no need to delete anything + + folders = sorted( + contents["folders"], + key=lambda x: ( + -x["size"], + datetime.strptime(x["last_update"], "%Y-%m-%d %H:%M:%S"), + ), + ) + + for folder in folders: + if available_space >= required_space: + break + # delete sub folder torrents and folders + sub_folder_content = seedr.listContents(folder["id"]) + for sub_folder in sub_folder_content["folders"]: + seedr.deleteFolder(sub_folder["id"]) + for sub_folder_torrent in sub_folder_content["torrents"]: + seedr.deleteTorrent(sub_folder_torrent["id"]) + seedr.deleteFolder(folder["id"]) + available_space += folder["size"] + + +def update_pm_cache_status(streams: list[TorrentStreams], user_data: UserData): + """Updates the cache status of streams based on Premiumize's instant availability.""" + + try: + pm_client = Premiumize(token=user_data.streaming_provider.token) + instant_availability_data = pm_client.get_torrent_instant_availability( + [stream.id for stream in streams] + ) + for stream, cached_status in zip( + streams, instant_availability_data["response"] + ): + stream.cached = cached_status + + except ProviderException: + pass + + +def fetch_downloaded_info_hashes_from_premiumize(user_data: UserData) -> list[str]: + """Fetches the info_hashes of all torrents downloaded in the Premiumize account.""" + try: + pm_client = Premiumize(token=user_data.streaming_provider.token) + available_folders = pm_client.get_folder_list() + return [ + folder["name"] + for folder in available_folders["content"] + if folder["name"] and folder["type"] == "folder" + ] + + except ProviderException: + return [] diff --git a/utils/parser.py b/utils/parser.py index e905130..1fec8f5 100644 --- a/utils/parser.py +++ b/utils/parser.py @@ -24,6 +24,10 @@ from streaming_providers.pikpak.utils import ( update_pikpak_cache_status, fetch_downloaded_info_hashes_from_pikpak, ) +from streaming_providers.premiumize.utils import ( + update_pm_cache_status, + fetch_downloaded_info_hashes_from_premiumize, +) from streaming_providers.realdebrid.utils import ( update_rd_cache_status, fetch_downloaded_info_hashes_from_rd, @@ -63,6 +67,7 @@ async def filter_and_sort_streams( "realdebrid": update_rd_cache_status, "seedr": update_seedr_cache_status, "torbox": update_torbox_cache_status, + "premiumize": update_pm_cache_status, } # Update cache status based on provider @@ -244,6 +249,7 @@ async def fetch_downloaded_info_hashes(user_data: UserData) -> list[str]: "realdebrid": fetch_downloaded_info_hashes_from_rd, "seedr": fetch_downloaded_info_hashes_from_seedr, "torbox": fetch_downloaded_info_hashes_from_torbox, + "premiumize": fetch_downloaded_info_hashes_from_premiumize, } if fetch_downloaded_info_hashes_function := fetch_downloaded_info_hashes_functions.get(