Add custom properties to M3UAccountProfile and implement account info retrieval

This commit is contained in:
SergeantPanda 2025-09-09 16:05:58 -05:00
parent c0ddec6b4b
commit c023cde8fe
6 changed files with 263 additions and 1 deletions

View file

@ -1,6 +1,6 @@
from django.contrib import admin
from django.utils.html import format_html
from .models import M3UAccount, M3UFilter, ServerGroup, UserAgent
from .models import M3UAccount, M3UFilter, ServerGroup, UserAgent, M3UAccountProfile
import json
@ -83,3 +83,108 @@ class M3UFilterAdmin(admin.ModelAdmin):
class ServerGroupAdmin(admin.ModelAdmin):
list_display = ("name",)
search_fields = ("name",)
@admin.register(M3UAccountProfile)
class M3UAccountProfileAdmin(admin.ModelAdmin):
list_display = (
"name",
"m3u_account",
"is_default",
"is_active",
"max_streams",
"current_viewers",
"account_status_display",
"account_expiration_display",
"last_refresh_display",
)
list_filter = ("is_active", "is_default", "m3u_account__account_type")
search_fields = ("name", "m3u_account__name")
readonly_fields = ("account_info_display",)
def account_status_display(self, obj):
"""Display account status from custom properties"""
status = obj.get_account_status()
if status:
# Create colored status display
color_map = {
'Active': 'green',
'Expired': 'red',
'Disabled': 'red',
'Banned': 'red',
}
color = color_map.get(status, 'black')
return format_html(
'<span style="color: {};">{}</span>',
color,
status
)
return "Unknown"
account_status_display.short_description = "Account Status"
def account_expiration_display(self, obj):
"""Display account expiration from custom properties"""
expiration = obj.get_account_expiration()
if expiration:
from datetime import datetime
if expiration < datetime.now():
return format_html(
'<span style="color: red;">{}</span>',
expiration.strftime('%Y-%m-%d %H:%M')
)
else:
return format_html(
'<span style="color: green;">{}</span>',
expiration.strftime('%Y-%m-%d %H:%M')
)
return "Unknown"
account_expiration_display.short_description = "Expires"
def last_refresh_display(self, obj):
"""Display last refresh time from custom properties"""
last_refresh = obj.get_last_refresh()
if last_refresh:
return last_refresh.strftime('%Y-%m-%d %H:%M:%S')
return "Never"
last_refresh_display.short_description = "Last Refresh"
def account_info_display(self, obj):
"""Display formatted account information from custom properties"""
if not obj.custom_properties:
return "No account information available"
html_parts = []
# User Info
user_info = obj.custom_properties.get('user_info', {})
if user_info:
html_parts.append("<h3>User Information:</h3>")
html_parts.append("<ul>")
for key, value in user_info.items():
if key == 'exp_date' and value:
try:
from datetime import datetime
exp_date = datetime.fromtimestamp(float(value))
value = exp_date.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, TypeError):
pass
html_parts.append(f"<li><strong>{key}:</strong> {value}</li>")
html_parts.append("</ul>")
# Server Info
server_info = obj.custom_properties.get('server_info', {})
if server_info:
html_parts.append("<h3>Server Information:</h3>")
html_parts.append("<ul>")
for key, value in server_info.items():
html_parts.append(f"<li><strong>{key}:</strong> {value}</li>")
html_parts.append("</ul>")
# Last Refresh
last_refresh = obj.custom_properties.get('last_refresh')
if last_refresh:
html_parts.append(f"<p><strong>Last Refresh:</strong> {last_refresh}</p>")
return format_html(''.join(html_parts)) if html_parts else "No account information available"
account_info_display.short_description = "Account Information"

View file

@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-09-09 20:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('m3u', '0017_alter_m3uaccount_custom_properties_and_more'),
]
operations = [
migrations.AddField(
model_name='m3uaccountprofile',
name='custom_properties',
field=models.JSONField(blank=True, default=dict, help_text='Custom properties for storing account information from provider (e.g., XC account details, expiration dates)', null=True),
),
]

View file

