mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-10-04 06:52:34 +02:00
Add subscription / Refactor config.js
This commit is contained in:
@@ -246,7 +246,7 @@ services:
|
||||
container_name: ultimate-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 7777:7777
|
||||
- "7777:7777"
|
||||
environment:
|
||||
- ULTIMATE_PORT=7777
|
||||
- ULTIMATE_DEBUG=false
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from typing import Dict, List, Optional
|
||||
from .provider import StreamingProvider
|
||||
from .models import StreamingChannel, DRMSystem
|
||||
from .models import UserSubscription
|
||||
from .drm import DRMPluginManager
|
||||
from .epg import EPGManager
|
||||
from .utils.logger import logger
|
||||
@@ -712,6 +713,124 @@ class ProviderManager:
|
||||
|
||||
return capabilities
|
||||
|
||||
# ============================================================================
|
||||
# SUBSCRIPTION METHODS (NEW)
|
||||
# ============================================================================
|
||||
|
||||
def get_subscription_status(self, provider_name: str, **kwargs) -> Optional[UserSubscription]:
|
||||
"""
|
||||
Get subscription status for a specific provider.
|
||||
|
||||
Args:
|
||||
provider_name: Name of the provider
|
||||
**kwargs: Additional arguments passed to provider
|
||||
|
||||
Returns:
|
||||
UserSubscription object or None if not supported/available
|
||||
|
||||
Raises:
|
||||
ValueError: If provider not found
|
||||
"""
|
||||
provider = self.get_provider(provider_name)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider '{provider_name}' not found")
|
||||
|
||||
try:
|
||||
subscription = provider.get_subscription_status(**kwargs)
|
||||
if subscription:
|
||||
logger.debug(f"Got subscription status for {provider_name}: "
|
||||
f"{subscription.package_count} packages, "
|
||||
f"{subscription.accessible_channel_count} channels")
|
||||
else:
|
||||
logger.debug(f"No subscription info available for {provider_name}")
|
||||
|
||||
return subscription
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting subscription status for {provider_name}: {e}")
|
||||
return None
|
||||
|
||||
def get_subscribed_channels(self, provider_name: str, **kwargs) -> List[StreamingChannel]:
|
||||
"""
|
||||
Get subscribed channels for a specific provider.
|
||||
|
||||
Args:
|
||||
provider_name: Name of the provider
|
||||
**kwargs: Additional arguments passed to provider
|
||||
|
||||
Returns:
|
||||
List of StreamingChannel objects the user can access
|
||||
|
||||
Raises:
|
||||
ValueError: If provider not found
|
||||
"""
|
||||
provider = self.get_provider(provider_name)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider '{provider_name}' not found")
|
||||
|
||||
try:
|
||||
channels = provider.get_subscribed_channels(**kwargs)
|
||||
logger.info(f"Got {len(channels)} subscribed channels from {provider_name}")
|
||||
return channels
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscribed channels for {provider_name}: {e}")
|
||||
# Fallback to all channels on error
|
||||
return provider.get_channels(**kwargs)
|
||||
|
||||
def get_available_packages(self, provider_name: str, **kwargs) -> List[SubscriptionPackage]:
|
||||
"""
|
||||
Get available subscription packages for a provider.
|
||||
|
||||
Args:
|
||||
provider_name: Name of the provider
|
||||
**kwargs: Additional arguments passed to provider
|
||||
|
||||
Returns:
|
||||
List of available SubscriptionPackage objects
|
||||
Empty list if not supported/available
|
||||
|
||||
Raises:
|
||||
ValueError: If provider not found
|
||||
"""
|
||||
provider = self.get_provider(provider_name)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider '{provider_name}' not found")
|
||||
|
||||
try:
|
||||
packages = provider.get_available_packages(**kwargs)
|
||||
logger.debug(f"Got {len(packages)} available packages from {provider_name}")
|
||||
return packages
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting available packages for {provider_name}: {e}")
|
||||
return []
|
||||
|
||||
def is_channel_accessible(self, provider_name: str, channel_id: str, **kwargs) -> bool:
|
||||
"""
|
||||
Check if a channel is accessible with current subscription.
|
||||
|
||||
Args:
|
||||
provider_name: Name of the provider
|
||||
channel_id: ID of the channel to check
|
||||
**kwargs: Additional arguments passed to provider
|
||||
|
||||
Returns:
|
||||
True if accessible, False otherwise
|
||||
True if subscription checking not supported
|
||||
|
||||
Raises:
|
||||
ValueError: If provider not found
|
||||
"""
|
||||
provider = self.get_provider(provider_name)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider '{provider_name}' not found")
|
||||
|
||||
try:
|
||||
accessible = provider.is_channel_accessible(channel_id, **kwargs)
|
||||
logger.debug(f"Channel {channel_id} accessible for {provider_name}: {accessible}")
|
||||
return accessible
|
||||
except Exception as e:
|
||||
logger.warning(f"Error checking channel accessibility for {provider_name}/{channel_id}: {e}")
|
||||
return True # Assume accessible on error (backward compatibility)
|
||||
|
||||
def _extract_pssh_from_manifest(self, manifest_url: str) -> List:
|
||||
"""
|
||||
Extract PSSH data from a manifest URL.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# streaming_providers/base/models/__init__.py
|
||||
from .streaming_channel import StreamingChannel
|
||||
from .drm_models import DRMConfig, LicenseConfig, DRMSystem, LicenseUnwrapperParams
|
||||
from .subscription import SubscriptionPackage, UserSubscription
|
||||
|
||||
__all__ = ['StreamingChannel', 'DRMConfig', 'LicenseConfig', 'LicenseUnwrapperParams', 'DRMSystem']
|
||||
|
||||
__all__ = ['StreamingChannel', 'DRMConfig', 'LicenseConfig', 'LicenseUnwrapperParams', 'DRMSystem', 'SubscriptionPackage', 'UserSubscription']
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Subscription models for provider packages and user entitlements.
|
||||
"""
|
||||
from typing import Dict, List, Optional, Set
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubscriptionPackage:
|
||||
"""Represents a subscription package with provider-specific naming"""
|
||||
|
||||
package_id: str
|
||||
"""Internal ID (e.g., 'sports_package_2024', 'joyn_plus')"""
|
||||
|
||||
name: str
|
||||
"""Display name (e.g., 'Sports Package', 'Sky Sport', 'Joyn Plus')"""
|
||||
|
||||
description: Optional[str] = None
|
||||
"""Optional description of the package"""
|
||||
|
||||
price_info: Optional[str] = None
|
||||
"""Optional price information (e.g., '€9.99/month', 'included')"""
|
||||
|
||||
channel_ids: List[str] = field(default_factory=list)
|
||||
"""List of channel IDs included in this package"""
|
||||
|
||||
metadata: Dict[str, any] = field(default_factory=dict)
|
||||
"""Provider-specific metadata (e.g., {'sky_id': 'SPORT1', 'category': 'sports'})"""
|
||||
|
||||
@property
|
||||
def channel_count(self) -> int:
|
||||
"""Number of channels in this package"""
|
||||
return len(self.channel_ids)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserSubscription:
|
||||
"""User's subscription status for a provider"""
|
||||
|
||||
provider: str
|
||||
"""Provider name (e.g., 'joyn', 'magenta')"""
|
||||
|
||||
country: str
|
||||
"""Country code (e.g., 'DE', 'AT')"""
|
||||
|
||||
active: bool = False
|
||||
"""Whether the subscription is currently active"""
|
||||
|
||||
packages: List[SubscriptionPackage] = field(default_factory=list)
|
||||
"""All subscription packages the user has access to"""
|
||||
|
||||
accessible_channel_ids: Set[str] = field(default_factory=set)
|
||||
"""Set of all channel IDs the user can access (derived from packages)"""
|
||||
|
||||
valid_from: Optional[datetime] = None
|
||||
"""When the subscription becomes/starts valid"""
|
||||
|
||||
valid_until: Optional[datetime] = None
|
||||
"""When the subscription expires"""
|
||||
|
||||
status_message: Optional[str] = None
|
||||
"""Human-readable status message (e.g., 'Active until 2024-12-31')"""
|
||||
|
||||
billing_status: Optional[str] = None
|
||||
"""Billing status (e.g., 'paid', 'trial', 'expired', 'cancelled')"""
|
||||
|
||||
def __post_init__(self):
|
||||
"""Populate derived fields after initialization"""
|
||||
self._update_derived_fields()
|
||||
|
||||
def _update_derived_fields(self):
|
||||
"""Update derived fields like accessible_channel_ids"""
|
||||
self.accessible_channel_ids.clear()
|
||||
for package in self.packages:
|
||||
self.accessible_channel_ids.update(package.channel_ids)
|
||||
|
||||
@property
|
||||
def has_packages(self) -> bool:
|
||||
"""Check if user has any packages"""
|
||||
return len(self.packages) > 0
|
||||
|
||||
@property
|
||||
def package_count(self) -> int:
|
||||
"""Number of packages"""
|
||||
return len(self.packages)
|
||||
|
||||
@property
|
||||
def accessible_channel_count(self) -> int:
|
||||
"""Number of accessible channels"""
|
||||
return len(self.accessible_channel_ids)
|
||||
|
||||
@property
|
||||
def package_names(self) -> List[str]:
|
||||
"""List of package names for display"""
|
||||
return [pkg.name for pkg in self.packages]
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert to dictionary for JSON serialization"""
|
||||
return {
|
||||
'provider': self.provider,
|
||||
'country': self.country,
|
||||
'active': self.active,
|
||||
'has_packages': self.has_packages,
|
||||
'package_count': self.package_count,
|
||||
'package_names': self.package_names,
|
||||
'accessible_channel_count': self.accessible_channel_count,
|
||||
'valid_from': self.valid_from.isoformat() if self.valid_from else None,
|
||||
'valid_until': self.valid_until.isoformat() if self.valid_until else None,
|
||||
'status_message': self.status_message,
|
||||
'billing_status': self.billing_status,
|
||||
'packages': [
|
||||
{
|
||||
'package_id': pkg.package_id,
|
||||
'name': pkg.name,
|
||||
'description': pkg.description,
|
||||
'price_info': pkg.price_info,
|
||||
'channel_count': pkg.channel_count,
|
||||
'metadata': pkg.metadata
|
||||
}
|
||||
for pkg in self.packages
|
||||
]
|
||||
}
|
||||
|
||||
def add_package(self, package: SubscriptionPackage):
|
||||
"""Add a package and update derived fields"""
|
||||
self.packages.append(package)
|
||||
self._update_derived_fields()
|
||||
|
||||
def can_access_channel(self, channel_id: str) -> bool:
|
||||
"""Check if user can access a specific channel"""
|
||||
return channel_id in self.accessible_channel_ids
|
||||
@@ -77,6 +77,7 @@ from datetime import datetime
|
||||
from .models.streaming_channel import StreamingChannel
|
||||
from .models.drm_models import DRMConfig
|
||||
from .models.proxy_models import ProxyConfig
|
||||
from .models.subscription import UserSubscription, SubscriptionPackage
|
||||
from .network import HTTPManagerFactory, HTTPManager
|
||||
from .utils.logger import logger
|
||||
from ..providers.auth import AuthContext, AuthStatus
|
||||
@@ -760,6 +761,107 @@ class StreamingProvider(ABC):
|
||||
separator = '&' if '?' in base_url else '?'
|
||||
return f"{base_url}{separator}start={start_time}&end={end_time}"
|
||||
|
||||
# ============================================================================
|
||||
# SUBSCRIPTION METHODS (NEW)
|
||||
# ============================================================================
|
||||
|
||||
def get_subscription_status(self, **kwargs) -> Optional[UserSubscription]:
|
||||
"""
|
||||
Get user's subscription status for this provider.
|
||||
|
||||
Returns:
|
||||
UserSubscription object with subscription details, or
|
||||
None if provider doesn't support subscription queries or
|
||||
subscription info isn't available.
|
||||
|
||||
Default implementation returns None (subscription not supported).
|
||||
Override in provider plugins that support subscription checking.
|
||||
|
||||
Example usage in providers:
|
||||
# Query provider API for user entitlements
|
||||
# Parse response into SubscriptionPackage objects
|
||||
# Return UserSubscription with packages and accessible channels
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_subscribed_channels(self, **kwargs) -> List[StreamingChannel]:
|
||||
"""
|
||||
Get channels the current user is subscribed to.
|
||||
|
||||
This method:
|
||||
1. Gets subscription status (if supported)
|
||||
2. Filters all channels based on accessible channel IDs
|
||||
3. Returns filtered list or all channels as fallback
|
||||
|
||||
Returns:
|
||||
List of StreamingChannel objects that the user can access
|
||||
|
||||
Note:
|
||||
Override get_subscription_status() in provider plugins
|
||||
to enable subscription filtering.
|
||||
"""
|
||||
# Get all channels first
|
||||
all_channels = self.get_channels(**kwargs)
|
||||
|
||||
# Try to get subscription status
|
||||
subscription = self.get_subscription_status(**kwargs)
|
||||
|
||||
# If no subscription info or not active, return all channels
|
||||
if not subscription or not subscription.active:
|
||||
return all_channels
|
||||
|
||||
# Filter channels based on accessible channel IDs
|
||||
if subscription.accessible_channel_ids:
|
||||
return [
|
||||
channel for channel in all_channels
|
||||
if channel.channel_id in subscription.accessible_channel_ids
|
||||
]
|
||||
|
||||
# No filtering possible, return all channels
|
||||
return all_channels
|
||||
|
||||
def get_available_packages(self, **kwargs) -> List[SubscriptionPackage]:
|
||||
"""
|
||||
Get all subscription packages available from this provider.
|
||||
|
||||
Useful for:
|
||||
- Displaying upgrade options in UI
|
||||
- Showing package comparison
|
||||
- Subscription management interface
|
||||
|
||||
Returns:
|
||||
List of available SubscriptionPackage objects
|
||||
Empty list if not implemented or no packages available
|
||||
|
||||
Default implementation returns empty list.
|
||||
Override in provider plugins that have package information.
|
||||
"""
|
||||
return []
|
||||
|
||||
def is_channel_accessible(self, channel_id: str, **kwargs) -> bool:
|
||||
"""
|
||||
Check if a specific channel is accessible with current subscription.
|
||||
|
||||
Args:
|
||||
channel_id: ID of the channel to check
|
||||
**kwargs: Additional arguments passed to get_subscription_status()
|
||||
|
||||
Returns:
|
||||
True if channel is accessible, False otherwise.
|
||||
Returns True if subscription checking is not supported.
|
||||
|
||||
Note:
|
||||
This is a convenience method for quick checks.
|
||||
"""
|
||||
subscription = self.get_subscription_status(**kwargs)
|
||||
|
||||
# If no subscription info, assume accessible (backward compatibility)
|
||||
if not subscription or not subscription.active:
|
||||
return True
|
||||
|
||||
# Check if channel is in accessible set
|
||||
return subscription.can_access_channel(channel_id)
|
||||
|
||||
@classmethod
|
||||
def get_supported_countries(cls) -> List[str]:
|
||||
"""
|
||||
|
||||
+33
-30
@@ -446,36 +446,6 @@ body {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.providers-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.token-expiration {
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
@@ -510,4 +480,37 @@ body {
|
||||
|
||||
.provider-info .token-expiration + .token-expiration {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* EPG Mapping specific styles - moved to separate file */
|
||||
/* .epg-mapping-container etc. */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.providers-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,9 @@
|
||||
</div>
|
||||
|
||||
<div id="proxy-container" class="providers-grid">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
<p>Select a provider from the credentials tab to configure proxy settings</p>
|
||||
<div class="empty-state" id="loading-proxy">
|
||||
<i class="fas fa-spinner loader"></i>
|
||||
<p>Loading proxy configurations...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+12
-179
@@ -8,7 +8,6 @@ const API_BASE = window.location.origin;
|
||||
const tabs = document.querySelectorAll('.tab');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
const credentialsContainer = document.getElementById('credentials-container');
|
||||
const proxyContainer = document.getElementById('proxy-container');
|
||||
const jsonEditor = document.getElementById('json-editor');
|
||||
const importModal = document.getElementById('import-modal');
|
||||
const importFile = document.getElementById('import-file');
|
||||
@@ -41,11 +40,12 @@ function switchTab(tabId) {
|
||||
currentTab = tabId;
|
||||
|
||||
// Load proxy data if switching to proxy tab
|
||||
if (tabId === 'proxy') {
|
||||
loadProxyForms();
|
||||
if (tabId === 'proxy' && window.proxyManager) {
|
||||
window.proxyManager.init(providers);
|
||||
window.proxyManager.loadProxyForms();
|
||||
}
|
||||
|
||||
// Load EPG mappings if switching to that tab
|
||||
// Load EPG mappings if switching to that tab
|
||||
if (tabId === 'epg-mapping' && window.epgMappingManager) {
|
||||
window.epgMappingManager.loadProviders();
|
||||
}
|
||||
@@ -427,108 +427,6 @@ async function renderCredentialsForms() {
|
||||
}
|
||||
}
|
||||
|
||||
// Load proxy forms
|
||||
async function loadProxyForms() {
|
||||
proxyContainer.innerHTML = await Promise.all(providers.map(async (provider) => {
|
||||
try {
|
||||
// Try to load existing proxy config
|
||||
const proxyResponse = await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`);
|
||||
let existingProxy = null;
|
||||
|
||||
if (proxyResponse.ok) {
|
||||
const proxyData = await proxyResponse.json();
|
||||
if (proxyData.proxy_config) {
|
||||
existingProxy = proxyData.proxy_config;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="provider-card" data-provider="${provider.name}">
|
||||
<div class="provider-header">
|
||||
${provider.logo ? `<img src="${provider.logo}" alt="${provider.label}" class="provider-logo">` : ''}
|
||||
<div class="provider-info">
|
||||
<h3>${provider.label}</h3>
|
||||
<div class="provider-id">Proxy Configuration</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-host-${provider.name}">
|
||||
<i class="fas fa-server"></i> Proxy Host
|
||||
</label>
|
||||
<input type="text"
|
||||
id="proxy-host-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="proxy.example.com"
|
||||
value="${existingProxy?.host || ''}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-port-${provider.name}">
|
||||
<i class="fas fa-plug"></i> Proxy Port
|
||||
</label>
|
||||
<input type="number"
|
||||
id="proxy-port-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="8080"
|
||||
min="1"
|
||||
max="65535"
|
||||
value="${existingProxy?.port || ''}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-type-${provider.name}">
|
||||
<i class="fas fa-network-wired"></i> Proxy Type
|
||||
</label>
|
||||
<select id="proxy-type-${provider.name}" class="form-control">
|
||||
<option value="http" ${(!existingProxy || existingProxy.proxy_type === 'http') ? 'selected' : ''}>HTTP</option>
|
||||
<option value="https" ${(existingProxy?.proxy_type === 'https') ? 'selected' : ''}>HTTPS</option>
|
||||
<option value="socks4" ${(existingProxy?.proxy_type === 'socks4') ? 'selected' : ''}>SOCKS4</option>
|
||||
<option value="socks5" ${(existingProxy?.proxy_type === 'socks5') ? 'selected' : ''}>SOCKS5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-user-${provider.name}">
|
||||
<i class="fas fa-user"></i> Username (Optional)
|
||||
</label>
|
||||
<input type="text"
|
||||
id="proxy-user-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="proxyuser"
|
||||
value="${existingProxy?.auth?.username || ''}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="proxy-pass-${provider.name}">
|
||||
<i class="fas fa-lock"></i> Password (Optional)
|
||||
</label>
|
||||
<input type="password"
|
||||
id="proxy-pass-${provider.name}"
|
||||
class="form-control"
|
||||
placeholder="••••••••"
|
||||
value="${existingProxy?.auth?.password || ''}">
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button onclick="saveProxy('${provider.name}')" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> ${existingProxy ? 'Update' : 'Save'} Proxy
|
||||
</button>
|
||||
${existingProxy ? `
|
||||
<button onclick="deleteProxy('${provider.name}')" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Delete Proxy
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error(`Error loading proxy for ${provider.name}:`, error);
|
||||
return ''; // Return empty string on error
|
||||
}
|
||||
})).then(htmls => htmls.join(''));
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function getStatusIcon(status) {
|
||||
if (!status) return '<i class="fas fa-question-circle"></i>';
|
||||
@@ -831,73 +729,6 @@ async function testAuth(providerName) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProxy(providerName) {
|
||||
const host = document.getElementById(`proxy-host-${providerName}`).value;
|
||||
const port = document.getElementById(`proxy-port-${providerName}`).value;
|
||||
const type = document.getElementById(`proxy-type-${providerName}`).value;
|
||||
const user = document.getElementById(`proxy-user-${providerName}`).value;
|
||||
const pass = document.getElementById(`proxy-pass-${providerName}`).value;
|
||||
|
||||
if (!host || !port) {
|
||||
showAlert('error', 'Please fill in at least Host and Port fields');
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyConfig = {
|
||||
host,
|
||||
port: parseInt(port),
|
||||
proxy_type: type
|
||||
};
|
||||
|
||||
if (user) proxyConfig.username = user;
|
||||
if (pass) proxyConfig.password = pass;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/providers/${providerName}/proxy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(proxyConfig)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('success', `Proxy configuration saved for ${providerName}`);
|
||||
// Clear password field
|
||||
document.getElementById(`proxy-pass-${providerName}`).value = '';
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showAlert('error', result.error || `Failed to save proxy for ${providerName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', `Network error: ${error.message}`);
|
||||
console.error('Proxy save error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProxy(providerName) {
|
||||
if (!confirm(`Delete proxy configuration for ${providerName}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/providers/${providerName}/proxy`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showAlert('success', `Proxy configuration deleted for ${providerName}`);
|
||||
// Clear fields
|
||||
document.getElementById(`proxy-host-${providerName}`).value = '';
|
||||
document.getElementById(`proxy-port-${providerName}`).value = '';
|
||||
document.getElementById(`proxy-user-${providerName}`).value = '';
|
||||
document.getElementById(`proxy-pass-${providerName}`).value = '';
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showAlert('error', result.error || `Failed to delete proxy for ${providerName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', `Network error: ${error.message}`);
|
||||
console.error('Proxy delete error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// UI Helper Functions
|
||||
function showAlert(type, message) {
|
||||
const alert = document.createElement('div');
|
||||
@@ -986,7 +817,6 @@ async function importConfig() {
|
||||
|
||||
// Reload providers
|
||||
await loadProviders();
|
||||
loadProxyForms();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showAlert('error', result.error || 'Import failed');
|
||||
@@ -1018,7 +848,6 @@ async function applyJsonConfig() {
|
||||
|
||||
// Reload providers
|
||||
await loadProviders();
|
||||
loadProxyForms();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showAlert('error', result.error || 'Failed to apply configuration');
|
||||
@@ -1064,6 +893,7 @@ async function clearAllConfigurations() {
|
||||
});
|
||||
}
|
||||
|
||||
// Delete proxy configurations
|
||||
await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
@@ -1073,7 +903,12 @@ async function clearAllConfigurations() {
|
||||
|
||||
// Reload providers
|
||||
await loadProviders();
|
||||
loadProxyForms();
|
||||
|
||||
// Reload proxy forms if proxy manager exists and we're on proxy tab
|
||||
if (window.proxyManager && currentTab === 'proxy') {
|
||||
window.proxyManager.init(data.providers);
|
||||
window.proxyManager.loadProxyForms();
|
||||
}
|
||||
} catch (error) {
|
||||
showAlert('error', `Failed to clear configurations: ${error.message}`);
|
||||
}
|
||||
@@ -1082,6 +917,4 @@ async function clearAllConfigurations() {
|
||||
// Make functions available globally
|
||||
window.saveCredentials = saveCredentials;
|
||||
window.deleteCredentials = deleteCredentials;
|
||||
window.testAuth = testAuth;
|
||||
window.saveProxy = saveProxy;
|
||||
window.deleteProxy = deleteProxy;
|
||||
window.testAuth = testAuth;
|
||||
@@ -0,0 +1,263 @@
|
||||
/* Proxy-specific styles */
|
||||
|
||||
.proxy-card {
|
||||
background: var(--light-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
border: 1px solid #dee2e6;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.proxy-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.proxy-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.proxy-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
object-fit: contain;
|
||||
background: white;
|
||||
padding: 8px;
|
||||
border: 2px solid var(--primary-color);
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.proxy-info h3 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.proxy-info .proxy-id {
|
||||
color: var(--gray-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Proxy form specific styles */
|
||||
.proxy-form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.proxy-form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.proxy-form-control {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.proxy-form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
|
||||
}
|
||||
|
||||
.proxy-select {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.proxy-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
|
||||
}
|
||||
|
||||
.proxy-btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Proxy type indicators */
|
||||
.proxy-type-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.proxy-type-http {
|
||||
background: rgba(67, 97, 238, 0.1);
|
||||
color: var(--primary-color);
|
||||
border: 1px solid rgba(67, 97, 238, 0.3);
|
||||
}
|
||||
|
||||
.proxy-type-https {
|
||||
background: rgba(76, 201, 240, 0.1);
|
||||
color: var(--success-color);
|
||||
border: 1px solid rgba(76, 201, 240, 0.3);
|
||||
}
|
||||
|
||||
.proxy-type-socks4 {
|
||||
background: rgba(255, 158, 0, 0.1);
|
||||
color: var(--warning-color);
|
||||
border: 1px solid rgba(255, 158, 0, 0.3);
|
||||
}
|
||||
|
||||
.proxy-type-socks5 {
|
||||
background: rgba(247, 37, 133, 0.1);
|
||||
color: var(--danger-color);
|
||||
border: 1px solid rgba(247, 37, 133, 0.3);
|
||||
}
|
||||
|
||||
/* Proxy status */
|
||||
.proxy-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.proxy-status-active {
|
||||
background: rgba(76, 201, 240, 0.2);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.proxy-status-inactive {
|
||||
background: rgba(108, 117, 125, 0.2);
|
||||
color: var(--gray-color);
|
||||
}
|
||||
|
||||
.proxy-status-error {
|
||||
background: rgba(247, 37, 133, 0.2);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
/* Proxy scope settings */
|
||||
.proxy-scope-settings {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 15px;
|
||||
margin-top: 15px;
|
||||
border-left: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.proxy-scope-settings h4 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1rem;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
.scope-checkboxes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.scope-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.scope-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.scope-checkbox label {
|
||||
margin-bottom: 0;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Advanced proxy options */
|
||||
.proxy-advanced-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.proxy-advanced-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.proxy-advanced-content.expanded {
|
||||
max-height: 500px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* Proxy test panel */
|
||||
.proxy-test-panel {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
border: 1px dashed #dee2e6;
|
||||
}
|
||||
|
||||
.proxy-test-result {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.proxy-test-result.success {
|
||||
background: rgba(76, 201, 240, 0.1);
|
||||
border-left: 4px solid var(--success-color);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.proxy-test-result.error {
|
||||
background: rgba(247, 37, 133, 0.1);
|
||||
border-left: 4px solid var(--danger-color);
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Responsive design for proxy */
|
||||
@media (max-width: 768px) {
|
||||
.scope-checkboxes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.proxy-btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.proxy-btn-group .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
// Proxy Management Module
|
||||
|
||||
class ProxyManager {
|
||||
constructor(API_BASE) {
|
||||
this.API_BASE = API_BASE;
|
||||
this.providers = [];
|
||||
}
|
||||
|
||||
// Initialize proxy manager
|
||||
init(providers) {
|
||||
this.providers = providers;
|
||||
}
|
||||
|
||||
// Load proxy forms
|
||||
async loadProxyForms() {
|
||||
const proxyContainer = document.getElementById('proxy-container');
|
||||
|
||||
if (!this.providers || this.providers.length === 0) {
|
||||
proxyContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
<p>No providers available</p>
|
||||
<p>Load providers from credentials tab first</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const htmls = await Promise.all(this.providers.map(async (provider) => {
|
||||
return await this.createProxyCard(provider);
|
||||
}));
|
||||
|
||||
proxyContainer.innerHTML = htmls.join('');
|
||||
}
|
||||
|
||||
// Create proxy card HTML for a provider
|
||||
async createProxyCard(provider) {
|
||||
try {
|
||||
// Try to load existing proxy config
|
||||
const existingProxy = await this.getProxyConfig(provider.name);
|
||||
|
||||
return `
|
||||
<div class="proxy-card" data-provider="${provider.name}">
|
||||
<div class="proxy-header">
|
||||
${provider.logo ? `<img src="${provider.logo}" alt="${provider.label}" class="proxy-logo">` : ''}
|
||||
<div class="proxy-info">
|
||||
<h3>${provider.label}</h3>
|
||||
<div class="proxy-id">Proxy Configuration • ${provider.country}</div>
|
||||
${existingProxy ? `
|
||||
<div class="proxy-status proxy-status-active">
|
||||
<i class="fas fa-check-circle"></i> Proxy Configured
|
||||
<span class="proxy-type-indicator proxy-type-${existingProxy.proxy_type || 'http'}">
|
||||
${(existingProxy.proxy_type || 'http').toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
` : `
|
||||
<div class="proxy-status proxy-status-inactive">
|
||||
<i class="fas fa-times-circle"></i> No Proxy
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proxy-form-group">
|
||||
<label for="proxy-host-${provider.name}">
|
||||
<i class="fas fa-server"></i> Proxy Host
|
||||
</label>
|
||||
<input type="text"
|
||||
id="proxy-host-${provider.name}"
|
||||
class="proxy-form-control"
|
||||
placeholder="proxy.example.com"
|
||||
value="${existingProxy?.host || ''}">
|
||||
</div>
|
||||
|
||||
<div class="proxy-form-group">
|
||||
<label for="proxy-port-${provider.name}">
|
||||
<i class="fas fa-plug"></i> Proxy Port
|
||||
</label>
|
||||
<input type="number"
|
||||
id="proxy-port-${provider.name}"
|
||||
class="proxy-form-control"
|
||||
placeholder="8080"
|
||||
min="1"
|
||||
max="65535"
|
||||
value="${existingProxy?.port || ''}">
|
||||
</div>
|
||||
|
||||
<div class="proxy-form-group">
|
||||
<label for="proxy-type-${provider.name}">
|
||||
<i class="fas fa-network-wired"></i> Proxy Type
|
||||
</label>
|
||||
<select id="proxy-type-${provider.name}" class="proxy-select">
|
||||
<option value="http" ${(!existingProxy || existingProxy.proxy_type === 'http') ? 'selected' : ''}>HTTP</option>
|
||||
<option value="https" ${(existingProxy?.proxy_type === 'https') ? 'selected' : ''}>HTTPS</option>
|
||||
<option value="socks4" ${(existingProxy?.proxy_type === 'socks4') ? 'selected' : ''}>SOCKS4</option>
|
||||
<option value="socks5" ${(existingProxy?.proxy_type === 'socks5') ? 'selected' : ''}>SOCKS5</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="proxy-form-group">
|
||||
<label for="proxy-user-${provider.name}">
|
||||
<i class="fas fa-user"></i> Username (Optional)
|
||||
</label>
|
||||
<input type="text"
|
||||
id="proxy-user-${provider.name}"
|
||||
class="proxy-form-control"
|
||||
placeholder="proxyuser"
|
||||
value="${existingProxy?.auth?.username || ''}">
|
||||
</div>
|
||||
|
||||
<div class="proxy-form-group">
|
||||
<label for="proxy-pass-${provider.name}">
|
||||
<i class="fas fa-lock"></i> Password (Optional)
|
||||
</label>
|
||||
<input type="password"
|
||||
id="proxy-pass-${provider.name}"
|
||||
class="proxy-form-control"
|
||||
placeholder="••••••••"
|
||||
value="${existingProxy?.auth?.password || ''}">
|
||||
</div>
|
||||
|
||||
${this.createAdvancedProxySection(provider.name, existingProxy)}
|
||||
|
||||
<div class="proxy-btn-group">
|
||||
<button onclick="window.proxyManager.saveProxy('${provider.name}')" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> ${existingProxy ? 'Update' : 'Save'} Proxy
|
||||
</button>
|
||||
${existingProxy ? `
|
||||
<button onclick="window.proxyManager.deleteProxy('${provider.name}')" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Delete Proxy
|
||||
</button>
|
||||
` : ''}
|
||||
<button onclick="window.proxyManager.testProxy('${provider.name}')" class="btn btn-success">
|
||||
<i class="fas fa-vial"></i> Test Proxy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="proxy-test-result-${provider.name}" class="proxy-test-result"></div>
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error(`Error creating proxy card for ${provider.name}:`, error);
|
||||
return ''; // Return empty string on error
|
||||
}
|
||||
}
|
||||
|
||||
// Create advanced proxy section
|
||||
createAdvancedProxySection(providerName, existingProxy) {
|
||||
const scope = existingProxy?.scope || {
|
||||
api_calls: true,
|
||||
authentication: true,
|
||||
manifests: true,
|
||||
license: true,
|
||||
all: true
|
||||
};
|
||||
|
||||
return `
|
||||
<div class="proxy-advanced-toggle" onclick="window.proxyManager.toggleAdvanced('${providerName}')">
|
||||
<i class="fas fa-chevron-down" id="proxy-advanced-icon-${providerName}"></i>
|
||||
<span>Advanced Options</span>
|
||||
</div>
|
||||
|
||||
<div class="proxy-advanced-content" id="proxy-advanced-content-${providerName}">
|
||||
<div class="proxy-scope-settings">
|
||||
<h4><i class="fas fa-filter"></i> Proxy Scope</h4>
|
||||
<p>Select which operations should use this proxy:</p>
|
||||
<div class="scope-checkboxes">
|
||||
<div class="scope-checkbox">
|
||||
<input type="checkbox"
|
||||
id="proxy-scope-api-${providerName}"
|
||||
${scope.all || scope.api_calls ? 'checked' : ''}>
|
||||
<label for="proxy-scope-api-${providerName}">API Calls</label>
|
||||
</div>
|
||||
<div class="scope-checkbox">
|
||||
<input type="checkbox"
|
||||
id="proxy-scope-auth-${providerName}"
|
||||
${scope.all || scope.authentication ? 'checked' : ''}>
|
||||
<label for="proxy-scope-auth-${providerName}">Authentication</label>
|
||||
</div>
|
||||
<div class="scope-checkbox">
|
||||
<input type="checkbox"
|
||||
id="proxy-scope-manifests-${providerName}"
|
||||
${scope.all || scope.manifests ? 'checked' : ''}>
|
||||
<label for="proxy-scope-manifests-${providerName}">Manifests</label>
|
||||
</div>
|
||||
<div class="scope-checkbox">
|
||||
<input type="checkbox"
|
||||
id="proxy-scope-license-${providerName}"
|
||||
${scope.all || scope.license ? 'checked' : ''}>
|
||||
<label for="proxy-scope-license-${providerName}">License Requests</label>
|
||||
</div>
|
||||
<div class="scope-checkbox">
|
||||
<input type="checkbox"
|
||||
id="proxy-scope-all-${providerName}"
|
||||
${scope.all ? 'checked' : ''}>
|
||||
<label for="proxy-scope-all-${providerName}">All Operations</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="proxy-form-group">
|
||||
<label for="proxy-timeout-${providerName}">
|
||||
<i class="fas fa-clock"></i> Timeout (seconds)
|
||||
</label>
|
||||
<input type="number"
|
||||
id="proxy-timeout-${providerName}"
|
||||
class="proxy-form-control"
|
||||
placeholder="30"
|
||||
min="1"
|
||||
max="300"
|
||||
value="${existingProxy?.timeout || 30}">
|
||||
</div>
|
||||
|
||||
<div class="proxy-form-group">
|
||||
<label for="proxy-verify-ssl-${providerName}" style="display: flex; align-items: center;">
|
||||
<input type="checkbox"
|
||||
id="proxy-verify-ssl-${providerName}"
|
||||
${existingProxy?.verify_ssl !== false ? 'checked' : ''}
|
||||
style="margin-right: 8px;">
|
||||
Verify SSL Certificate
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Get proxy configuration from API
|
||||
async getProxyConfig(providerName) {
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE}/api/providers/${providerName}/proxy`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.proxy_config || null;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`Error getting proxy config for ${providerName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Save proxy configuration
|
||||
async saveProxy(providerName) {
|
||||
const host = document.getElementById(`proxy-host-${providerName}`).value.trim();
|
||||
const port = document.getElementById(`proxy-port-${providerName}`).value.trim();
|
||||
const type = document.getElementById(`proxy-type-${providerName}`).value;
|
||||
const user = document.getElementById(`proxy-user-${providerName}`).value.trim();
|
||||
const pass = document.getElementById(`proxy-pass-${providerName}`).value;
|
||||
|
||||
// Advanced options
|
||||
const timeout = document.getElementById(`proxy-timeout-${providerName}`)?.value || 30;
|
||||
const verifySsl = document.getElementById(`proxy-verify-ssl-${providerName}`)?.checked !== false;
|
||||
|
||||
// Scope settings
|
||||
const scope = {
|
||||
api_calls: document.getElementById(`proxy-scope-api-${providerName}`)?.checked || false,
|
||||
authentication: document.getElementById(`proxy-scope-auth-${providerName}`)?.checked || false,
|
||||
manifests: document.getElementById(`proxy-scope-manifests-${providerName}`)?.checked || false,
|
||||
license: document.getElementById(`proxy-scope-license-${providerName}`)?.checked || false,
|
||||
all: document.getElementById(`proxy-scope-all-${providerName}`)?.checked || false
|
||||
};
|
||||
|
||||
if (!host || !port) {
|
||||
this.showProxyAlert('error', 'Please fill in at least Host and Port fields', providerName);
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyConfig = {
|
||||
host,
|
||||
port: parseInt(port),
|
||||
proxy_type: type,
|
||||
timeout: parseInt(timeout),
|
||||
verify_ssl: verifySsl,
|
||||
scope: scope
|
||||
};
|
||||
|
||||
if (user) {
|
||||
proxyConfig.auth = { username: user };
|
||||
if (pass) {
|
||||
proxyConfig.auth.password = pass;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE}/api/providers/${providerName}/proxy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(proxyConfig)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.showProxyAlert('success', `Proxy configuration saved for ${providerName}`, providerName);
|
||||
// Clear password field
|
||||
if (document.getElementById(`proxy-pass-${providerName}`)) {
|
||||
document.getElementById(`proxy-pass-${providerName}`).value = '';
|
||||
}
|
||||
// Reload proxy form to show updated status
|
||||
await this.loadProxyForms();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
this.showProxyAlert('error', result.error || `Failed to save proxy for ${providerName}`, providerName);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showProxyAlert('error', `Network error: ${error.message}`, providerName);
|
||||
console.error('Proxy save error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete proxy configuration
|
||||
async deleteProxy(providerName) {
|
||||
if (!confirm(`Delete proxy configuration for ${providerName}?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE}/api/providers/${providerName}/proxy`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.showProxyAlert('success', `Proxy configuration deleted for ${providerName}`, providerName);
|
||||
// Reload proxy form
|
||||
await this.loadProxyForms();
|
||||
} else {
|
||||
const result = await response.json();
|
||||
this.showProxyAlert('error', result.error || `Failed to delete proxy for ${providerName}`, providerName);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showProxyAlert('error', `Network error: ${error.message}`, providerName);
|
||||
console.error('Proxy delete error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Test proxy connection
|
||||
async testProxy(providerName) {
|
||||
const testResultEl = document.getElementById(`proxy-test-result-${providerName}`);
|
||||
if (testResultEl) {
|
||||
testResultEl.innerHTML = '<span class="loader"></span> Testing proxy connection...';
|
||||
testResultEl.className = 'proxy-test-result';
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to fetch a test endpoint through the provider
|
||||
const response = await fetch(`${this.API_BASE}/api/providers/${providerName}/channels?limit=1`);
|
||||
|
||||
if (response.ok) {
|
||||
if (testResultEl) {
|
||||
testResultEl.innerHTML = '<i class="fas fa-check-circle"></i> Proxy connection successful!';
|
||||
testResultEl.className = 'proxy-test-result success';
|
||||
}
|
||||
this.showProxyAlert('success', `Proxy test passed for ${providerName}`, providerName);
|
||||
} else {
|
||||
if (testResultEl) {
|
||||
testResultEl.innerHTML = `<i class="fas fa-exclamation-circle"></i> Proxy test failed (HTTP ${response.status})`;
|
||||
testResultEl.className = 'proxy-test-result error';
|
||||
}
|
||||
this.showProxyAlert('error', `Proxy test failed for ${providerName} (HTTP ${response.status})`, providerName);
|
||||
}
|
||||
} catch (error) {
|
||||
if (testResultEl) {
|
||||
testResultEl.innerHTML = `<i class="fas fa-exclamation-circle"></i> Network error: ${error.message}`;
|
||||
testResultEl.className = 'proxy-test-result error';
|
||||
}
|
||||
this.showProxyAlert('error', `Proxy test failed: ${error.message}`, providerName);
|
||||
console.error('Proxy test error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle advanced options
|
||||
toggleAdvanced(providerName) {
|
||||
const contentEl = document.getElementById(`proxy-advanced-content-${providerName}`);
|
||||
const iconEl = document.getElementById(`proxy-advanced-icon-${providerName}`);
|
||||
|
||||
if (contentEl && iconEl) {
|
||||
const isExpanded = contentEl.classList.contains('expanded');
|
||||
if (isExpanded) {
|
||||
contentEl.classList.remove('expanded');
|
||||
iconEl.classList.remove('fa-chevron-up');
|
||||
iconEl.classList.add('fa-chevron-down');
|
||||
} else {
|
||||
contentEl.classList.add('expanded');
|
||||
iconEl.classList.remove('fa-chevron-down');
|
||||
iconEl.classList.add('fa-chevron-up');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show alert for proxy operations
|
||||
showProxyAlert(type, message, providerName = null) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.innerHTML = `
|
||||
<i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
|
||||
<div>${providerName ? `<strong>${providerName}:</strong> ` : ''}${message}</div>
|
||||
`;
|
||||
|
||||
// Insert at beginning of proxy container
|
||||
const proxyContainer = document.getElementById('proxy-container');
|
||||
if (proxyContainer) {
|
||||
proxyContainer.insertBefore(alert, proxyContainer.firstChild);
|
||||
} else {
|
||||
// Fallback to card body
|
||||
const cardBody = document.querySelector('.card-body');
|
||||
if (cardBody) {
|
||||
cardBody.insertBefore(alert, cardBody.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove after 5 seconds
|
||||
setTimeout(() => alert.remove(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Make ProxyManager globally available
|
||||
window.ProxyManager = ProxyManager;
|
||||
|
||||
// Initialize proxy manager when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.proxyManager = new ProxyManager(window.location.origin);
|
||||
});
|
||||
+237
-10
@@ -126,6 +126,10 @@ class UltimateService:
|
||||
css_path = os.path.join(web_dir, 'config.css')
|
||||
js_path = os.path.join(web_dir, 'config.js')
|
||||
|
||||
# NEW: Proxy files
|
||||
proxy_css_path = os.path.join(web_dir, 'proxy.css')
|
||||
proxy_js_path = os.path.join(web_dir, 'proxy.js')
|
||||
|
||||
# EPG mapping files
|
||||
epg_css_path = os.path.join(web_dir, 'epg_mapping.css')
|
||||
epg_js_path = os.path.join(web_dir, 'epg_mapping.js')
|
||||
@@ -141,6 +145,10 @@ class UltimateService:
|
||||
with open(css_path, 'r', encoding='utf-8') as f:
|
||||
css = f.read()
|
||||
|
||||
# NEW: Load proxy CSS
|
||||
with open(proxy_css_path, 'r', encoding='utf-8') as f:
|
||||
proxy_css = f.read()
|
||||
|
||||
# Load EPG CSS
|
||||
with open(epg_css_path, 'r', encoding='utf-8') as f:
|
||||
epg_css = f.read()
|
||||
@@ -149,6 +157,10 @@ class UltimateService:
|
||||
with open(js_path, 'r', encoding='utf-8') as f:
|
||||
js = f.read()
|
||||
|
||||
# NEW: Load proxy JS
|
||||
with open(proxy_js_path, 'r', encoding='utf-8') as f:
|
||||
proxy_js = f.read()
|
||||
|
||||
# Load EPG JS and libraries
|
||||
with open(epg_js_path, 'r', encoding='utf-8') as f:
|
||||
epg_js = f.read()
|
||||
@@ -159,21 +171,23 @@ class UltimateService:
|
||||
with open(debounce_path, 'r', encoding='utf-8') as f:
|
||||
debounce_js = f.read()
|
||||
|
||||
# Combine all CSS
|
||||
combined_css = f"{css}\n\n/* EPG Mapping CSS */\n{epg_css}"
|
||||
# Combine all CSS (correct order: base -> proxy -> epg)
|
||||
combined_css = f"{css}\n\n/* Proxy CSS */\n{proxy_css}\n\n/* EPG Mapping CSS */\n{epg_css}"
|
||||
|
||||
# Combine all JS (with proper order)
|
||||
combined_js = f"""
|
||||
|
||||
/* Debounce Utility */
|
||||
{debounce_js}
|
||||
|
||||
|
||||
/* FuzzySet Library */
|
||||
{fuzzyset_js}
|
||||
|
||||
|
||||
/* Main Config JS */
|
||||
{js}
|
||||
|
||||
|
||||
/* Proxy Management JS */
|
||||
{proxy_js}
|
||||
|
||||
/* EPG Mapping JS */
|
||||
{epg_js}
|
||||
"""
|
||||
@@ -182,14 +196,12 @@ class UltimateService:
|
||||
html = html.replace('<link rel="stylesheet" href="config.css">',
|
||||
f'<style>\n{combined_css}\n</style>')
|
||||
|
||||
# Inject JavaScript before </body> tag since <script src="config.js"> was removed
|
||||
# Inject JavaScript before </body> tag
|
||||
script_tag = f'<script>\n{combined_js}\n</script>'
|
||||
|
||||
# Check if script tag exists in HTML (for backward compatibility)
|
||||
if '<script src="config.js"></script>' in html:
|
||||
html = html.replace('<script src="config.js"></script>', script_tag)
|
||||
else:
|
||||
# Script tag was removed, inject before </body>
|
||||
html = html.replace('</body>', f'{script_tag}\n</body>')
|
||||
|
||||
return html
|
||||
@@ -1793,7 +1805,222 @@ class UltimateService:
|
||||
response.status = 500
|
||||
return {'error': f'Internal server error: {str(e)}'}
|
||||
|
||||
# Add to service.py setup_routes()
|
||||
@self.app.route('/api/providers/<provider>/subscription')
|
||||
def get_provider_subscription(provider):
|
||||
"""Get subscription status for a provider"""
|
||||
try:
|
||||
subscription = self.manager.get_subscription_status(provider)
|
||||
|
||||
if subscription:
|
||||
return {
|
||||
'success': True,
|
||||
'subscription': subscription.to_dict()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': True,
|
||||
'subscription': None,
|
||||
'message': 'No subscription information available'
|
||||
}
|
||||
|
||||
except ValueError as val_err:
|
||||
response.status = 404
|
||||
return {'error': str(val_err)}
|
||||
except Exception as api_err:
|
||||
logger.error(f"API Error in subscription endpoint: {str(api_err)}")
|
||||
response.status = 500
|
||||
return {'error': f'Internal server error: {str(api_err)}'}
|
||||
|
||||
@self.app.route('/api/providers/<provider>/channels/subscribed')
|
||||
def get_subscribed_channels(provider):
|
||||
"""Get channels the user is subscribed to for a provider"""
|
||||
try:
|
||||
channels = self.manager.get_subscribed_channels(provider)
|
||||
|
||||
# Get provider instance for additional info
|
||||
provider_instance = self.manager.get_provider(provider)
|
||||
provider_catchup_hours = getattr(provider_instance, 'catchup_window', 0) if provider_instance else 0
|
||||
|
||||
channels_data = []
|
||||
for c in channels:
|
||||
channel_dict = c.to_dict()
|
||||
|
||||
# Add catchup hours
|
||||
if hasattr(c, 'catchup_hours'):
|
||||
channel_dict['CatchupHours'] = c.catchup_hours
|
||||
else:
|
||||
channel_dict['CatchupHours'] = provider_catchup_hours
|
||||
|
||||
channels_data.append(channel_dict)
|
||||
|
||||
return {
|
||||
'provider': provider,
|
||||
'channels': channels_data,
|
||||
'count': len(channels_data),
|
||||
'is_filtered': True # Indicates subscription filtering was applied
|
||||
}
|
||||
|
||||
except ValueError as val_err:
|
||||
response.status = 404
|
||||
return {'error': str(val_err)}
|
||||
except Exception as api_err:
|
||||
logger.error(f"API Error in subscribed channels endpoint: {str(api_err)}")
|
||||
response.status = 500
|
||||
return {'error': f'Internal server error: {str(api_err)}'}
|
||||
|
||||
@self.app.route('/api/providers/<provider>/packages')
|
||||
def get_provider_packages(provider):
|
||||
"""Get available subscription packages for a provider"""
|
||||
try:
|
||||
packages = self.manager.get_available_packages(provider)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'provider': provider,
|
||||
'packages': [
|
||||
{
|
||||
'package_id': pkg.package_id,
|
||||
'name': pkg.name,
|
||||
'description': pkg.description,
|
||||
'price_info': pkg.price_info,
|
||||
'channel_count': pkg.channel_count,
|
||||
'metadata': pkg.metadata
|
||||
}
|
||||
for pkg in packages
|
||||
],
|
||||
'count': len(packages)
|
||||
}
|
||||
|
||||
except ValueError as val_err:
|
||||
response.status = 404
|
||||
return {'error': str(val_err)}
|
||||
except Exception as api_err:
|
||||
logger.error(f"API Error in packages endpoint: {str(api_err)}")
|
||||
response.status = 500
|
||||
return {'error': f'Internal server error: {str(api_err)}'}
|
||||
|
||||
@self.app.route('/api/providers/channels/subscribed')
|
||||
def get_all_subscribed_channels():
|
||||
"""Get all subscribed channels across all providers"""
|
||||
try:
|
||||
all_subscribed = {}
|
||||
provider_list = self.manager.list_providers()
|
||||
|
||||
for provider in provider_list:
|
||||
try:
|
||||
channels = self.manager.get_subscribed_channels(provider)
|
||||
if channels:
|
||||
all_subscribed[provider] = [c.to_dict() for c in channels]
|
||||
except Exception as provider_err:
|
||||
logger.warning(f"Could not get subscribed channels for {provider}: {provider_err}")
|
||||
continue
|
||||
|
||||
total_channels = sum(len(channels) for channels in all_subscribed.values())
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'providers': list(all_subscribed.keys()),
|
||||
'channels_by_provider': all_subscribed,
|
||||
'total_channels': total_channels,
|
||||
'provider_count': len(all_subscribed)
|
||||
}
|
||||
|
||||
except Exception as api_err:
|
||||
logger.error(f"API Error in all subscribed channels endpoint: {str(api_err)}")
|
||||
response.status = 500
|
||||
return {'error': f'Internal server error: {str(api_err)}'}
|
||||
|
||||
@self.app.route('/api/m3u/subscribed')
|
||||
def get_m3u_subscribed():
|
||||
"""
|
||||
Generate M3U playlist with only subscribed channels.
|
||||
|
||||
Note: This uses the same caching mechanism as regular M3U,
|
||||
but with '_subscribed' suffix in cache filename.
|
||||
"""
|
||||
try:
|
||||
cache_file = "playlist_subscribed.m3u"
|
||||
|
||||
# Try cached version
|
||||
cached_content = self.vfs.read_text(cache_file)
|
||||
if cached_content:
|
||||
logger.info("Serving cached subscribed M3U playlist")
|
||||
response.content_type = 'audio/x-mpegurl; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename="playlist_subscribed.m3u8"'
|
||||
return cached_content
|
||||
|
||||
# Generate new M3U with subscribed channels
|
||||
base_url = f"{request.urlparts.scheme}://{request.urlparts.netloc}"
|
||||
m3u_content = "#EXTM3U\n"
|
||||
|
||||
provider_list = self.manager.list_providers()
|
||||
for provider_name in provider_list:
|
||||
try:
|
||||
channels = self.manager.get_subscribed_channels(provider_name)
|
||||
|
||||
for channel in channels:
|
||||
# Generate M3U entry for each subscribed channel
|
||||
channel_id = channel.channel_id
|
||||
channel_name = channel.name
|
||||
channel_logo = channel.logo_url or ''
|
||||
|
||||
# Get provider label
|
||||
try:
|
||||
provider_instance = self.manager.get_provider(provider_name)
|
||||
provider_label = getattr(provider_instance, 'provider_label', provider_name)
|
||||
except:
|
||||
provider_label = provider_name
|
||||
|
||||
# Build stream URL
|
||||
stream_url = f"{base_url}/api/providers/{provider_name}/channels/{channel_id}/stream"
|
||||
|
||||
# Add M3U entry
|
||||
m3u_content += f'#EXTINF:-1 tvg-id="{channel_id}" tvg-logo="{channel_logo}" group-title="{provider_label}",{channel_name}\n'
|
||||
|
||||
# Add DRM directives if available
|
||||
try:
|
||||
drm_configs = self.manager.get_channel_drm_configs(provider_name, channel_id)
|
||||
if drm_configs:
|
||||
drm_directives = self._generate_drm_directives(drm_configs)
|
||||
m3u_content += drm_directives
|
||||
except Exception as drm_err:
|
||||
logger.debug(f"Could not get DRM for {provider_name}/{channel_id}: {drm_err}")
|
||||
|
||||
m3u_content += f'{stream_url}\n'
|
||||
|
||||
except Exception as provider_err:
|
||||
logger.warning(
|
||||
f"Failed to process subscribed channels for '{provider_name}': {str(provider_err)}")
|
||||
continue
|
||||
|
||||
# Cache the result
|
||||
if self.vfs.write_text(cache_file, m3u_content):
|
||||
logger.info(f"Subscribed M3U playlist cached to {cache_file}")
|
||||
|
||||
response.content_type = 'audio/x-mpegurl; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename="playlist_subscribed.m3u8"'
|
||||
return m3u_content
|
||||
|
||||
except Exception as api_err:
|
||||
logger.error(f"API Error in /api/m3u/subscribed: {str(api_err)}")
|
||||
response.status = 500
|
||||
return {'error': f'Internal server error: {str(api_err)}'}
|
||||
|
||||
@self.app.route('/api/m3u/subscribed/generate')
|
||||
def generate_m3u_subscribed():
|
||||
"""Force regenerate subscribed M3U playlist"""
|
||||
try:
|
||||
# Clear cache and regenerate
|
||||
cache_file = "playlist_subscribed.m3u"
|
||||
self.vfs.delete(cache_file) # Delete if exists
|
||||
|
||||
# Call the subscribed M3U endpoint which will regenerate
|
||||
return get_m3u_subscribed()
|
||||
|
||||
except Exception as api_err:
|
||||
logger.error(f"API Error in /api/m3u/subscribed/generate: {str(api_err)}")
|
||||
response.status = 500
|
||||
return {'error': f'Internal server error: {str(api_err)}'}
|
||||
|
||||
@self.app.route('/api/providers/enabled', method='GET')
|
||||
def get_all_enabled_status():
|
||||
|
||||
Reference in New Issue
Block a user