Add Premiumize Streaming provider

This commit is contained in:
mhdzumair
2024-01-27 00:08:49 +05:30
parent f2d92a42f8
commit f498daf6fe
11 changed files with 380 additions and 7 deletions
+7
View File
@@ -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"])
+2
View File
@@ -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"
+8 -1
View File
@@ -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
+14 -4
View File
@@ -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:
+2 -1
View File
@@ -36,6 +36,7 @@
<option value="torbox" {% if user_data.streaming_provider.service=='torbox' %}selected{% endif %}>Torbox (Free Quota)</option>
<option value="realdebrid" {% if user_data.streaming_provider.service=='realdebrid' %}selected{% endif %}>Real-Debrid (Premium)</option>
<option value="debridlink" {% if user_data.streaming_provider.service=='debridlink' %}selected{% endif %}>Debrid-Link (Premium)</option>
<option value="premiumize" {% if user_data.streaming_provider.service=='premiumize' %}selected{% endif %}>Premiumize (Premium)</option>
<option value="alldebrid" {% if user_data.streaming_provider.service=='alldebrid' %}selected{% endif %}>AllDebrid (Premium)</option>
</select>
@@ -116,7 +117,7 @@
user_data.selected_catalogs
%}checked{% endif %}>
<label class="form-check-label" for="{{ catalog[0] }}">
{{ catalog[1] }}
<span class="label-text">{{ catalog[1] }}</span>
</label>
</div>
</div>
+3 -1
View File
@@ -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";
}
});
+34
View File
@@ -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)
+149
View File
@@ -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
+155
View File
@@ -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 []
+6
View File
@@ -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(