Add Configuration validations & loading widget for responsiveness (#342)

This commit is contained in:
Mohamed Zumair
2024-11-02 19:56:02 +05:30
committed by GitHub
parent 027ed6b640
commit 467f378207
23 changed files with 495 additions and 48 deletions
+46 -3
View File
@@ -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"])
+45
View File
@@ -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 */
+9
View File
@@ -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>
+64 -14
View File
@@ -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');
+3
View File
@@ -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")
+21
View File
@@ -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}",
}
+7 -1
View File
@@ -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(
+3
View File
@@ -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")
+12
View File
@@ -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}",
}
+22
View File
@@ -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,
}
+13
View File
@@ -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",
}
+9
View File
@@ -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"}
+16
View File
@@ -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}",
}
+40 -2
View File
@@ -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}",
}
+4 -1
View File
@@ -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")
+15
View File
@@ -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}",
}
+2 -10
View File
@@ -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:
+20 -1
View File
@@ -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}",
}
+27 -1
View File
@@ -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"}
)
+16
View File
@@ -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}",
}
+21
View File
@@ -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
View File
@@ -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(
+76 -2
View File
@@ -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.",
)