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(