diff --git a/lib/streaming_providers/base/provider.py b/lib/streaming_providers/base/provider.py
index e76d0e4..219dc37 100644
--- a/lib/streaming_providers/base/provider.py
+++ b/lib/streaming_providers/base/provider.py
@@ -446,6 +446,16 @@ class StreamingProvider(ABC):
"""
return self.catchup_window > 0
+ @property
+ def requires_user_credentials(self) -> bool:
+ """
+ Some providers do not need to authenticate
+
+ Returns:
+ bool: True if user credentials are required
+ """
+ return True
+
def get_epg(self, channel_id: str,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
diff --git a/lib/streaming_providers/providers/magenta2/provider.py b/lib/streaming_providers/providers/magenta2/provider.py
index 879b173..b463dce 100644
--- a/lib/streaming_providers/providers/magenta2/provider.py
+++ b/lib/streaming_providers/providers/magenta2/provider.py
@@ -405,6 +405,10 @@ class Magenta2Provider(StreamingProvider):
# This provider offers 4 hours of catchup
return 4
+ @property
+ def requires_user_credentials(self) -> bool:
+ return False
+
def get_discovery_status(self) -> Dict[str, Any]:
"""Get discovery and configuration status"""
if not self.discovery_service:
diff --git a/resources/web/config.html b/resources/web/config.html
index 9226fcf..4c64845 100644
--- a/resources/web/config.html
+++ b/resources/web/config.html
@@ -142,6 +142,7 @@
padding: 20px;
border: 1px solid #dee2e6;
transition: var(--transition);
+ position: relative;
}
.provider-card:hover {
@@ -160,10 +161,11 @@
width: 60px;
height: 60px;
border-radius: 50%;
- object-fit: cover;
+ object-fit: contain;
background: white;
- padding: 5px;
+ padding: 8px;
border: 2px solid var(--primary-color);
+ box-sizing: content-box;
}
.provider-info h3 {
@@ -214,6 +216,7 @@
align-items: center;
justify-content: center;
gap: 8px;
+ margin-top: 5px;
}
.btn-primary {
@@ -255,6 +258,7 @@
display: flex;
gap: 10px;
margin-top: 20px;
+ flex-wrap: wrap;
}
.status-indicator {
@@ -386,6 +390,27 @@
opacity: 1;
}
+ /* Special styling for provider types */
+ .client-credentials-only .form-group:has(label:contains("Username")),
+ .client-credentials-only .form-group:has(label:contains("Password")),
+ .client-credentials-only .form-group:has(label:contains("Client ID")),
+ .client-credentials-only .form-group:has(label:contains("Client Secret")) {
+ display: none;
+ }
+
+ .client-credentials-only .no-credentials-required {
+ display: block;
+ padding: 15px;
+ background: rgba(76, 201, 240, 0.1);
+ border-radius: var(--border-radius);
+ border-left: 4px solid var(--success-color);
+ margin-bottom: 20px;
+ }
+
+ .no-credentials-required {
+ display: none;
+ }
+
@media (max-width: 768px) {
.providers-grid {
grid-template-columns: 1fr;
@@ -402,6 +427,14 @@
.tab {
text-align: center;
}
+
+ .btn-group {
+ flex-direction: column;
+ }
+
+ .btn {
+ width: 100%;
+ }
}
@@ -444,7 +477,7 @@
+ credentialsContainer.innerHTML = providers.map(provider => {
+ // Check if this provider requires user credentials
+ const isClientOnly = !provider.requires_user_credentials;
+ const providerCardClass = isClientOnly ? 'provider-card client-credentials-only' : 'provider-card';
+
+ return `
+
+ ${isClientOnly ? `
+
+
+
No manual credentials required
+
This provider uses client credentials that are hardcoded in the application.
+
+ ` : ''}
+
-
-
-
-
-
-
-
-
-
-
- `).join('');
+ `;
+ }).join('');
}
// Load proxy forms
@@ -838,16 +863,24 @@
// API Functions
async function saveCredentials(providerName) {
+ // Check if provider is client-only
+ if (!provider.requires_user_credentials) {
+ showAlert('info', `${providerName} uses hardcoded client credentials - no manual setup needed`);
+ return;
+ }
+
const username = document.getElementById(`username-${providerName}`).value;
const password = document.getElementById(`password-${providerName}`).value;
- const clientId = document.getElementById(`client-id-${providerName}`).value;
- const clientSecret = document.getElementById(`client-secret-${providerName}`).value;
- const credentials = {};
- if (username) credentials.username = username;
- if (password) credentials.password = password;
- if (clientId) credentials.client_id = clientId;
- if (clientSecret) credentials.client_secret = clientSecret;
+ if (!username || !password) {
+ showAlert('error', 'Please enter both username and password');
+ return;
+ }
+
+ const credentials = {
+ username: username,
+ password: password
+ };
const statusEl = document.getElementById(`status-${providerName}`);
statusEl.innerHTML = '
Saving...';
@@ -867,7 +900,6 @@
// Clear password field
document.getElementById(`password-${providerName}`).value = '';
- document.getElementById(`client-secret-${providerName}`).value = '';
// Reload auth status
await loadAuthStatus();
@@ -889,6 +921,12 @@
}
async function deleteCredentials(providerName) {
+ // Check if provider is client-only
+ if (!provider.requires_user_credentials) {
+ showAlert('info', `${providerName} uses hardcoded client credentials - nothing to delete`);
+ return;
+ }
+
if (!confirm(`Delete credentials for ${providerName}?`)) return;
const statusEl = document.getElementById(`status-${providerName}`);
@@ -906,8 +944,6 @@
// Clear fields
document.getElementById(`username-${providerName}`).value = '';
document.getElementById(`password-${providerName}`).value = '';
- document.getElementById(`client-id-${providerName}`).value = '';
- document.getElementById(`client-secret-${providerName}`).value = '';
// Reload auth status
await loadAuthStatus();
@@ -937,14 +973,14 @@
const result = await response.json();
if (response.ok) {
- if (result.is_authenticated) {
+ if (result.is_ready) {
statusEl.className = 'status-indicator status-success';
- statusEl.innerHTML = '
Authentication successful';
- showAlert('success', `Authentication successful for ${providerName}`);
+ statusEl.innerHTML = '
Ready to use';
+ showAlert('success', `${providerName} is ready to use`);
} else {
statusEl.className = 'status-indicator status-warning';
statusEl.innerHTML = `
${result.auth_state}`;
- showAlert('warning', `Authentication status: ${result.auth_state}`);
+ showAlert('warning', `${providerName} status: ${result.auth_state}`);
}
} else {
statusEl.className = 'status-indicator status-error';
@@ -1179,11 +1215,18 @@
}
try {
+ // Get all providers first
+ const response = await fetch(`${API_BASE}/api/providers`);
+ const data = await response.json();
+
// Delete credentials for all providers
- for (const provider of providers) {
- await fetch(`${API_BASE}/api/providers/${provider.name}/credentials`, {
- method: 'DELETE'
- });
+ for (const provider of data.providers) {
+ // Skip client-only providers
+ if (provider.requires_user_credentials) {
+ await fetch(`${API_BASE}/api/providers/${provider.name}/credentials`, {
+ method: 'DELETE'
+ });
+ }
await fetch(`${API_BASE}/api/providers/${provider.name}/proxy`, {
method: 'DELETE'
diff --git a/service.py b/service.py
index ccdde7b..eaadf72 100644
--- a/service.py
+++ b/service.py
@@ -541,6 +541,23 @@ class UltimateService:
response.content_type = 'application/json'
return json.dumps({'error': f'Failed to fetch manifest: {str(fetch_err)}'})
+ @staticmethod
+ def _get_settings_manager():
+ """Simple helper to get SettingsManager"""
+ try:
+ from streaming_providers.base.settings.settings_manager import SettingsManager
+ return SettingsManager()
+ except ImportError:
+ # Try alternative path
+ try:
+ from base.settings.settings_manager import SettingsManager
+ return SettingsManager()
+ except ImportError as e:
+ logger.error(f"Cannot import SettingsManager: {e}")
+ # Re-raise with a clearer message
+ raise ImportError(
+ f"SettingsManager not available. Ensure streaming_providers module is installed. Error: {e}")
+
def setup_routes(self):
@self.app.route('/api/providers')
def list_providers():
@@ -556,12 +573,18 @@ class UltimateService:
provider_label = getattr(provider_instance, 'provider_label', provider_name)
country = getattr(provider_instance, 'country', default_country)
provider_logo = getattr(provider_instance, 'provider_logo', '')
+ requires_user_credentials = getattr(
+ provider_instance,
+ 'requires_user_credentials',
+ True # Default to True if property doesn't exist (for backward compatibility)
+ )
providers_details.append({
'name': provider_name,
'label': provider_label,
'logo': provider_logo,
- 'country': country
+ 'country': country,
+ 'requires_user_credentials': requires_user_credentials
})
return {
@@ -1201,7 +1224,8 @@ class UltimateService:
Example: GET /api/providers/joyn_de/auth/status
"""
try:
- status = self.manager.settings_manager.get_auth_status(provider)
+ settings_manager = self._get_settings_manager()
+ status = settings_manager.get_auth_status(provider)
response.content_type = 'application/json; charset=utf-8'
return status
@@ -1217,8 +1241,7 @@ class UltimateService:
Save credentials for a provider via API
Accepts JSON body with credentials:
- - User/password: {"username": "...", "password": "...", "client_id": "..." (optional)}
- - Client credentials: {"client_id": "...", "client_secret": "..."}
+ - User/password: {"username": "...", "password": "..."}
Example: POST /api/providers/joyn_de/credentials
Body: {"username": "user@example.com", "password": "secret123"}
@@ -1241,8 +1264,11 @@ class UltimateService:
response.status = 400
return {'error': 'Credentials data must be a JSON object'}
+ # Get settings manager
+ settings_manager = self._get_settings_manager()
+
# Save credentials
- success, message = self.manager.settings_manager.save_provider_credentials_from_api(
+ success, message = settings_manager.save_provider_credentials_from_api(
provider, credentials_data
)
@@ -1278,7 +1304,8 @@ class UltimateService:
Example: DELETE /api/providers/joyn_de/credentials
"""
try:
- success, message = self.manager.settings_manager.delete_provider_credentials_from_api(provider)
+ settings_manager = self._get_settings_manager()
+ success, message = settings_manager.delete_provider_credentials_from_api(provider)
if success:
response.status = 200
@@ -1345,8 +1372,8 @@ class UltimateService:
response.status = 400
return {'error': 'Proxy data must be a JSON object'}
- # Save proxy configuration
- success, message = self.manager.settings_manager.save_provider_proxy_from_api(
+ settings_manager = self._get_settings_manager()
+ success, message = settings_manager.save_provider_proxy_from_api(
provider, proxy_data
)
@@ -1382,7 +1409,8 @@ class UltimateService:
Example: DELETE /api/providers/joyn_de/proxy
"""
try:
- success, message = self.manager.settings_manager.delete_provider_proxy_from_api(provider)
+ settings_manager = self._get_settings_manager()
+ success, message = settings_manager.delete_provider_proxy_from_api(provider)
if success:
response.status = 200