@ -263,6 +263,12 @@ class M3UAccountProfile(models.Model):
max_length=255,
)
current_viewers = models.PositiveIntegerField(default=0)
custom_properties = models.JSONField(
default=dict,
blank=True,
null=True,
help_text="Custom properties for storing account information from provider (e.g., XC account details, expiration dates)"
)
class Meta:
constraints = [
@ -274,6 +280,70 @@ class M3UAccountProfile(models.Model):
def __str__(self):
return f"{self.name} ({self.m3u_account.name})"
def get_account_expiration(self):
"""Get account expiration date from custom properties if available"""
if not self.custom_properties:
return None
user_info = self.custom_properties.get('user_info', {})
exp_date = user_info.get('exp_date')
if exp_date:
try:
from datetime import datetime
# XC exp_date is typically a Unix timestamp
if isinstance(exp_date, (int, float)):
return datetime.fromtimestamp(exp_date)
elif isinstance(exp_date, str):
# Try to parse as timestamp first, then as ISO date
try:
return datetime.fromtimestamp(float(exp_date))
except ValueError:
return datetime.fromisoformat(exp_date)
except (ValueError, TypeError):
pass
return None
def get_account_status(self):
"""Get account status from custom properties if available"""
if not self.custom_properties:
return None
user_info = self.custom_properties.get('user_info', {})
return user_info.get('status')
def get_max_connections(self):
"""Get maximum connections from custom properties if available"""
if not self.custom_properties:
return None
user_info = self.custom_properties.get('user_info', {})
return user_info.get('max_connections')
def get_active_connections(self):
"""Get active connections from custom properties if available"""
if not self.custom_properties:
return None
user_info = self.custom_properties.get('user_info', {})
return user_info.get('active_cons')
def get_last_refresh(self):
"""Get last refresh timestamp from custom properties if available"""
if not self.custom_properties:
return None
last_refresh = self.custom_properties.get('last_refresh')
if last_refresh:
try:
from datetime import datetime
return datetime.fromisoformat(last_refresh)
except (ValueError, TypeError):
pass
return None
@receiver(models.signals.post_save, sender=M3UAccount)
def create_profile_for_m3u_account(sender, instance, created, **kwargs):

View file

@ -40,6 +40,7 @@ class M3UAccountProfileSerializer(serializers.ModelSerializer):
"current_viewers",
"search_pattern",
"replace_pattern",
"custom_properties",
]
read_only_fields = ["id"]

View file

@ -1191,6 +1191,33 @@ 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
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}")
except Exception as save_error:
logger.warning(f"Failed to save account information to profiles: {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)

View file

@ -136,6 +136,47 @@ class Client:
logger.error(traceback.format_exc())
raise
def get_account_info(self):
"""Get account information from the last authentication response"""
if not self.server_info:
raise ValueError("Not authenticated. Call authenticate() first.")
from datetime import datetime
# Extract relevant account information
user_info = self.server_info.get('user_info', {})
server_info = self.server_info.get('server_info', {})
account_info = {
'last_refresh': datetime.now().isoformat(),
'auth_timestamp': datetime.now().timestamp(),
'user_info': {
'username': user_info.get('username'),
'password': user_info.get('password'),
'message': user_info.get('message'),
'auth': user_info.get('auth'),
'status': user_info.get('status'),
'exp_date': user_info.get('exp_date'),
'is_trial': user_info.get('is_trial'),
'active_cons': user_info.get('active_cons'),
'created_at': user_info.get('created_at'),
'max_connections': user_info.get('max_connections'),
'allowed_output_formats': user_info.get('allowed_output_formats', [])
},
'server_info': {
'url': server_info.get('url'),
'port': server_info.get('port'),
'https_port': server_info.get('https_port'),
'server_protocol': server_info.get('server_protocol'),
'rtmp_port': server_info.get('rtmp_port'),
'timezone': server_info.get('timezone'),
'timestamp_now': server_info.get('timestamp_now'),
'time_now': server_info.get('time_now')
}
}
return account_info
def get_live_categories(self):
"""Get live TV categories"""
try: