mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-17 14:42:18 +02:00
800 lines
30 KiB
Python
800 lines
30 KiB
Python
# streaming_providers/base/manager.py
|
|
"""
|
|
Main ProviderManager as a facade coordinating all operations.
|
|
Maintains backward compatibility while delegating to specialized classes.
|
|
"""
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from .catchup_operations import CatchupOperations
|
|
from .channel_operations import ChannelOperations
|
|
from .drm_operations import DRMOperations
|
|
from .epg_operations import EPGOperations
|
|
from .event_operations import EventOperations
|
|
from .vod_operations import VodOperations
|
|
from .recording_operations import RecordingOperations
|
|
from .timer_operations import TimerOperations
|
|
from .bookmark_operations import BookmarkOperations
|
|
from .favorite_operations import FavoriteOperations
|
|
from .models import StreamingChannel
|
|
from .models.bookmark import Bookmark, ContentType
|
|
from .models.favorite import FavoriteType, Favorite
|
|
from .provider_registry import ProviderRegistry
|
|
from .subscription_operations import SubscriptionOperations
|
|
from .utils.logger import logger
|
|
|
|
|
|
class ProviderManager:
|
|
"""
|
|
Facade coordinating all provider-related operations.
|
|
Maintains backward compatibility while delegating to specialized classes.
|
|
"""
|
|
|
|
def __init__(self):
|
|
# Core components
|
|
self.registry = ProviderRegistry()
|
|
|
|
# Specialized operations
|
|
self.channel_ops = ChannelOperations(self.registry)
|
|
self.epg_ops = EPGOperations(self.registry)
|
|
self.drm_ops = DRMOperations(self.registry)
|
|
self.catchup_ops = CatchupOperations(self.registry, self.drm_ops)
|
|
self.subscription_ops = SubscriptionOperations(self.registry)
|
|
self.event_ops = EventOperations(self.registry)
|
|
self.vod_ops = VodOperations(self.registry)
|
|
self.recording_ops = RecordingOperations(self.registry)
|
|
self.timer_ops = TimerOperations(self.registry)
|
|
self.bookmark_ops = BookmarkOperations(self.registry)
|
|
self.favorite_ops = FavoriteOperations(self.registry)
|
|
|
|
# Backward compatibility - expose managers directly
|
|
self.drm_plugin_manager = self.drm_ops.drm_plugin_manager
|
|
self.epg_manager = self.epg_ops.epg_manager
|
|
|
|
# Legacy compatibility - expose providers dict
|
|
self.providers = self.registry.providers
|
|
self.provider_metadata = self.registry.provider_metadata
|
|
|
|
logger.info("ProviderManager: Initialized with modular architecture")
|
|
|
|
# ==========================================================================
|
|
# REGISTRY OPERATIONS (delegate to ProviderRegistry)
|
|
# ==========================================================================
|
|
|
|
def discover_all_providers(self, default_country: str = "DE") -> List[str]:
|
|
return self.registry.discover_all_providers(default_country)
|
|
|
|
def discover_providers(self, country: str = "DE", detected_providers: Dict = None) -> List[str]:
|
|
"""Legacy method for backward compatibility."""
|
|
if not self.registry.provider_metadata:
|
|
self.registry.discover_all_providers(country)
|
|
return self.registry.get_enabled_providers()
|
|
|
|
def rediscover_providers(self, country: str = "DE") -> List[str]:
|
|
"""
|
|
Re-scan for new providers, particularly useful for dynamically added M3U playlists.
|
|
|
|
Args:
|
|
country: Country code for provider discovery
|
|
|
|
Returns:
|
|
List of all discovered provider names
|
|
"""
|
|
logger.info(f"ProviderManager: Re-discovering providers for country '{country}'...")
|
|
discovered = self.registry.discover_all_providers(country)
|
|
logger.info(f"ProviderManager: Re-discovery complete. Found {len(discovered)} providers")
|
|
return discovered
|
|
|
|
def get_provider(self, provider_name: str):
|
|
return self.registry.get_provider(provider_name)
|
|
|
|
def set_provider_enabled(self, provider_name: str, enabled: bool) -> bool:
|
|
return self.registry.set_provider_enabled(provider_name, enabled)
|
|
|
|
def get_all_providers_metadata(self) -> List[Dict[str, Any]]:
|
|
return self.registry.get_all_providers_metadata()
|
|
|
|
def reinitialize_provider(self, provider_name: str) -> bool:
|
|
return self.registry.reinitialize_provider(provider_name)
|
|
|
|
def reinitialize_providers(self, provider_names: List[str]) -> Dict[str, bool]:
|
|
return {name: self.registry.reinitialize_provider(name) for name in provider_names}
|
|
|
|
def reinitialize_all_providers(self) -> Dict[str, bool]:
|
|
enabled = self.registry.get_enabled_providers()
|
|
return self.reinitialize_providers(enabled)
|
|
|
|
def list_providers(self) -> List[str]:
|
|
return self.registry.list_providers()
|
|
|
|
def list_all_providers(self) -> List[str]:
|
|
return self.registry.list_all_providers()
|
|
|
|
def clear_providers(self):
|
|
self.registry.clear_providers()
|
|
|
|
@staticmethod
|
|
def get_provider_class(provider_name: str):
|
|
"""Get provider class from AVAILABLE_PROVIDERS registry."""
|
|
from streaming_providers import AVAILABLE_PROVIDERS
|
|
|
|
# Remove country suffix if present
|
|
base_name = provider_name
|
|
if "_" in provider_name:
|
|
name_parts = provider_name.rsplit("_", 1)
|
|
if len(name_parts[1]) in (2, 3) and name_parts[1].isalpha():
|
|
base_name = name_parts[0]
|
|
|
|
return AVAILABLE_PROVIDERS.get(base_name)
|
|
|
|
# ==========================================================================
|
|
# CHANNEL OPERATIONS (delegate to ChannelOperations)
|
|
# ==========================================================================
|
|
|
|
def get_channels(
|
|
self, provider_name: str, fetch_manifests: bool = False, **kwargs
|
|
) -> List[StreamingChannel]:
|
|
return self.channel_ops.get_channels(provider_name, fetch_manifests, **kwargs)
|
|
|
|
def get_channel_manifest(self, provider_name: str, channel_id: str, **kwargs) -> Optional[str]:
|
|
return self.channel_ops.get_channel_manifest(provider_name, channel_id, **kwargs)
|
|
|
|
def get_all_channels(
|
|
self, fetch_manifests: bool = True, **kwargs
|
|
) -> Dict[str, List[StreamingChannel]]:
|
|
return self.channel_ops.get_all_channels(fetch_manifests, **kwargs)
|
|
|
|
# ==========================================================================
|
|
# EPG OPERATIONS (delegate to EPGOperations)
|
|
# ==========================================================================
|
|
|
|
def get_channel_epg(self, provider_name: str, channel_id: str, **kwargs) -> List[Dict]:
|
|
return self.epg_ops.get_channel_epg(provider_name, channel_id, **kwargs)
|
|
|
|
def get_provider_epg_xmltv(self, provider_name: str, **kwargs) -> Optional[str]:
|
|
return self.epg_ops.get_provider_epg_xmltv(provider_name, **kwargs)
|
|
|
|
def clear_epg_cache(self) -> bool:
|
|
return self.epg_ops.clear_epg_cache()
|
|
|
|
def reload_epg_mapping(self) -> bool:
|
|
return self.epg_ops.reload_epg_mapping()
|
|
|
|
def get_epg_cache_info(self) -> Optional[Dict]:
|
|
return self.epg_ops.get_epg_cache_info()
|
|
|
|
def get_epg_mapping_stats(self) -> Dict:
|
|
return self.epg_ops.get_epg_mapping_stats()
|
|
|
|
def has_epg_mapping(self, provider_name: str, channel_id: str) -> bool:
|
|
return self.epg_ops.has_epg_mapping(provider_name, channel_id)
|
|
|
|
# ==========================================================================
|
|
# DRM OPERATIONS (delegate to DRMOperations)
|
|
# ==========================================================================
|
|
|
|
def get_channel_drm_configs(self, provider_name: str, channel_id: str, **kwargs) -> List:
|
|
return self.drm_ops.get_content_drm_configs(provider_name, channel_id, **kwargs)
|
|
|
|
def list_drm_plugins(self) -> Dict:
|
|
return self.drm_ops.list_drm_plugins()
|
|
|
|
def clear_drm_plugins(self):
|
|
self.drm_ops.clear_drm_plugins()
|
|
|
|
# ==========================================================================
|
|
# CATCHUP OPERATIONS (delegate to CatchupOperations)
|
|
# ==========================================================================
|
|
|
|
def get_catchup_manifest(
|
|
self,
|
|
provider_name: str,
|
|
channel_id: str,
|
|
start_time: int,
|
|
end_time: int,
|
|
epg_id: Optional[str] = None,
|
|
country: Optional[str] = None,
|
|
drm_variant = "auto",
|
|
) -> Optional[str]:
|
|
return self.catchup_ops.get_catchup_manifest(
|
|
provider_name, channel_id, start_time, end_time, epg_id, country, drm_variant
|
|
)
|
|
|
|
def get_catchup_drm_configs(
|
|
self,
|
|
provider_name: str,
|
|
channel_id: str,
|
|
start_time: int,
|
|
end_time: int,
|
|
epg_id: Optional[str] = None,
|
|
country: Optional[str] = None,
|
|
drm_variant: Optional[str] = None,
|
|
) -> List:
|
|
return self.catchup_ops.get_catchup_drm_configs(
|
|
provider_name, channel_id, start_time, end_time, epg_id, country, drm_variant=drm_variant
|
|
)
|
|
|
|
def get_catchup_window(self, provider_name: str, channel_id: Optional[str] = None) -> int:
|
|
return self.catchup_ops.get_catchup_window(provider_name, channel_id)
|
|
|
|
def supports_catchup(self, provider_name: str) -> bool:
|
|
return self.catchup_ops.supports_catchup(provider_name)
|
|
|
|
def get_all_catchup_capabilities(self) -> Dict[str, Dict]:
|
|
return self.catchup_ops.get_all_catchup_capabilities()
|
|
|
|
# ==========================================================================
|
|
# EVENT OPERATIONS (delegate to EventOperations)
|
|
# ==========================================================================
|
|
|
|
def get_events(
|
|
self,
|
|
provider_name: str,
|
|
start_time=None,
|
|
end_time=None,
|
|
):
|
|
return self.event_ops.get_events(provider_name, start_time, end_time)
|
|
|
|
def get_all_events(self, start_time=None, end_time=None):
|
|
return self.event_ops.get_all_events(start_time, end_time)
|
|
|
|
def get_event_manifest(self, provider_name: str, event_id: str, **kwargs) -> Optional[str]:
|
|
return self.event_ops.get_event_manifest(provider_name, event_id, **kwargs)
|
|
|
|
def get_event_drm_configs(self, provider_name: str, event_id: str, **kwargs) -> List:
|
|
return self.drm_ops.get_content_drm_configs(provider_name, event_id, **kwargs)
|
|
|
|
# ==========================================================================
|
|
# VOD OPERATIONS (delegate to VodOperations)
|
|
# ==========================================================================
|
|
|
|
def get_vod_node(
|
|
self,
|
|
provider_name: str,
|
|
content_id: str = "",
|
|
cursor: Optional[str] = None,
|
|
page_size: int = 24,
|
|
**kwargs,
|
|
) -> dict:
|
|
return self.vod_ops.get_vod_node(
|
|
provider_name,
|
|
content_id=content_id,
|
|
cursor=cursor,
|
|
page_size=page_size,
|
|
**kwargs,
|
|
)
|
|
|
|
def get_vod_manifest(self, provider_name: str, vod_id: str, **kwargs):
|
|
return self.vod_ops.get_vod_manifest(provider_name, vod_id, **kwargs)
|
|
|
|
def get_vod_drm_configs(self, provider_name: str, vod_id: str, **kwargs) -> list:
|
|
return self.drm_ops.get_content_drm_configs(provider_name, vod_id, **kwargs)
|
|
|
|
def get_all_vod_roots(self) -> dict:
|
|
return self.vod_ops.get_all_vod_roots()
|
|
|
|
def search_vod(
|
|
self,
|
|
provider_name: str,
|
|
query: str,
|
|
cursor: Optional[str] = None,
|
|
page_size: int = 24,
|
|
**kwargs,
|
|
) -> dict:
|
|
"""Search VOD catalogue of a single provider."""
|
|
return self.vod_ops.search_vod(
|
|
provider_name,
|
|
query=query,
|
|
cursor=cursor,
|
|
page_size=page_size,
|
|
**kwargs,
|
|
)
|
|
|
|
# Keep this for searching across all providers
|
|
def search_all_vod(self, query: str, **kwargs) -> dict:
|
|
"""Search VOD across all providers."""
|
|
return self.vod_ops.search_all_vod(query, **kwargs)
|
|
|
|
# ==========================================================================
|
|
# RECORDING OPERATIONS (delegate to RecordingOperations)
|
|
# ==========================================================================
|
|
|
|
def get_recordings(
|
|
self,
|
|
provider_name: str,
|
|
include_deleted: bool = False,
|
|
):
|
|
return self.recording_ops.get_recordings(
|
|
provider_name, include_deleted=include_deleted
|
|
)
|
|
|
|
def get_all_recordings(self, include_deleted: bool = False):
|
|
return self.recording_ops.get_all_recordings(
|
|
include_deleted=include_deleted
|
|
)
|
|
|
|
def get_recording_manifest(
|
|
self, provider_name: str, recording_id: str, **kwargs
|
|
) -> Optional[str]:
|
|
return self.recording_ops.get_recording_manifest(
|
|
provider_name, recording_id, **kwargs
|
|
)
|
|
|
|
def get_recording_drm_configs(
|
|
self, provider_name: str, recording_id: str, **kwargs
|
|
) -> List:
|
|
# Reuses the same DRM resolution path as channels, events, and VOD.
|
|
return self.drm_ops.get_content_drm_configs(
|
|
provider_name, recording_id, **kwargs
|
|
)
|
|
|
|
def delete_recording(
|
|
self, provider_name: str, recording_id: str, **kwargs
|
|
) -> None:
|
|
return self.recording_ops.delete_recording(
|
|
provider_name, recording_id, **kwargs
|
|
)
|
|
|
|
# ==========================================================================
|
|
# TIMER OPERATIONS (delegate to TimerOperations)
|
|
# ==========================================================================
|
|
|
|
def get_timer_types(self, provider_name: str) -> List:
|
|
return self.timer_ops.get_timer_types(provider_name)
|
|
|
|
def get_all_timer_types(self) -> Dict[str, List]:
|
|
return self.timer_ops.get_all_timer_types()
|
|
|
|
def get_timers(
|
|
self,
|
|
provider_name: str,
|
|
include_inactive: bool = False,
|
|
) -> List:
|
|
return self.timer_ops.get_timers(
|
|
provider_name, include_inactive=include_inactive
|
|
)
|
|
|
|
def get_all_timers(self, include_inactive: bool = False) -> Dict[str, List]:
|
|
return self.timer_ops.get_all_timers(include_inactive=include_inactive)
|
|
|
|
def get_timer(self, provider_name: str, client_index: int):
|
|
return self.timer_ops.get_timer(provider_name, client_index)
|
|
|
|
def add_timer(self, provider_name: str, timer, **kwargs):
|
|
return self.timer_ops.add_timer(provider_name, timer, **kwargs)
|
|
|
|
def update_timer(self, provider_name: str, timer, **kwargs):
|
|
return self.timer_ops.update_timer(provider_name, timer, **kwargs)
|
|
|
|
def delete_timer(
|
|
self,
|
|
provider_name: str,
|
|
client_index: int,
|
|
force_delete: bool = False,
|
|
**kwargs,
|
|
) -> None:
|
|
return self.timer_ops.delete_timer(
|
|
provider_name, client_index, force_delete=force_delete, **kwargs
|
|
)
|
|
|
|
# ==========================================================================
|
|
# BOOKMARK OPERATIONS (delegate to BookmarkOperations)
|
|
# ==========================================================================
|
|
|
|
def get_bookmarks(
|
|
self,
|
|
provider_name: str,
|
|
content_type: Optional[ContentType] = None,
|
|
include_completed: bool = False,
|
|
include_stale: bool = False,
|
|
max_age_hours: int = 720,
|
|
) -> List[Bookmark]:
|
|
"""
|
|
Get bookmarks from a specific provider.
|
|
|
|
Args:
|
|
provider_name: Name of the provider to query.
|
|
content_type: Optional filter by content type.
|
|
include_completed: If True, include completed bookmarks.
|
|
include_stale: If True, include stale bookmarks.
|
|
max_age_hours: Maximum age before a bookmark is considered stale.
|
|
|
|
Returns:
|
|
List of Bookmark objects.
|
|
|
|
Raises:
|
|
ValueError: If the provider is not found or disabled.
|
|
"""
|
|
return self.bookmark_ops.get_bookmarks(
|
|
provider_name,
|
|
content_type=content_type,
|
|
include_completed=include_completed,
|
|
include_stale=include_stale,
|
|
max_age_hours=max_age_hours,
|
|
)
|
|
|
|
def get_bookmark(
|
|
self, provider_name: str, content_id: str
|
|
) -> Optional[Bookmark]:
|
|
"""
|
|
Get a specific bookmark by content ID from a provider.
|
|
|
|
Args:
|
|
provider_name: Name of the provider.
|
|
content_id: Content identifier.
|
|
|
|
Returns:
|
|
Bookmark object if found, None otherwise.
|
|
|
|
Raises:
|
|
ValueError: If the provider is not found or disabled.
|
|
"""
|
|
return self.bookmark_ops.get_bookmark(provider_name, content_id)
|
|
|
|
def update_bookmark(
|
|
self,
|
|
provider_name: str,
|
|
content_id: str,
|
|
content_type: ContentType,
|
|
position_seconds: int,
|
|
duration_seconds: Optional[int] = None,
|
|
title: Optional[str] = None,
|
|
**kwargs,
|
|
) -> Optional[Bookmark]:
|
|
"""
|
|
Update or create a bookmark for a specific content.
|
|
|
|
Args:
|
|
provider_name: Name of the provider.
|
|
content_id: Content identifier.
|
|
content_type: Type of content being bookmarked.
|
|
position_seconds: Playback position in seconds (0 = start, -1 = completed).
|
|
duration_seconds: Total duration of the content (optional).
|
|
title: Content title for caching (optional).
|
|
**kwargs: Additional metadata (thumbnail_url, series_title, etc.).
|
|
|
|
Returns:
|
|
Updated Bookmark object, or None if provider doesn't support bookmarks.
|
|
|
|
Raises:
|
|
ValueError: If the provider is not found or disabled.
|
|
RuntimeError: If the provider rejects the update.
|
|
"""
|
|
return self.bookmark_ops.update_bookmark(
|
|
provider_name,
|
|
content_id,
|
|
content_type,
|
|
position_seconds,
|
|
duration_seconds=duration_seconds,
|
|
title=title,
|
|
**kwargs,
|
|
)
|
|
|
|
def delete_bookmark(
|
|
self, provider_name: str, content_id: str
|
|
) -> bool:
|
|
"""
|
|
Delete a bookmark from a specific provider.
|
|
|
|
Args:
|
|
provider_name: Name of the provider.
|
|
content_id: Content identifier.
|
|
|
|
Returns:
|
|
True if deleted, False if bookmark didn't exist or provider doesn't support.
|
|
|
|
Raises:
|
|
ValueError: If the provider is not found or disabled.
|
|
RuntimeError: If the provider refuses deletion.
|
|
"""
|
|
return self.bookmark_ops.delete_bookmark(provider_name, content_id)
|
|
|
|
def mark_bookmark_completed(
|
|
self,
|
|
provider_name: str,
|
|
content_id: str,
|
|
content_type: ContentType,
|
|
duration_seconds: Optional[int] = None,
|
|
title: Optional[str] = None,
|
|
**kwargs,
|
|
) -> Optional[Bookmark]:
|
|
"""
|
|
Mark a content as completed (watched to end).
|
|
|
|
Args:
|
|
provider_name: Name of the provider.
|
|
content_id: Content identifier.
|
|
content_type: Type of content.
|
|
duration_seconds: Total duration (optional).
|
|
title: Content title (optional).
|
|
**kwargs: Additional metadata.
|
|
|
|
Returns:
|
|
Updated Bookmark with position set to -1, or None if not supported.
|
|
|
|
Raises:
|
|
ValueError: If the provider is not found or disabled.
|
|
"""
|
|
return self.bookmark_ops.mark_completed(
|
|
provider_name,
|
|
content_id,
|
|
content_type,
|
|
duration_seconds=duration_seconds,
|
|
title=title,
|
|
**kwargs,
|
|
)
|
|
|
|
def get_all_bookmarks(
|
|
self,
|
|
content_type: Optional[ContentType] = None,
|
|
include_completed: bool = False,
|
|
include_stale: bool = False,
|
|
max_age_hours: int = 720,
|
|
) -> Dict[str, List[Bookmark]]:
|
|
"""
|
|
Get bookmarks from all enabled providers.
|
|
|
|
Args:
|
|
content_type: Optional filter by content type.
|
|
include_completed: If True, include completed bookmarks.
|
|
include_stale: If True, include stale bookmarks.
|
|
max_age_hours: Maximum age for stale detection.
|
|
|
|
Returns:
|
|
Dict mapping provider name → list of Bookmark objects.
|
|
Failed providers are represented by an '_errors' key.
|
|
"""
|
|
return self.bookmark_ops.get_all_bookmarks(
|
|
content_type=content_type,
|
|
include_completed=include_completed,
|
|
include_stale=include_stale,
|
|
max_age_hours=max_age_hours,
|
|
)
|
|
|
|
def get_all_bookmarks_flat(
|
|
self,
|
|
content_type: Optional[ContentType] = None,
|
|
include_completed: bool = False,
|
|
include_stale: bool = False,
|
|
max_age_hours: int = 720,
|
|
sort_by: str = "last_updated",
|
|
) -> List[Bookmark]:
|
|
"""
|
|
Get all bookmarks as a flat list sorted by the specified field.
|
|
|
|
Args:
|
|
content_type: Optional filter by content type.
|
|
include_completed: If True, include completed bookmarks.
|
|
include_stale: If True, include stale bookmarks.
|
|
max_age_hours: Maximum age for stale detection.
|
|
sort_by: Sort field ('last_updated', 'created_at', 'title', 'provider').
|
|
|
|
Returns:
|
|
Flat list of Bookmark objects sorted by sort_by.
|
|
|
|
Raises:
|
|
ValueError: If sort_by is not a recognised field.
|
|
"""
|
|
return self.bookmark_ops.get_all_bookmarks_flat(
|
|
content_type=content_type,
|
|
include_completed=include_completed,
|
|
include_stale=include_stale,
|
|
max_age_hours=max_age_hours,
|
|
sort_by=sort_by,
|
|
)
|
|
|
|
def delete_all_bookmarks_for_content(
|
|
self,
|
|
content_id: str,
|
|
provider_filter: Optional[List[str]] = None,
|
|
) -> Dict[str, bool]:
|
|
"""
|
|
Delete bookmarks for a specific content ID across all providers.
|
|
|
|
Useful when content is removed from a provider.
|
|
|
|
Args:
|
|
content_id: Content identifier to delete.
|
|
provider_filter: Optional list of provider names to restrict deletion.
|
|
|
|
Returns:
|
|
Dict mapping provider name → True if deleted, False if not found.
|
|
"""
|
|
return self.bookmark_ops.delete_all_bookmarks_for_content(
|
|
content_id, provider_filter=provider_filter
|
|
)
|
|
|
|
def cleanup_stale_bookmarks(
|
|
self,
|
|
max_age_hours: int = 720,
|
|
dry_run: bool = True,
|
|
provider_filter: Optional[List[str]] = None,
|
|
) -> Dict[str, int]:
|
|
"""
|
|
Remove bookmarks older than max_age_hours.
|
|
|
|
Args:
|
|
max_age_hours: Age threshold in hours (default 720 = 30 days).
|
|
dry_run: If True, only report what would be deleted without actually deleting.
|
|
provider_filter: Optional list of provider names to restrict cleanup.
|
|
|
|
Returns:
|
|
Dict mapping provider name → number of bookmarks deleted (or would-be deleted).
|
|
"""
|
|
return self.bookmark_ops.cleanup_stale_bookmarks(
|
|
max_age_hours=max_age_hours,
|
|
dry_run=dry_run,
|
|
provider_filter=provider_filter,
|
|
)
|
|
|
|
def get_bookmark_stats(self, max_age_hours: int = 720) -> Dict[str, Any]:
|
|
"""
|
|
Get statistics about bookmarks across all providers.
|
|
|
|
Args:
|
|
max_age_hours: Age threshold used for staleness classification.
|
|
|
|
Returns:
|
|
Dictionary with counts by status, content type, and provider.
|
|
"""
|
|
return self.bookmark_ops.get_bookmark_stats(max_age_hours=max_age_hours)
|
|
|
|
def validate_bookmarks(
|
|
self, provider_name: str, auto_fix: bool = False
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Validate all bookmarks from a provider and optionally fix common issues.
|
|
|
|
Args:
|
|
provider_name: Name of the provider.
|
|
auto_fix: If True, attempt to fix fixable issues.
|
|
|
|
Returns:
|
|
Dictionary with validation results (total, errors, warnings, fixed).
|
|
"""
|
|
return self.bookmark_ops.validate_bookmarks(provider_name, auto_fix=auto_fix)
|
|
|
|
# ==========================================================================
|
|
# Favorite OPERATIONS (delegate to FavoriteOperations)
|
|
# ==========================================================================
|
|
|
|
def get_favorites(
|
|
self,
|
|
provider_name: str,
|
|
favorite_type: Optional[FavoriteType] = None,
|
|
) -> List[Favorite]:
|
|
"""Get favorites from a specific provider."""
|
|
return self.favorite_ops.get_favorites(provider_name, favorite_type)
|
|
|
|
def add_favorite(
|
|
self,
|
|
provider_name: str,
|
|
content_id: str,
|
|
favorite_type: FavoriteType,
|
|
title: Optional[str] = None,
|
|
**kwargs,
|
|
) -> Optional[Favorite]:
|
|
"""Add a favorite."""
|
|
return self.favorite_ops.add_favorite(
|
|
provider_name, content_id, favorite_type, title, **kwargs
|
|
)
|
|
|
|
def remove_favorite(
|
|
self, provider_name: str, content_id: str
|
|
) -> bool:
|
|
"""Remove a favorite."""
|
|
return self.favorite_ops.remove_favorite(provider_name, content_id)
|
|
|
|
def get_all_favorites(
|
|
self,
|
|
favorite_type: Optional[FavoriteType] = None,
|
|
) -> Dict[str, List[Favorite]]:
|
|
"""Get favorites from all enabled providers."""
|
|
return self.favorite_ops.get_all_favorites(favorite_type)
|
|
|
|
def is_favorited(
|
|
self, provider_name: str, content_id: str
|
|
) -> bool:
|
|
"""Check if content is favorited."""
|
|
return self.favorite_ops.is_favorited(provider_name, content_id)
|
|
|
|
# ==========================================================================
|
|
# SUBSCRIPTION OPERATIONS (delegate to SubscriptionOperations)
|
|
# ==========================================================================
|
|
|
|
def get_subscription_status(self, provider_name: str, **kwargs):
|
|
return self.subscription_ops.get_subscription_status(provider_name, **kwargs)
|
|
|
|
def get_subscribed_channels(self, provider_name: str, **kwargs):
|
|
return self.subscription_ops.get_subscribed_channels(provider_name, **kwargs)
|
|
|
|
def get_available_packages(self, provider_name: str, **kwargs):
|
|
return self.subscription_ops.get_available_packages(provider_name, **kwargs)
|
|
|
|
def is_channel_accessible(self, provider_name: str, channel_id: str, **kwargs):
|
|
return self.subscription_ops.is_channel_accessible(provider_name, channel_id, **kwargs)
|
|
|
|
# ==========================================================================
|
|
# UTILITY METHODS (remain in ProviderManager as helpers)
|
|
# ==========================================================================
|
|
|
|
def get_provider_http_manager(self, provider_name: str):
|
|
"""Get HTTP manager for a specific provider."""
|
|
provider = self.get_provider(provider_name)
|
|
if not provider:
|
|
logger.warning(f"ProviderManager: Provider '{provider_name}' not found")
|
|
return None
|
|
|
|
http_manager = provider.http_manager
|
|
if not http_manager:
|
|
logger.warning(f"ProviderManager: Provider '{provider_name}' has no HTTP manager")
|
|
return None
|
|
|
|
logger.debug(f"ProviderManager: Retrieved HTTP manager for '{provider_name}'")
|
|
return http_manager
|
|
|
|
def needs_proxy(self, provider_name: str) -> bool:
|
|
"""Check if a provider needs proxy support."""
|
|
http_manager = self.get_provider_http_manager(provider_name)
|
|
if not http_manager:
|
|
return False
|
|
|
|
has_proxy = http_manager.config.proxy_config is not None
|
|
logger.debug(
|
|
f"ProviderManager: Provider '{provider_name}' {'requires' if has_proxy else 'does not require'} proxy"
|
|
)
|
|
return has_proxy
|
|
|
|
def get_provider_choices(self) -> Dict[int, str]:
|
|
"""
|
|
Get numbered provider choices for user selection.
|
|
Includes an 'all' option as the last choice.
|
|
"""
|
|
enabled_providers = self.registry.get_enabled_providers()
|
|
choices = {i + 1: name for i, name in enumerate(enabled_providers)}
|
|
choices[len(choices) + 1] = "all"
|
|
return choices
|
|
|
|
def get_selected_providers(self, choices_input: str) -> List[str]:
|
|
"""
|
|
Convert user input string to list of provider names.
|
|
|
|
Args:
|
|
choices_input: Comma-separated string of choice numbers
|
|
|
|
Returns:
|
|
List of selected provider names
|
|
"""
|
|
available = self.registry.get_enabled_providers()
|
|
|
|
if not available:
|
|
logger.warning("ProviderManager: No enabled providers available for selection")
|
|
return []
|
|
|
|
selected = []
|
|
for choice in choices_input.split(","):
|
|
choice = choice.strip()
|
|
if choice.isdigit():
|
|
idx = int(choice) - 1
|
|
if 0 <= idx < len(available):
|
|
selected.append(available[idx])
|
|
elif idx == len(available): # 'all' option
|
|
selected = available.copy()
|
|
break
|
|
|
|
result = selected if selected else available
|
|
logger.debug(
|
|
f"ProviderManager: Selected {len(result)} providers from input '{choices_input}'"
|
|
)
|
|
return result
|
|
|
|
# ==========================================================================
|
|
# LEGACY COMPATIBILITY METHODS
|
|
# ==========================================================================
|
|
|
|
def _extract_pssh_from_manifest(self, manifest_url: str) -> List:
|
|
"""
|
|
Extract PSSH data from a manifest URL.
|
|
Kept for backward compatibility but delegated to DRMOperations.
|
|
"""
|
|
return self.drm_ops._extract_pssh_from_manifest(manifest_url) |