mirror of
https://github.com/Viren070/MediaFusion.git
synced 2025-12-01 23:21:11 +01:00
Add Configuration validations & loading widget for responsiveness (#342)
This commit is contained in:
+46
-3
@@ -31,6 +31,7 @@ from scrapers.routes import router as scrapers_router
|
||||
from scrapers.rpdb import update_rpdb_posters, update_rpdb_poster
|
||||
from streaming_providers import mapper
|
||||
from streaming_providers.routes import router as streaming_provider_router
|
||||
from streaming_providers.validator import validate_provider_credentials
|
||||
from utils import const, crypto, poster, torrent, wrappers
|
||||
from utils.lock import (
|
||||
acquire_scheduler_lock,
|
||||
@@ -45,9 +46,13 @@ from utils.runtime_const import (
|
||||
TEMPLATES,
|
||||
REDIS_ASYNC_CLIENT,
|
||||
)
|
||||
from utils.validation_helper import (
|
||||
validate_mediaflow_proxy_credentials,
|
||||
validate_rpdb_token,
|
||||
)
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(levelname)s::%(asctime)s::%(filename)s::%(lineno)d - %(message)s",
|
||||
format="%(levelname)s::%(asctime)s::%(pathname)s::%(lineno)d - %(message)s",
|
||||
datefmt="%d-%b-%y %H:%M:%S",
|
||||
level=settings.logging_level,
|
||||
)
|
||||
@@ -600,9 +605,47 @@ async def get_streams(
|
||||
|
||||
@app.post("/encrypt-user-data", tags=["user_data"])
|
||||
@wrappers.rate_limit(30, 60 * 5, "user_data")
|
||||
async def encrypt_user_data(user_data: schemas.UserData):
|
||||
async def encrypt_user_data(user_data: schemas.UserData, request: Request):
|
||||
async def _validate_all_config() -> dict:
|
||||
if not settings.is_public_instance and (
|
||||
not user_data.api_password
|
||||
or user_data.api_password != settings.api_password
|
||||
):
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Invalid MediaFusion API Password. Make sure to enter the correct password which is configured in environment variables.",
|
||||
}
|
||||
try:
|
||||
validation_tasks = [
|
||||
validate_provider_credentials(request, user_data),
|
||||
validate_mediaflow_proxy_credentials(user_data),
|
||||
validate_rpdb_token(user_data),
|
||||
]
|
||||
|
||||
results = await asyncio.gather(*validation_tasks, return_exceptions=True)
|
||||
|
||||
for result in results:
|
||||
if isinstance(result, Exception):
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Validation failed: {str(result)}",
|
||||
}
|
||||
if isinstance(result, dict) and result["status"] == "error":
|
||||
return result
|
||||
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected error during validation: {str(e)}",
|
||||
}
|
||||
|
||||
validation_result = await _validate_all_config()
|
||||
if validation_result["status"] == "error":
|
||||
return validation_result
|
||||
|
||||
encrypted_str = crypto.encrypt_user_data(user_data)
|
||||
return {"encrypted_str": encrypted_str}
|
||||
return {"status": "success", "encrypted_str": encrypted_str}
|
||||
|
||||
|
||||
@app.get("/poster/{catalog_type}/{mediafusion_id}.jpg", tags=["poster"])
|
||||
|
||||
@@ -433,6 +433,51 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
backdrop-filter: blur(3px);
|
||||
-webkit-backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
background: rgba(20, 20, 35, 0.7);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(74, 71, 163, 0.2);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
min-width: 250px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-content .spinner-border {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
color: #4a47a3;
|
||||
}
|
||||
|
||||
.loading-content h5 {
|
||||
color: #E0E0E0;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.loading-content p {
|
||||
color: #9f9f9f;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Responsive Design */
|
||||
|
||||
@@ -719,6 +719,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loadingWidget" class="loading-overlay" style="display: none;">
|
||||
<div class="loading-content">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5>Please Wait</h5>
|
||||
<p id="loadingMessage">Processing your configuration...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS for Bootstrap and form validation -->
|
||||
<script src="/static/js/jquery-3.6.0.min.js"></script>
|
||||
|
||||
@@ -215,20 +215,50 @@ function updateProviderFields(isChangeEvent = false) {
|
||||
adjustOAuthSectionDisplay();
|
||||
}
|
||||
|
||||
// Function to show loading widget
|
||||
function showLoadingWidget(message = "Processing your configuration...") {
|
||||
const loadingWidget = document.getElementById('loadingWidget');
|
||||
const loadingMessage = document.getElementById('loadingMessage');
|
||||
|
||||
if (loadingMessage) {
|
||||
loadingMessage.textContent = message;
|
||||
}
|
||||
|
||||
if (loadingWidget) {
|
||||
loadingWidget.style.display = 'flex';
|
||||
// Prevent background scrolling while loading
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// Function to hide loading widget
|
||||
function hideLoadingWidget() {
|
||||
const loadingWidget = document.getElementById('loadingWidget');
|
||||
if (loadingWidget) {
|
||||
loadingWidget.style.display = 'none';
|
||||
// Restore background scrolling
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Function to get installation URL
|
||||
async function getInstallationUrl(isRedirect = false) {
|
||||
const userData = getUserData();
|
||||
let urlPrefix = window.location.protocol + "//";
|
||||
if (isRedirect) {
|
||||
urlPrefix = "stremio://";
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
showNotification('Validation failed. Please check your input.', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoadingWidget();
|
||||
|
||||
const userData = getUserData();
|
||||
let urlPrefix = window.location.protocol + "//";
|
||||
if (isRedirect) {
|
||||
urlPrefix = "stremio://";
|
||||
}
|
||||
|
||||
if (!userData) {
|
||||
hideLoadingWidget();
|
||||
showNotification('Validation failed. Please check your input.', 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch('/encrypt-user-data', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -236,13 +266,27 @@ async function getInstallationUrl(isRedirect = false) {
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'error') {
|
||||
hideLoadingWidget();
|
||||
showNotification(data.message, 'error');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!data.encrypted_str) {
|
||||
hideLoadingWidget();
|
||||
showNotification('An error occurred while encrypting user data', 'error');
|
||||
return null;
|
||||
}
|
||||
return urlPrefix + window.location.host + "/" + data.encrypted_str + "/manifest.json";
|
||||
|
||||
const installationUrl = urlPrefix + window.location.host + "/" + data.encrypted_str + "/manifest.json";
|
||||
hideLoadingWidget();
|
||||
return installationUrl;
|
||||
|
||||
} catch (error) {
|
||||
hideLoadingWidget();
|
||||
showNotification('An error occurred while encrypting user data', 'error');
|
||||
console.error('Error encrypting user data:', error);
|
||||
return null;
|
||||
@@ -466,9 +510,11 @@ async function submitKodiCodeAndSetup() {
|
||||
}
|
||||
|
||||
async function setupKodiAddon(kodiCode) {
|
||||
showLoadingWidget('Preparing Kodi setup...')
|
||||
const installationUrl = await getInstallationUrl();
|
||||
|
||||
if (installationUrl) {
|
||||
showLoadingWidget('Setting up Kodi addon...')
|
||||
try {
|
||||
const response = await fetch('/kodi/associate_manifest', {
|
||||
method: 'POST',
|
||||
@@ -490,6 +536,8 @@ async function setupKodiAddon(kodiCode) {
|
||||
} catch (error) {
|
||||
console.error('Error setting up Kodi addon:', error);
|
||||
showNotification('An error occurred while setting up the Kodi addon.', 'error');
|
||||
} finally {
|
||||
hideLoadingWidget();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,7 +579,7 @@ oAuthBtn.addEventListener('click', async function () {
|
||||
|
||||
document.getElementById('configForm').addEventListener('submit', async function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
showLoadingWidget('Preparing Stremio installation...');
|
||||
const installationUrl = await getInstallationUrl(true);
|
||||
if (installationUrl) {
|
||||
window.location.href = installationUrl;
|
||||
@@ -540,6 +588,7 @@ document.getElementById('configForm').addEventListener('submit', async function
|
||||
|
||||
document.getElementById('shareBtn').addEventListener('click', async function (event) {
|
||||
event.preventDefault();
|
||||
showLoadingWidget('Preparing share URL...');
|
||||
const manifestUrl = await getInstallationUrl();
|
||||
if (manifestUrl) {
|
||||
try {
|
||||
@@ -557,6 +606,7 @@ document.getElementById('shareBtn').addEventListener('click', async function (ev
|
||||
|
||||
document.getElementById('copyBtn').addEventListener('click', async function (event) {
|
||||
event.preventDefault();
|
||||
showLoadingWidget('Generating installation URL...');
|
||||
const manifestUrl = await getInstallationUrl();
|
||||
if (manifestUrl) {
|
||||
try {
|
||||
@@ -638,7 +688,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
|
||||
// Add event listeners for mode switching
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let storedMode = 'newbie';
|
||||
try {
|
||||
const savedMode = localStorage.getItem('configMode');
|
||||
|
||||
@@ -123,3 +123,6 @@ class AllDebrid(DebridClient):
|
||||
return await self._make_request(
|
||||
"GET", "/magnet/delete", params={"id": magnet_id}
|
||||
)
|
||||
|
||||
async def get_user_info(self):
|
||||
return await self._make_request("GET", "/user")
|
||||
|
||||
@@ -163,3 +163,24 @@ async def delete_all_torrents_from_ad(user_data: UserData, user_ip: str, **kwarg
|
||||
for torrent in torrents["data"]["magnets"]
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def validate_alldebrid_credentials(user_data: UserData, user_ip: str) -> dict:
|
||||
"""Validates the AllDebrid credentials."""
|
||||
try:
|
||||
async with AllDebrid(
|
||||
token=user_data.streaming_provider.token, user_ip=user_ip
|
||||
) as ad_client:
|
||||
response = await ad_client.get_user_info()
|
||||
if response.get("status") == "success":
|
||||
return {"status": "success"}
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "Invalid AllDebrid credentials.",
|
||||
}
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to verify AllDebrid credential, error: {error.message}",
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
import traceback
|
||||
from abc import abstractmethod
|
||||
from base64 import b64encode, b64decode
|
||||
from contextlib import AsyncContextDecorator
|
||||
from typing import Optional, Dict, Union
|
||||
|
||||
import aiohttp
|
||||
@@ -10,7 +11,7 @@ from aiohttp import ClientResponse, ClientTimeout, ContentTypeError
|
||||
from streaming_providers.exceptions import ProviderException
|
||||
|
||||
|
||||
class DebridClient:
|
||||
class DebridClient(AsyncContextDecorator):
|
||||
def __init__(self, token: Optional[str] = None):
|
||||
self.token = token
|
||||
self.is_private_token = False
|
||||
@@ -32,6 +33,9 @@ class DebridClient:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.close()
|
||||
|
||||
async def close(self):
|
||||
if self.token and not self.is_private_token:
|
||||
try:
|
||||
await self.disable_access_token()
|
||||
@@ -62,6 +66,8 @@ class DebridClient:
|
||||
response, is_return_none, is_expected_to_fail
|
||||
)
|
||||
|
||||
except ProviderException as error:
|
||||
raise error
|
||||
except aiohttp.ClientConnectorError as error:
|
||||
if retry_count < 1: # Try one more time
|
||||
return await self._make_request(
|
||||
|
||||
@@ -158,3 +158,6 @@ class DebridLink(DebridClient):
|
||||
if torrent["hashString"] == info_hash:
|
||||
return torrent
|
||||
return None
|
||||
|
||||
async def get_user_info(self) -> dict[str, Any]:
|
||||
return await self._make_request("GET", f"{self.BASE_URL}/account/infos")
|
||||
|
||||
@@ -135,3 +135,15 @@ async def delete_all_torrents_from_dl(user_data: UserData, **kwargs):
|
||||
await asyncio.gather(
|
||||
*[dl_client.delete_torrent(torrent["id"]) for torrent in torrents["value"]]
|
||||
)
|
||||
|
||||
|
||||
async def validate_debridlink_credentials(user_data: UserData, **kwargs) -> dict:
|
||||
try:
|
||||
async with DebridLink(token=user_data.streaming_provider.token) as dl_client:
|
||||
await dl_client.get_user_info()
|
||||
return {"status": "success"}
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to verify DebridLink credential, error: {error.message}",
|
||||
}
|
||||
|
||||
@@ -3,54 +3,63 @@ from streaming_providers.alldebrid.utils import (
|
||||
fetch_downloaded_info_hashes_from_ad,
|
||||
delete_all_torrents_from_ad,
|
||||
get_video_url_from_alldebrid,
|
||||
validate_alldebrid_credentials,
|
||||
)
|
||||
from streaming_providers.debridlink.utils import (
|
||||
update_dl_cache_status,
|
||||
fetch_downloaded_info_hashes_from_dl,
|
||||
delete_all_torrents_from_dl,
|
||||
get_video_url_from_debridlink,
|
||||
validate_debridlink_credentials,
|
||||
)
|
||||
from streaming_providers.offcloud.utils import (
|
||||
update_oc_cache_status,
|
||||
fetch_downloaded_info_hashes_from_oc,
|
||||
delete_all_torrents_from_oc,
|
||||
get_video_url_from_offcloud,
|
||||
validate_offcloud_credentials,
|
||||
)
|
||||
from streaming_providers.pikpak.utils import (
|
||||
update_pikpak_cache_status,
|
||||
fetch_downloaded_info_hashes_from_pikpak,
|
||||
delete_all_torrents_from_pikpak,
|
||||
get_video_url_from_pikpak,
|
||||
validate_pikpak_credentials,
|
||||
)
|
||||
from streaming_providers.premiumize.utils import (
|
||||
update_pm_cache_status,
|
||||
fetch_downloaded_info_hashes_from_premiumize,
|
||||
delete_all_torrents_from_pm,
|
||||
get_video_url_from_premiumize,
|
||||
validate_premiumize_credentials,
|
||||
)
|
||||
from streaming_providers.qbittorrent.utils import (
|
||||
update_qbittorrent_cache_status,
|
||||
fetch_info_hashes_from_webdav,
|
||||
delete_all_torrents_from_qbittorrent,
|
||||
get_video_url_from_qbittorrent,
|
||||
validate_qbittorrent_credentials,
|
||||
)
|
||||
from streaming_providers.realdebrid.utils import (
|
||||
update_rd_cache_status,
|
||||
fetch_downloaded_info_hashes_from_rd,
|
||||
delete_all_watchlist_rd,
|
||||
get_video_url_from_realdebrid,
|
||||
validate_realdebrid_credentials,
|
||||
)
|
||||
from streaming_providers.seedr.utils import (
|
||||
update_seedr_cache_status,
|
||||
fetch_downloaded_info_hashes_from_seedr,
|
||||
delete_all_torrents_from_seedr,
|
||||
get_video_url_from_seedr,
|
||||
validate_seedr_credentials,
|
||||
)
|
||||
from streaming_providers.torbox.utils import (
|
||||
update_torbox_cache_status,
|
||||
fetch_downloaded_info_hashes_from_torbox,
|
||||
delete_all_torrents_from_torbox,
|
||||
get_video_url_from_torbox,
|
||||
validate_torbox_credentials,
|
||||
)
|
||||
|
||||
# Define provider-specific cache update functions
|
||||
@@ -104,3 +113,16 @@ GET_VIDEO_URL_FUNCTIONS = {
|
||||
"seedr": get_video_url_from_seedr,
|
||||
"torbox": get_video_url_from_torbox,
|
||||
}
|
||||
|
||||
|
||||
VALIDATE_CREDENTIALS_FUNCTIONS = {
|
||||
"alldebrid": validate_alldebrid_credentials,
|
||||
"debridlink": validate_debridlink_credentials,
|
||||
"offcloud": validate_offcloud_credentials,
|
||||
"pikpak": validate_pikpak_credentials,
|
||||
"premiumize": validate_premiumize_credentials,
|
||||
"qbittorrent": validate_qbittorrent_credentials,
|
||||
"realdebrid": validate_realdebrid_credentials,
|
||||
"seedr": validate_seedr_credentials,
|
||||
"torbox": validate_torbox_credentials,
|
||||
}
|
||||
|
||||
@@ -104,3 +104,16 @@ async def delete_all_torrents_from_oc(user_data: UserData, **kwargs):
|
||||
*[oc_client.delete_torrent(torrent["requestId"]) for torrent in torrents],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
|
||||
async def validate_offcloud_credentials(user_data: UserData, **kwargs) -> dict:
|
||||
"""Validates the OffCloud credentials."""
|
||||
try:
|
||||
async with OffCloud(token=user_data.streaming_provider.token) as oc_client:
|
||||
await oc_client.get_user_torrent_list()
|
||||
return {"status": "success"}
|
||||
except ProviderException:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "OffCloud API key is invalid or has expired",
|
||||
}
|
||||
|
||||
@@ -393,3 +393,12 @@ async def delete_all_torrents_from_pikpak(user_data: UserData, **kwargs):
|
||||
file_list_content = await pikpak.file_list(parent_id=my_pack_folder_id)
|
||||
file_ids = [file["id"] for file in file_list_content["files"]]
|
||||
await pikpak.delete_forever(file_ids)
|
||||
|
||||
|
||||
async def validate_pikpak_credentials(user_data: UserData, **kwargs) -> dict:
|
||||
"""Validates the PikPak credentials."""
|
||||
try:
|
||||
async with initialize_pikpak(user_data):
|
||||
return {"status": "success"}
|
||||
except ProviderException:
|
||||
return {"status": "error", "message": "Invalid PikPak credentials"}
|
||||
|
||||
@@ -139,3 +139,19 @@ async def delete_all_torrents_from_pm(user_data: UserData, **kwargs):
|
||||
pm_client.delete_folder(folder["id"]) for folder in folders["content"]
|
||||
]
|
||||
await asyncio.gather(*delete_tasks)
|
||||
|
||||
|
||||
async def validate_premiumize_credentials(user_data: UserData, **kwargs) -> dict:
|
||||
"""Validates the Premiumize credentials."""
|
||||
try:
|
||||
async with Premiumize(token=user_data.streaming_provider.token) as pm_client:
|
||||
response = await pm_client.get_account_info()
|
||||
if response["status"] == "success":
|
||||
return {"status": "success"}
|
||||
return {"status": "error", "message": "Premiumize token is invalid"}
|
||||
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to verify Premiumize credential, error: {error.message}",
|
||||
}
|
||||
|
||||
@@ -5,7 +5,13 @@ from os import path
|
||||
from typing import Optional
|
||||
from urllib.parse import urljoin, urlparse, quote
|
||||
|
||||
from aiohttp import ClientConnectorError, ClientSession, CookieJar
|
||||
from aiohttp import (
|
||||
ClientConnectorError,
|
||||
ClientSession,
|
||||
CookieJar,
|
||||
ClientTimeout,
|
||||
ClientResponseError,
|
||||
)
|
||||
from aioqbt.api import AddFormBuilder, TorrentInfo, InfoFilter
|
||||
from aioqbt.client import create_client, APIClient
|
||||
from aioqbt.exc import LoginError, AddTorrentError, NotFoundError
|
||||
@@ -133,7 +139,9 @@ async def find_file_in_folder_tree(
|
||||
|
||||
@asynccontextmanager
|
||||
async def initialize_qbittorrent(user_data: UserData) -> APIClient:
|
||||
async with ClientSession(cookie_jar=CookieJar(unsafe=True), timeout=18) as session:
|
||||
async with ClientSession(
|
||||
cookie_jar=CookieJar(unsafe=True), timeout=ClientTimeout(total=15)
|
||||
) as session:
|
||||
try:
|
||||
qbittorrent = await create_client(
|
||||
user_data.streaming_provider.qbittorrent_config.qbittorrent_url.rstrip(
|
||||
@@ -148,6 +156,15 @@ async def initialize_qbittorrent(user_data: UserData) -> APIClient:
|
||||
raise ProviderException(
|
||||
"Invalid qBittorrent credentials", "invalid_credentials.mp4"
|
||||
)
|
||||
except ClientResponseError as err:
|
||||
if err.status == 403:
|
||||
raise ProviderException(
|
||||
"Invalid qBittorrent credentials", "invalid_credentials.mp4"
|
||||
)
|
||||
raise ProviderException(
|
||||
f"An error occurred while connecting to qBittorrent: {err}",
|
||||
"qbittorrent_error.mp4",
|
||||
)
|
||||
except (ClientConnectorError, NotFoundError):
|
||||
raise ProviderException(
|
||||
"Failed to connect to qBittorrent", "qbittorrent_error.mp4"
|
||||
@@ -355,3 +372,24 @@ async def delete_all_torrents_from_qbittorrent(user_data: UserData, **kwargs):
|
||||
await qbittorrent.torrents.delete(
|
||||
hashes=[torrent.hash for torrent in torrents], delete_files=True
|
||||
)
|
||||
|
||||
|
||||
async def validate_qbittorrent_credentials(user_data: UserData, **kwargs) -> dict:
|
||||
"""Validates the qBittorrent credentials."""
|
||||
try:
|
||||
async with initialize_qbittorrent(user_data):
|
||||
pass
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to verify QBittorrent, error: {error.message}",
|
||||
}
|
||||
|
||||
try:
|
||||
async with initialize_webdav(user_data):
|
||||
return {"status": "success"}
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to verify WebDAV, error: {error.message}",
|
||||
}
|
||||
|
||||
@@ -195,9 +195,12 @@ class RealDebrid(DebridClient):
|
||||
f"Failed to create download link. response: {response}", "api_error.mp4"
|
||||
)
|
||||
|
||||
async def delete_torrent(self, torrent_id):
|
||||
async def delete_torrent(self, torrent_id) -> dict:
|
||||
return await self._make_request(
|
||||
"DELETE",
|
||||
f"{self.BASE_URL}/torrents/delete/{torrent_id}",
|
||||
is_return_none=True,
|
||||
)
|
||||
|
||||
async def get_user_info(self) -> dict:
|
||||
return await self._make_request("GET", f"{self.BASE_URL}/user")
|
||||
|
||||
@@ -222,3 +222,18 @@ async def delete_all_watchlist_rd(user_data: UserData, user_ip: str, **kwargs):
|
||||
await asyncio.gather(
|
||||
*[rd_client.delete_torrent(torrent["id"]) for torrent in torrents]
|
||||
)
|
||||
|
||||
|
||||
async def validate_realdebrid_credentials(user_data: UserData, user_ip: str) -> dict:
|
||||
"""Validates the RealDebrid credentials."""
|
||||
try:
|
||||
async with RealDebrid(
|
||||
token=user_data.streaming_provider.token, user_ip=user_ip
|
||||
) as rd_client:
|
||||
await rd_client.get_user_info()
|
||||
return {"status": "success"}
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to verify RealDebrid credential, error: {error.message}",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from os import path
|
||||
from typing import Annotated
|
||||
@@ -115,10 +114,7 @@ async def get_or_create_video_url(
|
||||
),
|
||||
)
|
||||
|
||||
if asyncio.iscoroutinefunction(get_video_url):
|
||||
return await get_video_url(**kwargs)
|
||||
else:
|
||||
return await asyncio.to_thread(get_video_url, **kwargs)
|
||||
return await get_video_url(**kwargs)
|
||||
|
||||
|
||||
async def cache_stream_url(cached_stream_url_key, video_url):
|
||||
@@ -278,11 +274,7 @@ async def delete_all_watchlist(
|
||||
)
|
||||
|
||||
try:
|
||||
# Call the appropriate delete function asynchronously
|
||||
if asyncio.iscoroutinefunction(delete_all_watchlist_function):
|
||||
await delete_all_watchlist_function(**kwargs)
|
||||
else:
|
||||
await asyncio.to_thread(delete_all_watchlist_function, **kwargs)
|
||||
await delete_all_watchlist_function(**kwargs)
|
||||
video_url = f"{settings.host_url}/static/exceptions/watchlist_deleted.mp4"
|
||||
|
||||
except ProviderException as error:
|
||||
|
||||
@@ -37,6 +37,8 @@ async def get_seedr_client(user_data: UserData) -> Seedr:
|
||||
if "error" in response:
|
||||
raise ProviderException("Invalid Seedr token", "invalid_token.mp4")
|
||||
yield seedr
|
||||
except SyntaxError:
|
||||
raise ProviderException("Invalid Seedr token", "invalid_token.mp4")
|
||||
except ProviderException as error:
|
||||
raise error
|
||||
except Exception as error:
|
||||
@@ -174,7 +176,7 @@ async def get_video_url_from_seedr(
|
||||
stream: TorrentStreams,
|
||||
filename: Optional[str] = None,
|
||||
episode: Optional[int] = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""Main function to get video URL from Seedr."""
|
||||
async with get_seedr_client(user_data) as seedr:
|
||||
@@ -259,3 +261,20 @@ async def delete_all_torrents_from_seedr(user_data: UserData, **kwargs) -> None:
|
||||
"""Delete all torrents from the user's account."""
|
||||
async with get_seedr_client(user_data) as seedr:
|
||||
await ensure_space_available(seedr, math.inf)
|
||||
|
||||
|
||||
async def validate_seedr_credentials(user_data: UserData, **kwargs) -> dict:
|
||||
"""Validate the Seedr credentials."""
|
||||
try:
|
||||
async with get_seedr_client(user_data):
|
||||
return {"status": "success"}
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to validate Seedr credentials: {error.message}",
|
||||
}
|
||||
except Exception as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to validate Seedr credentials: {error}",
|
||||
}
|
||||
|
||||
@@ -17,7 +17,28 @@ class Torbox(DebridClient):
|
||||
await super().__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
async def _handle_service_specific_errors(self, error_data: dict, status_code: int):
|
||||
pass
|
||||
error_code = error_data.get("error")
|
||||
match error_code:
|
||||
case "BAD_TOKEN" | "AUTH_ERROR" | "OAUTH_VERIFICATION_ERROR":
|
||||
raise ProviderException(
|
||||
"Invalid Torbox token",
|
||||
"invalid_token.mp4",
|
||||
)
|
||||
case "DOWNLOAD_TOO_LARGE":
|
||||
raise ProviderException(
|
||||
"Download size too large for the user plan",
|
||||
"not_enough_space.mp4",
|
||||
)
|
||||
case "ACTIVE_LIMIT" | "MONTHLY_LIMIT":
|
||||
raise ProviderException(
|
||||
"Download limit exceeded",
|
||||
"daily_download_limit.mp4",
|
||||
)
|
||||
case "DOWNLOAD_SERVER_ERROR" | "DATABASE_ERROR":
|
||||
raise ProviderException(
|
||||
"Torbox server error",
|
||||
"debrid_service_down_error.mp4",
|
||||
)
|
||||
|
||||
async def _make_request(
|
||||
self,
|
||||
@@ -100,3 +121,8 @@ class Torbox(DebridClient):
|
||||
"/torrents/controltorrent",
|
||||
json={"torrent_id": torrent_id, "operation": "delete"},
|
||||
)
|
||||
|
||||
async def get_user_info(self, get_settings: bool = False):
|
||||
return await self._make_request(
|
||||
"GET", "/user/me", params={"settings": "true" if get_settings else "false"}
|
||||
)
|
||||
|
||||
@@ -128,3 +128,19 @@ async def delete_all_torrents_from_torbox(user_data: UserData, **kwargs: Any) ->
|
||||
return
|
||||
for torrent in torrents:
|
||||
await torbox_client.delete_torrent(torrent.get("id", ""))
|
||||
|
||||
|
||||
async def validate_torbox_credentials(
|
||||
user_data: UserData, **kwargs: Any
|
||||
) -> Dict[str, str]:
|
||||
"""Validates the Torbox credentials."""
|
||||
try:
|
||||
async with Torbox(token=user_data.streaming_provider.token) as torbox_client:
|
||||
await torbox_client.get_user_info()
|
||||
return {"status": "success"}
|
||||
|
||||
except ProviderException as error:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to validate TorBox credentials: {error.message}",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from db.schemas import UserData
|
||||
from streaming_providers.mapper import VALIDATE_CREDENTIALS_FUNCTIONS
|
||||
from utils.network import get_user_public_ip
|
||||
from fastapi import Request
|
||||
|
||||
|
||||
async def validate_provider_credentials(request: Request, user_data: UserData) -> dict:
|
||||
"""Validate the provider credentials."""
|
||||
if not user_data.streaming_provider:
|
||||
return {"status": "success"}
|
||||
|
||||
if user_data.streaming_provider.service not in VALIDATE_CREDENTIALS_FUNCTIONS:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Provider {user_data.streaming_provider.service} is not supported.",
|
||||
}
|
||||
|
||||
user_ip = await get_user_public_ip(request, user_data)
|
||||
return await VALIDATE_CREDENTIALS_FUNCTIONS[user_data.streaming_provider.service](
|
||||
user_data=user_data, user_ip=user_ip
|
||||
)
|
||||
+4
-13
@@ -85,10 +85,7 @@ async def filter_and_sort_streams(
|
||||
kwargs = dict(streams=filtered_streams, user_data=user_data, user_ip=user_ip)
|
||||
if cache_update_function:
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(cache_update_function):
|
||||
await cache_update_function(**kwargs)
|
||||
else:
|
||||
await asyncio.to_thread(cache_update_function, **kwargs)
|
||||
await cache_update_function(**kwargs)
|
||||
except Exception as error:
|
||||
logging.exception(
|
||||
f"Failed to update cache status for {user_data.streaming_provider.service}: {error}"
|
||||
@@ -473,15 +470,9 @@ async def fetch_downloaded_info_hashes(
|
||||
user_data.streaming_provider.service
|
||||
):
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(fetch_downloaded_info_hashes_function):
|
||||
downloaded_info_hashes = await fetch_downloaded_info_hashes_function(
|
||||
**kwargs
|
||||
)
|
||||
else:
|
||||
downloaded_info_hashes = await asyncio.to_thread(
|
||||
fetch_downloaded_info_hashes_function, **kwargs
|
||||
)
|
||||
|
||||
downloaded_info_hashes = await fetch_downloaded_info_hashes_function(
|
||||
**kwargs
|
||||
)
|
||||
return downloaded_info_hashes
|
||||
except Exception as error:
|
||||
logging.exception(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
import httpx
|
||||
|
||||
from db import schemas
|
||||
from utils import const
|
||||
from utils.runtime_const import REDIS_ASYNC_CLIENT
|
||||
from utils.runtime_const import REDIS_ASYNC_CLIENT, PRIVATE_CIDR
|
||||
|
||||
|
||||
def is_valid_url(url: str) -> bool:
|
||||
@@ -200,3 +200,77 @@ def validate_parent_guide_nudity(metadata, user_data: schemas.UserData) -> bool:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def validate_service(
|
||||
url: str,
|
||||
params: dict = None,
|
||||
success_message: str = None,
|
||||
invalid_creds_message: str = None,
|
||||
) -> dict:
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
response = await client.get(url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"status": "success",
|
||||
"message": success_message or "Validation successful.",
|
||||
}
|
||||
except httpx.HTTPStatusError as err:
|
||||
if err.response.status_code == 403 and invalid_creds_message:
|
||||
return {"status": "error", "message": invalid_creds_message}
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"HTTPStatusError: Failed to validate service at {url}: {err}",
|
||||
}
|
||||
except httpx.RequestError as err:
|
||||
logging.error("Validation error for service at %s: %s", url, err)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"RequestError: Failed to validate service at {url}: {err}",
|
||||
}
|
||||
except Exception as err:
|
||||
logging.error("Validation error for service at %s: %s", url, err)
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Failed to validate service at {url}: {err}",
|
||||
}
|
||||
|
||||
|
||||
async def validate_mediaflow_proxy_credentials(user_data: schemas.UserData) -> dict:
|
||||
if not user_data.mediaflow_config:
|
||||
return {"status": "success", "message": "Mediaflow Proxy is not set."}
|
||||
|
||||
validation_url = urljoin(user_data.mediaflow_config.proxy_url, "/proxy/ip")
|
||||
params = {"api_password": user_data.mediaflow_config.api_password}
|
||||
results = await validate_service(
|
||||
url=validation_url,
|
||||
params=params,
|
||||
invalid_creds_message="Invalid Mediaflow Proxy API Password. Please check your Mediaflow Proxy API Password.",
|
||||
)
|
||||
if results["status"] == "success":
|
||||
return results
|
||||
|
||||
if results["message"].startswith("RequestError"):
|
||||
parsed_url = urlparse(user_data.mediaflow_config.proxy_url)
|
||||
if PRIVATE_CIDR.match(parsed_url.netloc):
|
||||
# MediaFlow proxy URL is a private IP address
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Mediaflow Proxy URL is a private IP address.",
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def validate_rpdb_token(user_data: schemas.UserData) -> dict:
|
||||
if not user_data.rpdb_config:
|
||||
return {"status": "success", "message": "RPDB is not enabled."}
|
||||
|
||||
validation_url = (
|
||||
f"https://api.ratingposterdb.com/{user_data.rpdb_config.api_key}/isValid"
|
||||
)
|
||||
return await validate_service(
|
||||
url=validation_url,
|
||||
invalid_creds_message="Invalid RPDB API Key. Please check your RPDB API Key.",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user