mirror of
https://github.com/nirvana-7777/script.service.ultimate.git
synced 2026-09-16 06:02:35 +02:00
Backend add favorites
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# streaming_providers/base/favorite_operations.py
|
||||
"""
|
||||
Favorite-related operations separated from core registry.
|
||||
Mirrors the structure of BookmarkOperations.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from .models.favorite import Favorite, FavoriteType
|
||||
from .utils.logger import logger
|
||||
|
||||
|
||||
class FavoriteOperations:
|
||||
"""Handles all favorite-related operations."""
|
||||
|
||||
def __init__(self, registry):
|
||||
self.registry = registry
|
||||
logger.debug("FavoriteOperations: Initialized")
|
||||
|
||||
def get_favorites(
|
||||
self,
|
||||
provider_name: str,
|
||||
favorite_type: Optional[FavoriteType] = None,
|
||||
) -> List[Favorite]:
|
||||
"""Get favorites from a specific provider."""
|
||||
provider = self.registry.get_provider(provider_name)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider '{provider_name}' not found or disabled")
|
||||
|
||||
if not provider.implements_favorites:
|
||||
return []
|
||||
|
||||
favorites = provider.get_favorites()
|
||||
|
||||
if favorite_type:
|
||||
favorites = [f for f in favorites if f.favorite_type == favorite_type]
|
||||
|
||||
return favorites
|
||||
|
||||
def add_favorite(
|
||||
self,
|
||||
provider_name: str,
|
||||
content_id: str,
|
||||
favorite_type: FavoriteType,
|
||||
title: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Optional[Favorite]:
|
||||
"""Add a favorite."""
|
||||
provider = self.registry.get_provider(provider_name)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider '{provider_name}' not found or disabled")
|
||||
|
||||
if not provider.implements_favorites:
|
||||
return None
|
||||
|
||||
return provider.add_favorite(
|
||||
content_id=content_id,
|
||||
favorite_type=favorite_type,
|
||||
title=title,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def remove_favorite(
|
||||
self, provider_name: str, content_id: str
|
||||
) -> bool:
|
||||
"""Remove a favorite."""
|
||||
provider = self.registry.get_provider(provider_name)
|
||||
if not provider:
|
||||
raise ValueError(f"Provider '{provider_name}' not found or disabled")
|
||||
|
||||
if not provider.implements_favorites:
|
||||
return False
|
||||
|
||||
try:
|
||||
provider.remove_favorite(content_id=content_id)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def get_all_favorites(
|
||||
self,
|
||||
favorite_type: Optional[FavoriteType] = None,
|
||||
) -> Dict[str, List[Favorite]]:
|
||||
"""Get favorites from all enabled providers."""
|
||||
enabled = self.registry.get_enabled_providers()
|
||||
result = {}
|
||||
errors = {}
|
||||
|
||||
for name in enabled:
|
||||
try:
|
||||
favorites = self.get_favorites(name, favorite_type=favorite_type)
|
||||
result[name] = favorites
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get favorites from '{name}': {e}")
|
||||
result[name] = []
|
||||
errors[name] = str(e)
|
||||
|
||||
if errors:
|
||||
result["_errors"] = errors
|
||||
|
||||
return result
|
||||
|
||||
def is_favorited(
|
||||
self, provider_name: str, content_id: str
|
||||
) -> bool:
|
||||
"""Check if content is favorited."""
|
||||
favorites = self.get_favorites(provider_name)
|
||||
return any(f.content_id == content_id for f in favorites)
|
||||
@@ -15,8 +15,10 @@ 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
|
||||
@@ -43,6 +45,7 @@ class ProviderManager:
|
||||
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
|
||||
@@ -649,6 +652,50 @@ class ProviderManager:
|
||||
"""
|
||||
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)
|
||||
# ==========================================================================
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# streaming_providers/base/models/favorite.py
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class FavoriteType(str, Enum):
|
||||
"""Type of content that can be favorited."""
|
||||
PROGRAM = "PROGRAM" # TV show, series, or movie
|
||||
CLIP = "CLIP" # Individual episode or video
|
||||
LIVE = "LIVE" # Live channel
|
||||
EVENT = "EVENT" # One-time event
|
||||
|
||||
|
||||
@dataclass
|
||||
class Favorite:
|
||||
"""
|
||||
A user-saved content reference for "Watch Later" or "My Bookmarks".
|
||||
|
||||
Unlike Bookmark (which tracks playback position), Favorite is just
|
||||
a pointer to content the user wants to remember. No progress tracking.
|
||||
"""
|
||||
|
||||
favorite_id: str # Composite "provider:content_id"
|
||||
provider: str
|
||||
content_id: str
|
||||
favorite_type: FavoriteType
|
||||
|
||||
# When it was saved
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
# Optional cached metadata for UI (reduces API calls)
|
||||
title: Optional[str] = None
|
||||
thumbnail_url: Optional[str] = None
|
||||
|
||||
# Series context (if applicable)
|
||||
series_title: Optional[str] = None
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
provider: str,
|
||||
content_id: str,
|
||||
favorite_type: FavoriteType,
|
||||
title: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "Favorite":
|
||||
"""Create a new favorite."""
|
||||
return cls(
|
||||
favorite_id=f"{provider}:{content_id}",
|
||||
provider=provider,
|
||||
content_id=content_id,
|
||||
favorite_type=favorite_type,
|
||||
title=title,
|
||||
created_at=datetime.now(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict:
|
||||
"""Convert to API-friendly dictionary."""
|
||||
return {
|
||||
"FavoriteId": self.favorite_id,
|
||||
"Provider": self.provider,
|
||||
"ContentId": self.content_id,
|
||||
"FavoriteType": self.favorite_type.value,
|
||||
"CreatedAt": self.created_at.isoformat(),
|
||||
"Title": self.title,
|
||||
"ThumbnailUrl": self.thumbnail_url,
|
||||
"SeriesTitle": self.series_title,
|
||||
}
|
||||
@@ -18,6 +18,7 @@ from ..providers.auth import AuthContext, AuthStatus
|
||||
from .models.proxy_models import ProxyConfig
|
||||
from .models import DRMConfig, Event, StreamingChannel
|
||||
from .models.bookmark import Bookmark, ContentType
|
||||
from .models.favorite import FavoriteType, Favorite
|
||||
from .models.subscription import SubscriptionPackage, UserSubscription
|
||||
from .models.recording import Recording
|
||||
from .models.timer import Timer
|
||||
@@ -1110,6 +1111,68 @@ class StreamingProvider(ABC):
|
||||
"Override this method to support timer deletion."
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Favorite Management
|
||||
# ============================================================================
|
||||
|
||||
@property
|
||||
def implements_favorites(self) -> bool:
|
||||
"""
|
||||
True if this provider can store/retrieve user favorites.
|
||||
|
||||
Default False. Override in providers that support favorites.
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_favorites(self, **kwargs) -> List["Favorite"]:
|
||||
"""
|
||||
Return all favorites for the authenticated user.
|
||||
|
||||
Returns:
|
||||
List of Favorite objects, or [] if not supported or none exist.
|
||||
"""
|
||||
return []
|
||||
|
||||
def add_favorite(
|
||||
self,
|
||||
content_id: str,
|
||||
favorite_type: FavoriteType,
|
||||
title: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "Favorite":
|
||||
"""
|
||||
Add a content to user's favorites.
|
||||
|
||||
Args:
|
||||
content_id: Content identifier.
|
||||
favorite_type: Type of content (PROGRAM, CLIP, LIVE, EVENT).
|
||||
title: Content title (optional for caching).
|
||||
|
||||
Returns:
|
||||
The saved Favorite object.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the provider rejects the favorite.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not implement add_favorite()."
|
||||
)
|
||||
|
||||
def remove_favorite(self, content_id: str, **kwargs) -> None:
|
||||
"""
|
||||
Remove a content from user's favorites.
|
||||
|
||||
Args:
|
||||
content_id: Content identifier to remove.
|
||||
|
||||
Raises:
|
||||
KeyError: If no favorite with this content_id exists.
|
||||
RuntimeError: If the provider refuses deletion.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not implement remove_favorite()."
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# CATCHUP ABSTRACT METHODS
|
||||
# ============================================================================
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Favorite-related route handlers.
|
||||
Mirrors the structure of bookmarks.py.
|
||||
"""
|
||||
|
||||
from bottle import request, response
|
||||
from streaming_providers.base.models.favorite import FavoriteType
|
||||
from streaming_providers.base.utils import logger
|
||||
|
||||
|
||||
def setup_favorites_routes(app, manager, service):
|
||||
"""Setup favorite-related routes."""
|
||||
|
||||
# ==========================================================================
|
||||
# SINGLE PROVIDER ROUTES
|
||||
# ==========================================================================
|
||||
|
||||
@app.route("/api/providers/<provider>/favorites", method="GET")
|
||||
def get_provider_favorites(provider):
|
||||
"""
|
||||
Get favorites from a specific provider.
|
||||
|
||||
Query parameters:
|
||||
- favorite_type: Optional (PROGRAM, CLIP, LIVE, EVENT)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"provider": "provider_name",
|
||||
"favorites": [ { ...Favorite fields... } ],
|
||||
"count": 1,
|
||||
"filters": {
|
||||
"favorite_type": null
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
favorite_type_str = request.params.get("favorite_type")
|
||||
favorite_type = None
|
||||
if favorite_type_str:
|
||||
try:
|
||||
favorite_type = FavoriteType(favorite_type_str.upper())
|
||||
except ValueError:
|
||||
response.status = 400
|
||||
return {
|
||||
"error": "Invalid favorite_type",
|
||||
"message": f"favorite_type must be one of: {[ft.value for ft in FavoriteType]}",
|
||||
"provider": provider,
|
||||
}
|
||||
|
||||
try:
|
||||
favorites = manager.get_favorites(
|
||||
provider_name=provider,
|
||||
favorite_type=favorite_type,
|
||||
)
|
||||
except ValueError as e:
|
||||
response.status = 404
|
||||
return {
|
||||
"error": "Provider not found",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get favorites from '{provider}': {e}")
|
||||
response.status = 500
|
||||
return {
|
||||
"error": "Failed to get favorites",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
}
|
||||
|
||||
response.status = 200
|
||||
return {
|
||||
"provider": provider,
|
||||
"favorites": [f.to_dict() for f in favorites],
|
||||
"count": len(favorites),
|
||||
"filters": {
|
||||
"favorite_type": favorite_type.value if favorite_type else None,
|
||||
},
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in get_provider_favorites: {e}")
|
||||
response.status = 500
|
||||
return {"error": "Internal server error", "message": str(e), "provider": provider}
|
||||
|
||||
@app.route("/api/providers/<provider>/favorites/<content_id>", method="GET")
|
||||
def get_provider_favorite(provider, content_id):
|
||||
"""
|
||||
Check if a specific content ID is favorited.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"provider": "provider_name",
|
||||
"content_id": "123",
|
||||
"is_favorited": true,
|
||||
"favorite": { ...Favorite fields... } // Only if favorited
|
||||
}
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
is_favorited = manager.is_favorited(
|
||||
provider_name=provider,
|
||||
content_id=content_id,
|
||||
)
|
||||
# If favorited, get the full favorite object
|
||||
favorite = None
|
||||
if is_favorited:
|
||||
favorites = manager.get_favorites(provider_name=provider)
|
||||
for f in favorites:
|
||||
if f.content_id == content_id:
|
||||
favorite = f
|
||||
break
|
||||
except ValueError as e:
|
||||
response.status = 404
|
||||
return {"error": "Provider not found", "message": str(e), "provider": provider}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check favorite from '{provider}': {e}")
|
||||
response.status = 500
|
||||
return {
|
||||
"error": "Failed to check favorite",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
}
|
||||
|
||||
response.status = 200
|
||||
result = {
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
"is_favorited": is_favorited,
|
||||
}
|
||||
if favorite:
|
||||
result["favorite"] = favorite.to_dict()
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in get_provider_favorite: {e}")
|
||||
response.status = 500
|
||||
return {"error": "Internal server error", "message": str(e), "provider": provider}
|
||||
|
||||
@app.route("/api/providers/<provider>/favorites/<content_id>", method="POST")
|
||||
def add_provider_favorite(provider, content_id):
|
||||
"""
|
||||
Add a favorite.
|
||||
|
||||
Request body (JSON):
|
||||
{
|
||||
"favorite_type": "PROGRAM", // Required
|
||||
"title": "Show Title", // Optional
|
||||
"thumbnail_url": "https://...", // Optional
|
||||
"series_title": "Series Name" // Optional (for series/program)
|
||||
}
|
||||
|
||||
Returns:
|
||||
{ ...Favorite fields... }
|
||||
"""
|
||||
try:
|
||||
data = request.json
|
||||
if not data:
|
||||
response.status = 400
|
||||
return {
|
||||
"error": "Invalid request",
|
||||
"message": "Request body is required",
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
# --- favorite_type (required) ---
|
||||
favorite_type_str = data.get("favorite_type")
|
||||
if not favorite_type_str:
|
||||
response.status = 400
|
||||
return {
|
||||
"error": "Missing required field",
|
||||
"message": "favorite_type is required",
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
try:
|
||||
favorite_type = FavoriteType(favorite_type_str.upper())
|
||||
except ValueError:
|
||||
response.status = 400
|
||||
return {
|
||||
"error": "Invalid favorite_type",
|
||||
"message": f"favorite_type must be one of: {[ft.value for ft in FavoriteType]}",
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
try:
|
||||
favorite = manager.add_favorite(
|
||||
provider_name=provider,
|
||||
content_id=content_id,
|
||||
favorite_type=favorite_type,
|
||||
title=data.get("title"),
|
||||
thumbnail_url=data.get("thumbnail_url"),
|
||||
series_title=data.get("series_title"),
|
||||
)
|
||||
except ValueError as e:
|
||||
response.status = 404
|
||||
return {
|
||||
"error": "Provider not found",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
except RuntimeError as e:
|
||||
response.status = 409
|
||||
return {
|
||||
"error": "Favorite addition rejected",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add favorite '{content_id}' to '{provider}': {e}")
|
||||
response.status = 500
|
||||
return {
|
||||
"error": "Failed to add favorite",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
if favorite is None:
|
||||
response.status = 501
|
||||
return {
|
||||
"error": "Favorites not supported",
|
||||
"message": f"Provider '{provider}' does not support favorites",
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
response.status = 200
|
||||
return favorite.to_dict()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in add_provider_favorite: {e}")
|
||||
response.status = 500
|
||||
return {
|
||||
"error": "Internal server error",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
@app.route("/api/providers/<provider>/favorites/<content_id>", method="DELETE")
|
||||
def remove_provider_favorite(provider, content_id):
|
||||
"""
|
||||
Remove a favorite.
|
||||
|
||||
Returns:
|
||||
204 No Content on success.
|
||||
404 if the provider or favorite is not found.
|
||||
409 if the provider refuses deletion.
|
||||
500 on unexpected errors.
|
||||
"""
|
||||
try:
|
||||
try:
|
||||
removed = manager.remove_favorite(
|
||||
provider_name=provider,
|
||||
content_id=content_id,
|
||||
)
|
||||
except ValueError as e:
|
||||
response.status = 404
|
||||
return {
|
||||
"error": "Provider not found",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
except RuntimeError as e:
|
||||
response.status = 409
|
||||
return {
|
||||
"error": "Deletion refused by provider",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove favorite '{content_id}' from '{provider}': {e}")
|
||||
response.status = 500
|
||||
return {
|
||||
"error": "Failed to remove favorite",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
if not removed:
|
||||
response.status = 404
|
||||
return {
|
||||
"error": "Favorite not found",
|
||||
"message": f"No favorite with content_id '{content_id}' from '{provider}'",
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
response.status = 204
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in remove_provider_favorite: {e}")
|
||||
response.status = 500
|
||||
return {
|
||||
"error": "Internal server error",
|
||||
"message": str(e),
|
||||
"provider": provider,
|
||||
"content_id": content_id,
|
||||
}
|
||||
|
||||
# ==========================================================================
|
||||
# AGGREGATE ROUTES (ALL PROVIDERS)
|
||||
# ==========================================================================
|
||||
|
||||
@app.route("/api/favorites/all", method="GET")
|
||||
def get_all_favorites():
|
||||
"""
|
||||
Get favorites from all enabled providers.
|
||||
|
||||
Query parameters:
|
||||
- favorite_type: Optional (PROGRAM, CLIP, LIVE, EVENT)
|
||||
|
||||
Returns:
|
||||
{
|
||||
"favorites": [ { ...Favorite fields... } ],
|
||||
"count": 1,
|
||||
"by_provider": { "provider_name": 1 },
|
||||
"filters": { "favorite_type": null },
|
||||
"errors": { ... } // Only present if any providers failed
|
||||
}
|
||||
"""
|
||||
try:
|
||||
favorite_type_str = request.params.get("favorite_type")
|
||||
favorite_type = None
|
||||
if favorite_type_str:
|
||||
try:
|
||||
favorite_type = FavoriteType(favorite_type_str.upper())
|
||||
except ValueError:
|
||||
response.status = 400
|
||||
return {
|
||||
"error": "Invalid favorite_type",
|
||||
"message": f"favorite_type must be one of: {[ft.value for ft in FavoriteType]}",
|
||||
}
|
||||
|
||||
all_data = manager.get_all_favorites(favorite_type=favorite_type)
|
||||
errors = all_data.pop("_errors", None)
|
||||
|
||||
# Flatten the list
|
||||
flat_list = [f for favorites in all_data.values() for f in favorites]
|
||||
|
||||
by_provider = {}
|
||||
for favorite in flat_list:
|
||||
by_provider[favorite.provider] = by_provider.get(favorite.provider, 0) + 1
|
||||
|
||||
result = {
|
||||
"favorites": [f.to_dict() for f in flat_list],
|
||||
"count": len(flat_list),
|
||||
"by_provider": by_provider,
|
||||
"filters": {
|
||||
"favorite_type": favorite_type.value if favorite_type else None,
|
||||
},
|
||||
}
|
||||
if errors:
|
||||
result["errors"] = errors
|
||||
|
||||
response.status = 200
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in get_all_favorites: {e}")
|
||||
response.status = 500
|
||||
return {"error": "Internal server error", "message": str(e)}
|
||||
|
||||
# ==========================================================================
|
||||
# BULK OPERATIONS ROUTES
|
||||
# ==========================================================================
|
||||
|
||||
@app.route("/api/favorites/clear", method="POST")
|
||||
def clear_all_favorites():
|
||||
"""
|
||||
Remove all favorites from all providers (or specific providers).
|
||||
|
||||
Request body (JSON):
|
||||
{
|
||||
"provider_filter": ["rtl_de"] // Optional, list of provider name strings
|
||||
}
|
||||
|
||||
Returns:
|
||||
{
|
||||
"deleted": { "rtl_de": 5, "joyn_de": 3 },
|
||||
"total_deleted": 8
|
||||
}
|
||||
"""
|
||||
try:
|
||||
data = request.json or {}
|
||||
|
||||
provider_filter = data.get("provider_filter")
|
||||
if provider_filter is not None:
|
||||
if not isinstance(provider_filter, list) or not all(
|
||||
isinstance(p, str) for p in provider_filter
|
||||
):
|
||||
response.status = 400
|
||||
return {
|
||||
"error": "Invalid provider_filter",
|
||||
"message": "provider_filter must be a list of provider name strings",
|
||||
}
|
||||
|
||||
results = {}
|
||||
total = 0
|
||||
|
||||
# Get all favorites first
|
||||
all_favorites = manager.get_all_favorites()
|
||||
errors = all_favorites.pop("_errors", None)
|
||||
|
||||
for provider_name, favorites in all_favorites.items():
|
||||
if provider_filter and provider_name not in provider_filter:
|
||||
continue
|
||||
|
||||
deleted = 0
|
||||
for favorite in favorites:
|
||||
try:
|
||||
if manager.remove_favorite(provider_name, favorite.content_id):
|
||||
deleted += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove favorite {favorite.content_id}: {e}")
|
||||
|
||||
if deleted > 0:
|
||||
results[provider_name] = deleted
|
||||
total += deleted
|
||||
|
||||
response.status = 200
|
||||
return {
|
||||
"deleted": results,
|
||||
"total_deleted": total,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error in clear_all_favorites: {e}")
|
||||
response.status = 500
|
||||
return {"error": "Internal server error", "message": str(e)}
|
||||
@@ -1705,6 +1705,7 @@ class UltimateService:
|
||||
from routes.recordings import setup_recordings_routes
|
||||
from routes.timers import setup_timers_routes
|
||||
from routes.bookmarks import setup_bookmarks_routes
|
||||
from routes.favorites import setup_favorites_routes
|
||||
|
||||
# Setup routes from separate modules
|
||||
setup_provider_routes(self.app, self.manager, self)
|
||||
@@ -1719,6 +1720,7 @@ class UltimateService:
|
||||
setup_recordings_routes(self.app, self.manager, self)
|
||||
setup_timers_routes(self.app, self.manager, self)
|
||||
setup_bookmarks_routes(self.app, self.manager, self)
|
||||
setup_favorites_routes(app, manager, service)
|
||||
|
||||
# Core UI routes
|
||||
@self.app.route("/config")
|
||||
|
||||
Reference in New Issue
Block a user