mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
Add Premiumize Streaming provider
This commit is contained in:
@@ -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"])
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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 []
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user