mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Ability to refresh account info from account info modal and provide notification on results.
This commit is contained in:
parent
d1a5143312
commit
84c752761a
8 changed files with 433 additions and 32 deletions
|
|
@ -6,6 +6,7 @@ from .api_views import (
|
|||
ServerGroupViewSet,
|
||||
RefreshM3UAPIView,
|
||||
RefreshSingleM3UAPIView,
|
||||
RefreshAccountInfoAPIView,
|
||||
UserAgentViewSet,
|
||||
M3UAccountProfileViewSet,
|
||||
)
|
||||
|
|
@ -33,6 +34,11 @@ urlpatterns = [
|
|||
RefreshSingleM3UAPIView.as_view(),
|
||||
name="m3u_refresh_single",
|
||||
),
|
||||
path(
|
||||
"refresh-account-info/<int:profile_id>/",
|
||||
RefreshAccountInfoAPIView.as_view(),
|
||||
name="m3u_refresh_account_info",
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ from .serializers import (
|
|||
M3UAccountProfileSerializer,
|
||||
)
|
||||
|
||||
from .tasks import refresh_single_m3u_account, refresh_m3u_accounts
|
||||
from .tasks import refresh_single_m3u_account, refresh_m3u_accounts, refresh_account_info
|
||||
import json
|
||||
|
||||
|
||||
|
|
@ -348,6 +348,54 @@ class RefreshSingleM3UAPIView(APIView):
|
|||
)
|
||||
|
||||
|
||||
class RefreshAccountInfoAPIView(APIView):
|
||||
"""Triggers account info refresh for a single M3U account"""
|
||||
|
||||
def get_permissions(self):
|
||||
try:
|
||||
return [
|
||||
perm() for perm in permission_classes_by_method[self.request.method]
|
||||
]
|
||||
except KeyError:
|
||||
return [Authenticated()]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Triggers a refresh of account information for a specific M3U profile",
|
||||
responses={202: "Account info refresh initiated", 400: "Profile not found or not XtreamCodes"},
|
||||
)
|
||||
def post(self, request, profile_id, format=None):
|
||||
try:
|
||||
from .models import M3UAccountProfile
|
||||
profile = M3UAccountProfile.objects.get(id=profile_id)
|
||||
account = profile.m3u_account
|
||||
|
||||
if account.account_type != M3UAccount.Types.XC:
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Account info refresh is only available for XtreamCodes accounts",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
refresh_account_info.delay(profile_id)
|
||||
return Response(
|
||||
{
|
||||
"success": True,
|
||||
"message": f"Account info refresh initiated for profile {profile.name}.",
|
||||
},
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
)
|
||||
except M3UAccountProfile.DoesNotExist:
|
||||
return Response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Profile not found",
|
||||
},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
|
||||
class UserAgentViewSet(viewsets.ModelViewSet):
|
||||
"""Handles CRUD operations for User Agents"""
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,17 @@ class M3UFilterSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class M3UAccountProfileSerializer(serializers.ModelSerializer):
|
||||
account = serializers.SerializerMethodField()
|
||||
|
||||
def get_account(self, obj):
|
||||
"""Include basic account information for frontend use"""
|
||||
return {
|
||||
'id': obj.m3u_account.id,
|
||||
'name': obj.m3u_account.name,
|
||||
'account_type': obj.m3u_account.account_type,
|
||||
'is_xtream_codes': obj.m3u_account.account_type == 'XC'
|
||||
}
|
||||
|
||||
class Meta:
|
||||
model = M3UAccountProfile
|
||||
fields = [
|
||||
|
|
@ -41,8 +52,9 @@ class M3UAccountProfileSerializer(serializers.ModelSerializer):
|
|||
"search_pattern",
|
||||
"replace_pattern",
|
||||
"custom_properties",
|
||||
"account",
|
||||
]
|
||||
read_only_fields = ["id"]
|
||||
read_only_fields = ["id", "account"]
|
||||
|
||||
def create(self, validated_data):
|
||||
m3u_account = self.context.get("m3u_account")
|
||||
|
|
|
|||
|
|
@ -1176,13 +1176,13 @@ def refresh_m3u_groups(account_id, use_cache=False, full_refresh=False):
|
|||
)
|
||||
|
||||
logger.info(
|
||||
f"Creating XCClient with URL: {server_url}, Username: {account.username}, User-Agent: {user_agent_string}"
|
||||
f"Creating XCClient with URL: {account.server_url}, Username: {account.username}, User-Agent: {user_agent_string}"
|
||||
)
|
||||
|
||||
# Create XCClient with explicit error handling
|
||||
try:
|
||||
with XCClient(
|
||||
server_url, account.username, account.password, user_agent_string
|
||||
account.server_url, account.username, account.password, user_agent_string
|
||||
) as xc_client:
|
||||
logger.info(f"XCClient instance created successfully")
|
||||
|
||||
|
|
@ -1191,33 +1191,54 @@ def refresh_m3u_groups(account_id, use_cache=False, full_refresh=False):
|
|||
logger.debug(f"Authenticating with XC server {server_url}")
|
||||
auth_result = xc_client.authenticate()
|
||||
logger.debug(f"Authentication response: {auth_result}")
|
||||
|
||||
|
||||
# Save account information to all active profiles
|
||||
try:
|
||||
from apps.m3u.models import M3UAccountProfile
|
||||
|
||||
|
||||
profiles = M3UAccountProfile.objects.filter(
|
||||
m3u_account=account,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
# Get structured account information from XC client
|
||||
account_info = xc_client.get_account_info()
|
||||
|
||||
# Update each profile with account information
|
||||
|
||||
# Update each profile with account information using its own transformed credentials
|
||||
for profile in profiles:
|
||||
# Merge with existing custom_properties if they exist
|
||||
existing_props = profile.custom_properties or {}
|
||||
existing_props.update(account_info)
|
||||
profile.custom_properties = existing_props
|
||||
profile.save(update_fields=['custom_properties'])
|
||||
|
||||
logger.info(f"Saved account information to {profiles.count()} profiles for account {account.name}")
|
||||
|
||||
try:
|
||||
# Get transformed credentials for this specific profile
|
||||
profile_url, profile_username, profile_password = get_transformed_credentials(account, profile)
|
||||
|
||||
# Create a separate XC client for this profile's credentials
|
||||
with XCClient(
|
||||
profile_url,
|
||||
profile_username,
|
||||
profile_password,
|
||||
user_agent_string
|
||||
) as profile_client:
|
||||
# Authenticate with this profile's credentials
|
||||
if profile_client.authenticate():
|
||||
# Get account information specific to this profile's credentials
|
||||
profile_account_info = profile_client.get_account_info()
|
||||
|
||||
# Merge with existing custom_properties if they exist
|
||||
existing_props = profile.custom_properties or {}
|
||||
existing_props.update(profile_account_info)
|
||||
profile.custom_properties = existing_props
|
||||
profile.save(update_fields=['custom_properties'])
|
||||
|
||||
logger.info(f"Updated account information for profile '{profile.name}' with transformed credentials")
|
||||
else:
|
||||
logger.warning(f"Failed to authenticate profile '{profile.name}' with transformed credentials")
|
||||
|
||||
except Exception as profile_error:
|
||||
logger.error(f"Failed to update account information for profile '{profile.name}': {str(profile_error)}")
|
||||
# Continue with other profiles even if one fails
|
||||
|
||||
logger.info(f"Processed account information for {profiles.count()} profiles for account {account.name}")
|
||||
|
||||
except Exception as save_error:
|
||||
logger.warning(f"Failed to save account information to profiles: {str(save_error)}")
|
||||
logger.warning(f"Failed to process profile account information: {str(save_error)}")
|
||||
# Don't fail the whole process if saving account info fails
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to authenticate with XC server: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
|
|
@ -2017,6 +2038,200 @@ def sync_auto_channels(account_id, scan_start_time=None):
|
|||
return f"Auto sync error: {str(e)}"
|
||||
|
||||
|
||||
def get_transformed_credentials(account, profile=None):
|
||||
"""
|
||||
Get transformed credentials for XtreamCodes API calls.
|
||||
|
||||
Args:
|
||||
account: M3UAccount instance
|
||||
profile: M3UAccountProfile instance (optional, if not provided will use primary profile)
|
||||
|
||||
Returns:
|
||||
tuple: (transformed_url, transformed_username, transformed_password)
|
||||
"""
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
# If no profile is provided, find the primary active profile
|
||||
if profile is None:
|
||||
try:
|
||||
from apps.m3u.models import M3UAccountProfile
|
||||
profile = M3UAccountProfile.objects.filter(
|
||||
m3u_account=account,
|
||||
is_active=True
|
||||
).first()
|
||||
if profile:
|
||||
logger.debug(f"Using primary profile '{profile.name}' for URL transformation")
|
||||
else:
|
||||
logger.debug(f"No active profiles found for account {account.name}, using base credentials")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get primary profile for account {account.name}: {e}")
|
||||
profile = None
|
||||
|
||||
base_url = account.server_url
|
||||
base_username = account.username
|
||||
base_password = account.password # Build a complete URL with credentials (similar to how IPTV URLs are structured)
|
||||
# Format: http://server.com:port/username/password/rest_of_path
|
||||
if base_url and base_username and base_password:
|
||||
# Remove trailing slash from server URL if present
|
||||
clean_server_url = base_url.rstrip('/')
|
||||
|
||||
# Build the complete URL with embedded credentials
|
||||
complete_url = f"{clean_server_url}/{base_username}/{base_password}/"
|
||||
logger.debug(f"Built complete URL: {complete_url}")
|
||||
|
||||
# Apply profile-specific transformations if profile is provided
|
||||
if profile and profile.search_pattern and profile.replace_pattern:
|
||||
try:
|
||||
# Handle backreferences in the replacement pattern
|
||||
safe_replace_pattern = re.sub(r'\$(\d+)', r'\\\1', profile.replace_pattern)
|
||||
|
||||
# Apply transformation to the complete URL
|
||||
transformed_complete_url = re.sub(profile.search_pattern, safe_replace_pattern, complete_url)
|
||||
logger.info(f"Transformed complete URL: {complete_url} -> {transformed_complete_url}")
|
||||
|
||||
# Extract components from the transformed URL
|
||||
# Pattern: http://server.com:port/username/password/
|
||||
parsed_url = urllib.parse.urlparse(transformed_complete_url)
|
||||
path_parts = [part for part in parsed_url.path.split('/') if part]
|
||||
|
||||
if len(path_parts) >= 2:
|
||||
# Extract username and password from path
|
||||
transformed_username = path_parts[0]
|
||||
transformed_password = path_parts[1]
|
||||
|
||||
# Rebuild server URL without the username/password path
|
||||
transformed_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
||||
if parsed_url.port:
|
||||
transformed_url = f"{parsed_url.scheme}://{parsed_url.hostname}:{parsed_url.port}"
|
||||
|
||||
logger.debug(f"Extracted transformed credentials:")
|
||||
logger.debug(f" Server URL: {transformed_url}")
|
||||
logger.debug(f" Username: {transformed_username}")
|
||||
logger.debug(f" Password: {transformed_password}")
|
||||
|
||||
return transformed_url, transformed_username, transformed_password
|
||||
else:
|
||||
logger.warning(f"Could not extract credentials from transformed URL: {transformed_complete_url}")
|
||||
return base_url, base_username, base_password
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error transforming URL for profile {profile.name if profile else 'unknown'}: {e}")
|
||||
return base_url, base_username, base_password
|
||||
else:
|
||||
# No profile or no transformation patterns
|
||||
return base_url, base_username, base_password
|
||||
else:
|
||||
logger.warning(f"Missing credentials for account {account.name}")
|
||||
return base_url, base_username, base_password
|
||||
|
||||
|
||||
@shared_task
|
||||
def refresh_account_info(profile_id):
|
||||
"""Refresh only the account information for a specific M3U profile."""
|
||||
if not acquire_task_lock("refresh_account_info", profile_id):
|
||||
return f"Account info refresh task already running for profile_id={profile_id}."
|
||||
|
||||
try:
|
||||
from apps.m3u.models import M3UAccountProfile
|
||||
import re
|
||||
|
||||
profile = M3UAccountProfile.objects.get(id=profile_id)
|
||||
account = profile.m3u_account
|
||||
|
||||
if account.account_type != M3UAccount.Types.XC:
|
||||
release_task_lock("refresh_account_info", profile_id)
|
||||
return f"Profile {profile_id} belongs to account {account.id} which is not an XtreamCodes account."
|
||||
|
||||
# Get transformed credentials using the helper function
|
||||
transformed_url, transformed_username, transformed_password = get_transformed_credentials(account, profile)
|
||||
|
||||
# Initialize XtreamCodes client with extracted/transformed credentials
|
||||
client = XCClient(
|
||||
transformed_url,
|
||||
transformed_username,
|
||||
transformed_password,
|
||||
account.get_user_agent(),
|
||||
) # Authenticate and get account info
|
||||
auth_result = client.authenticate()
|
||||
if not auth_result:
|
||||
error_msg = f"Authentication failed for profile {profile.name} ({profile_id})"
|
||||
logger.error(error_msg)
|
||||
|
||||
# Send error notification to frontend via websocket
|
||||
send_websocket_update(
|
||||
"updates",
|
||||
"update",
|
||||
{
|
||||
"type": "account_info_refresh_error",
|
||||
"profile_id": profile_id,
|
||||
"profile_name": profile.name,
|
||||
"error": "Authentication failed with the provided credentials",
|
||||
"message": f"Failed to authenticate profile '{profile.name}'. Please check the credentials."
|
||||
}
|
||||
)
|
||||
|
||||
release_task_lock("refresh_account_info", profile_id)
|
||||
return error_msg
|
||||
|
||||
# Get account information
|
||||
account_info = client.get_account_info()
|
||||
|
||||
# Update only this specific profile with the new account info
|
||||
if not profile.custom_properties:
|
||||
profile.custom_properties = {}
|
||||
profile.custom_properties.update(account_info)
|
||||
profile.save()
|
||||
|
||||
# Send success notification to frontend via websocket
|
||||
send_websocket_update(
|
||||
"updates",
|
||||
"update",
|
||||
{
|
||||
"type": "account_info_refresh_success",
|
||||
"profile_id": profile_id,
|
||||
"profile_name": profile.name,
|
||||
"message": f"Account information successfully refreshed for profile '{profile.name}'"
|
||||
}
|
||||
)
|
||||
|
||||
release_task_lock("refresh_account_info", profile_id)
|
||||
return f"Account info refresh completed for profile {profile_id} ({profile.name})."
|
||||
|
||||
except M3UAccountProfile.DoesNotExist:
|
||||
error_msg = f"Profile {profile_id} not found"
|
||||
logger.error(error_msg)
|
||||
|
||||
send_websocket_update(
|
||||
"updates",
|
||||
"update",
|
||||
{
|
||||
"type": "account_refresh_error",
|
||||
"profile_id": profile_id,
|
||||
"error": "Profile not found",
|
||||
"message": f"Profile {profile_id} not found"
|
||||
}
|
||||
)
|
||||
|
||||
release_task_lock("refresh_account_info", profile_id)
|
||||
return error_msg
|
||||
except Exception as e:
|
||||
error_msg = f"Error refreshing account info for profile {profile_id}: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
|
||||
send_websocket_update(
|
||||
"updates",
|
||||
"update",
|
||||
{
|
||||
"type": "account_refresh_error",
|
||||
"profile_id": profile_id,
|
||||
"error": str(e),
|
||||
"message": f"Failed to refresh account info: {str(e)}"
|
||||
}
|
||||
)
|
||||
|
||||
release_task_lock("refresh_account_info", profile_id)
|
||||
return error_msg
|
||||
@shared_task
|
||||
def refresh_single_m3u_account(account_id):
|
||||
"""Splits M3U processing into chunks and dispatches them as parallel tasks."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue