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 @@
Credentials Setup -

Enter your login credentials for each streaming provider. Some providers may also require client ID/secret for API access.

+

Enter your login credentials for each streaming provider. Some providers like magenta2 don't require manual credentials.

@@ -653,8 +686,13 @@ return; } - credentialsContainer.innerHTML = providers.map(provider => ` -
+ 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 ` +
${provider.logo ? `` : ''}
@@ -667,6 +705,14 @@
+ ${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