diff --git a/apps/article/urls.py b/apps/article/urls.py index 5946b8f..2705478 100755 --- a/apps/article/urls.py +++ b/apps/article/urls.py @@ -1,8 +1,28 @@ +from functools import wraps from django.urls import include, path, re_path +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers from rest_framework.routers import SimpleRouter from .views import * from .views_admin import AdminArticleCategoryViewSet, AdminArticleContentViewSet, AdminArticleViewSet +# Helper function to cache public views +def cached_view(view_func): + return cache_page(60 * 60 * 2, key_prefix='article_api')(vary_on_headers('Accept-Language')(view_func)) + +# Helper to conditionally cache list views (bypasses cache when is_bookmark=true) +def conditional_cached_view(timeout, key_prefix): + def decorator(view_func): + cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func)) + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower() + if is_bookmark in ('true', '1'): + return view_func(request, *args, **kwargs) + return cached_func(request, *args, **kwargs) + return _wrapped_view + return decorator + app_name = 'article' admin_router = SimpleRouter() @@ -20,11 +40,11 @@ dovodi_router.register(r'collections', DovodiArticleCollectionViewSet, basename= urlpatterns = [ path('dovodi/', include(dovodi_router.urls)), - path('categories/', ArticleCategoryListAPIView.as_view(), name='category-list'), - path('pinned-collections/', PinnedArticleCollectionListView.as_view(), name='pinned-collection-list'), - path('collections/', MiddleArticleCollectionListView.as_view(), name='collection-list'), + path('categories/', cached_view(ArticleCategoryListAPIView.as_view()), name='category-list'), + path('pinned-collections/', cached_view(PinnedArticleCollectionListView.as_view()), name='pinned-collection-list'), + path('collections/', cached_view(MiddleArticleCollectionListView.as_view()), name='collection-list'), - path('list/', ArticleListAPIView.as_view(), name='article-list'), + path('list/', conditional_cached_view(60 * 30, 'article_list')(ArticleListAPIView.as_view()), name='article-list'), path('detail//', ArticleDetailAPIView.as_view(), name='article-detail'), re_path(r'detail/(?P.+)/$', ArticleDetailAPIView.as_view(), name='podcast-detail'), path('admin/', include(admin_router.urls)), diff --git a/apps/article/views.py b/apps/article/views.py index d325805..2f0d092 100755 --- a/apps/article/views.py +++ b/apps/article/views.py @@ -72,14 +72,9 @@ class PinnedArticleCollectionListView(generics.ListAPIView): def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) categories_count = ArticleCategory.objects.filter(status=True).count() - from apps.bookmark.models import Bookmark - bookmarks_count = Bookmark.objects.filter( - service=Bookmark.ServiceChoices.ARTICLE, - ).count() info = { "categories_count": categories_count, - "bookmarks_count": bookmarks_count, } data = { @@ -199,7 +194,7 @@ class ArticleListAPIView(generics.ListAPIView): # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower() if is_bookmark == 'true': # Import Bookmark model here to avoid circular imports from apps.bookmark.models import Bookmark diff --git a/apps/dobodbi_calendar/urls.py b/apps/dobodbi_calendar/urls.py index 2581fec..ec2edfd 100644 --- a/apps/dobodbi_calendar/urls.py +++ b/apps/dobodbi_calendar/urls.py @@ -1,8 +1,14 @@ from django.urls import include, path +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers from rest_framework.routers import SimpleRouter from apps.dobodbi_calendar.views import CalendarList, AdjustmentConfigView, OccasionsList from apps.dobodbi_calendar.views_admin import AdminCalendarOccasionViewSet +# Helper function for caching calendar endpoints +def cached_view(view_func): + return cache_page(60 * 60 * 6, key_prefix='calendar_api')(vary_on_headers('Accept-Language')(view_func)) + admin_router = SimpleRouter() admin_router.register(r'occasions', AdminCalendarOccasionViewSet, basename='admin-calendar-occasions') @@ -11,7 +17,7 @@ for prefix, viewset, basename in admin_router.registry: viewset.swagger_schema = None urlpatterns = [ path('admin/', include(admin_router.urls)), - path('sync-occasions/', CalendarList.as_view()), - path('adjustemnts/', AdjustmentConfigView.as_view()), - path('occasions/', OccasionsList.as_view()), + path('sync-occasions/', cached_view(CalendarList.as_view())), + path('adjustemnts/', cached_view(AdjustmentConfigView.as_view())), + path('occasions/', cached_view(OccasionsList.as_view())), ] diff --git a/apps/library/serializers.py b/apps/library/serializers.py index 2b0d085..5bfa4d5 100644 --- a/apps/library/serializers.py +++ b/apps/library/serializers.py @@ -185,7 +185,7 @@ class BookSerializer(serializers.ModelSerializer): # user = User.objects.get(email='root@admin.com') # # Get the current user from the request context request = self.context.get('request') - user = request.user if request and request.user.is_authenticated else None + user = request.user if request and getattr(request.user, 'is_authenticated', False) else None if not user: return { diff --git a/apps/library/urls.py b/apps/library/urls.py index 389f05e..e9fb9c8 100644 --- a/apps/library/urls.py +++ b/apps/library/urls.py @@ -1,4 +1,6 @@ from django.urls import include, path +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers from rest_framework.routers import SimpleRouter from apps.library.views import ( @@ -13,6 +15,25 @@ from apps.library.views import ( DownloadedBooksListView, BookDownloadCreateAPIView, ) + +from functools import wraps + +# Helper function to cache public views +def cached_view(view_func): + return cache_page(60 * 60 * 2, key_prefix='library_api')(vary_on_headers('Accept-Language')(view_func)) + +# Helper to conditionally cache list views (bypasses cache when is_bookmark=true) +def conditional_cached_view(timeout, key_prefix): + def decorator(view_func): + cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func)) + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower() + if is_bookmark in ('true', '1'): + return view_func(request, *args, **kwargs) + return cached_func(request, *args, **kwargs) + return _wrapped_view + return decorator from apps.library.views_admin import ( AdminAuthorViewSet, AdminBookViewSet, @@ -40,13 +61,13 @@ for prefix, viewset, basename in admin_router.registry: urlpatterns = [ path('admin/', include(admin_router.urls)), path('dovodi/', include(dovodi_router.urls)), - path('categories/', CategoryListView.as_view(), name='category-list'), - path('pinned-collections/', PinnedBookCollectionListView.as_view(), name='pinned-collection-list'), - path('collections/', MiddleBookCollectionListView.as_view(), name='collection-list'), - path('authors/', AuthorListView.as_view(), name='author-list'), + path('categories/', cached_view(CategoryListView.as_view()), name='category-list'), + path('pinned-collections/', cached_view(PinnedBookCollectionListView.as_view()), name='pinned-collection-list'), + path('collections/', cached_view(MiddleBookCollectionListView.as_view()), name='collection-list'), + path('authors/', cached_view(AuthorListView.as_view()), name='author-list'), path('authors//', AuthorDetailView.as_view(), name='author-detail'), path('authors//books/', AuthorBookListView.as_view(), name='author-books'), - path('books/', BookListView.as_view(), name='book-list'), + path('books/', conditional_cached_view(60 * 30, 'library_books')(BookListView.as_view()), name='book-list'), path('books//', BookDetailView.as_view(), name='book-detail'), path('books/downloaded/', DownloadedBooksListView.as_view(), name='downloaded-books-list'), path('books/download/', BookDownloadCreateAPIView.as_view(), name='book-download'), diff --git a/apps/library/views.py b/apps/library/views.py index 15f4f66..cb5f097 100644 --- a/apps/library/views.py +++ b/apps/library/views.py @@ -75,14 +75,9 @@ class PinnedBookCollectionListView(ListAPIView): def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) categories_count = Category.objects.filter(status=True).count() - from apps.bookmark.models import Bookmark - bookmarks_count = Bookmark.objects.filter( - service=Bookmark.ServiceChoices.LIBRARY, - ).count() downloads_count = BookDownload.objects.all().count() info = { "categories_count": categories_count, - "bookmarks_count": bookmarks_count, "downloads_count": downloads_count } data = { @@ -110,6 +105,20 @@ class BookListView(ListAPIView): def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) + def get_serializer_context(self): + context = super().get_serializer_context() + is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower() + if is_bookmark not in ('true', '1') and 'request' in context: + from django.contrib.auth.models import AnonymousUser + class PublicRequestWrapper: + def __init__(self, req): + self._req = req + self.user = AnonymousUser() + def __getattr__(self, item): + return getattr(self._req, item) + context['request'] = PublicRequestWrapper(context['request']) + return context + def get_queryset(self): queryset = Book.objects.filter(status=True).select_related('author') @@ -148,8 +157,8 @@ class BookListView(ListAPIView): # queryset = queryset.filter(collections__in=bottom_collections) # Filter by bookmarked books if requested - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: + is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower() + if is_bookmark in ('true', '1') and self.request.user.is_authenticated: # Import Bookmark model here to avoid circular imports from apps.bookmark.models import Bookmark diff --git a/apps/podcast/urls.py b/apps/podcast/urls.py index e16ff09..4706229 100755 --- a/apps/podcast/urls.py +++ b/apps/podcast/urls.py @@ -1,8 +1,29 @@ +from functools import wraps from django.urls import path, re_path , include +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers from rest_framework.routers import SimpleRouter from .views import * from .views_admin import * +# Helper function to cache public views +def cached_view(view_func): + return cache_page(60 * 60 * 2, key_prefix='podcast_api')(vary_on_headers('Accept-Language')(view_func)) + +# Helper to conditionally cache list views (bypasses cache when is_bookmark=true or in_playlist=true) +def conditional_cached_view(timeout, key_prefix): + def decorator(view_func): + cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func)) + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower() + in_playlist = request.GET.get('in_playlist', '').lower() + if is_bookmark in ('true', '1') or in_playlist in ('true', '1'): + return view_func(request, *args, **kwargs) + return cached_func(request, *args, **kwargs) + return _wrapped_view + return decorator + admin_router = SimpleRouter() admin_router.register(r'podcasts', AdminPodcastViewSet, basename='admin-podcast') admin_router.register(r'playlists', AdminPodcastPlaylistViewSet, basename='admin-podcast-playlist') @@ -23,11 +44,11 @@ app_name = 'podcast' urlpatterns = [ path('admin/', include(admin_router.urls)), path('dovodi/', include(dovodi_router.urls)), - path('categories/', PodcastCategoryListAPIView.as_view(), name='category-list'), - path('pinned-collections/', PinnedPodcastCollectionListView.as_view(), name='pinned-collection-list'), - path('collections/', MiddlePodcastCollectionListView.as_view(), name='collection-list'), + path('categories/', cached_view(PodcastCategoryListAPIView.as_view()), name='category-list'), + path('pinned-collections/', cached_view(PinnedPodcastCollectionListView.as_view()), name='pinned-collection-list'), + path('collections/', cached_view(MiddlePodcastCollectionListView.as_view()), name='collection-list'), - path('list/', PodcastListAPIView.as_view(), name='podcast-list'), + path('list/', conditional_cached_view(60 * 30, 'podcast_list')(PodcastListAPIView.as_view()), name='podcast-list'), re_path(r'detail/(?P[\w-]+)/$', PodcastDetailAPIView.as_view(), name='podcast-detail'), # User playlist endpoints diff --git a/apps/podcast/views.py b/apps/podcast/views.py index aa87fcb..7cb3911 100644 --- a/apps/podcast/views.py +++ b/apps/podcast/views.py @@ -84,17 +84,9 @@ class PinnedPodcastCollectionListView(generics.ListAPIView): def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) categories_count = PodcastCategory.objects.filter(status=True).count() - # Count podcasts in the user's playlist - user_playlist_count = 0 - if request.user.is_authenticated: - user_playlist_count = UserPlaylist.objects.filter( - user=request.user, - status=True - ).count() info = { "categories_count": categories_count, - "user_playlist_count": user_playlist_count, } data = { "count": response.data.get("count"), @@ -219,7 +211,7 @@ class PodcastListAPIView(generics.ListAPIView): ) # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower() if is_bookmark == 'true': from apps.bookmark.models import Bookmark diff --git a/apps/video/management/commands/assign_random_videos_to_playlists.py b/apps/video/management/commands/assign_random_videos_to_playlists.py new file mode 100644 index 0000000..f4cbb86 --- /dev/null +++ b/apps/video/management/commands/assign_random_videos_to_playlists.py @@ -0,0 +1,126 @@ +import random +from django.core.management.base import BaseCommand +from django.db import transaction +from apps.video.models import Video, VideoPlaylist, PlaylistItem + + +class Command(BaseCommand): + help = 'Assign random videos from the database to all video playlists so each has at least 7-8 items' + + def add_arguments(self, parser): + parser.add_argument( + '--min-items', + type=int, + default=7, + help='Minimum number of videos per playlist (default: 7)' + ) + parser.add_argument( + '--max-items', + type=int, + default=8, + help='Maximum number of videos per playlist (default: 8)' + ) + parser.add_argument( + '--clear-existing', + action='store_true', + help='Clear existing playlist items before assigning new random videos' + ) + + def handle(self, *args, **options): + min_items = options['min_items'] + max_items = options['max_items'] + clear_existing = options['clear_existing'] + + if max_items < min_items: + max_items = min_items + + videos = list(Video.objects.filter(status=True)) + if not videos: + videos = list(Video.objects.all()) + + if not videos: + self.stdout.write(self.style.ERROR('No videos found in the database!')) + return + + playlists = list(VideoPlaylist.objects.all()) + if not playlists: + self.stdout.write(self.style.ERROR('No video playlists found in the database!')) + return + + self.stdout.write(self.style.NOTICE( + f'Found {len(videos)} videos and {len(playlists)} playlists.' + )) + self.stdout.write(f'Target items per playlist: {min_items} to {max_items}\n') + + total_created = 0 + + with transaction.atomic(): + for playlist in playlists: + if clear_existing: + playlist.playlist_items.all().delete() + existing_video_ids = set() + current_count = 0 + else: + existing_items = list(playlist.playlist_items.all()) + existing_video_ids = {item.video_id for item in existing_items} + current_count = len(existing_video_ids) + + target_count = random.randint(min_items, max_items) + + # If the playlist already has at least min_items and clear_existing is False, skip or top up + needed_count = target_count - current_count + if needed_count <= 0: + self.stdout.write( + f' [=] Playlist #{playlist.id} already has {current_count} videos (>= {min_items}).' + ) + continue + + # Available videos not already in this playlist + available_videos = [v for v in videos if v.id not in existing_video_ids] + + if not available_videos: + self.stdout.write( + self.style.WARNING( + f' [!] Playlist #{playlist.id}: No more unique videos available to add.' + ) + ) + continue + + # Select random sample of available videos + sample_size = min(needed_count, len(available_videos)) + selected_videos = random.sample(available_videos, sample_size) + + # Determine starting priority + start_priority = current_count + 1 + new_items = [] + for idx, video in enumerate(selected_videos): + new_items.append( + PlaylistItem( + playlist=playlist, + video=video, + priority=start_priority + idx + ) + ) + + PlaylistItem.objects.bulk_create(new_items) + total_created += len(new_items) + + # Update total duration of the playlist + try: + playlist.total_time = playlist.calculate_total_time() + playlist.save(update_fields=['total_time']) + except Exception as e: + self.stdout.write(self.style.WARNING(f' Warning updating total_time: {e}')) + + new_total = current_count + len(new_items) + self.stdout.write( + self.style.SUCCESS( + f' [+] Playlist #{playlist.id}: Added {len(new_items)} videos (Total: {new_total})' + ) + ) + + self.stdout.write( + self.style.SUCCESS( + f'\nDone! Successfully assigned {total_created} video items across {len(playlists)} playlists.' + ) + ) diff --git a/apps/video/serializers.py b/apps/video/serializers.py index 9f632c2..c307510 100644 --- a/apps/video/serializers.py +++ b/apps/video/serializers.py @@ -329,17 +329,4 @@ class MiddleVideoCollectionSerializer(serializers.ModelSerializer): status=True, playlist_items__video__status=True ).distinct().order_by('order', '-created_at') - - # Filter by bookmarks if requested in context - is_bookmark = self.context.get('is_bookmark') - request = self.context.get('request') - if is_bookmark == 'true' and request and request.user.is_authenticated: - from apps.bookmark.models import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=request.user, - service=Bookmark.ServiceChoices.VIDEO_PLAYLIST, - status=True - ).values_list('content_id', flat=True) - playlists = playlists.filter(id__in=bookmarked_ids) - return VideoPlaylistListSerializer(playlists, many=True, context=self.context).data diff --git a/apps/video/urls.py b/apps/video/urls.py index e3b3d80..163303e 100644 --- a/apps/video/urls.py +++ b/apps/video/urls.py @@ -1,8 +1,28 @@ +from functools import wraps from django.urls import include, path, re_path +from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_headers from rest_framework.routers import SimpleRouter from .views import * from .views_admin import * +# Helper function to cache public views +def cached_view(view_func): + return cache_page(60 * 60 * 2, key_prefix='video_api')(vary_on_headers('Accept-Language')(view_func)) + +# Helper to conditionally cache list views (bypasses cache when is_bookmark=true) +def conditional_cached_view(timeout, key_prefix): + def decorator(view_func): + cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func)) + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower() + if is_bookmark in ('true', '1'): + return view_func(request, *args, **kwargs) + return cached_func(request, *args, **kwargs) + return _wrapped_view + return decorator + app_name = 'video' admin_router = SimpleRouter() @@ -23,15 +43,15 @@ dovodi_router.register(r'items', DovodiVideoItemViewSet, basename='dovodi-video- urlpatterns = [ path('admin/', include(admin_router.urls)), path('dovodi/', include(dovodi_router.urls)), - path('categories/', VideoCategoryListAPIView.as_view(), name='category-list'), - path('pinned-collections/', PinnedVideoCollectionListView.as_view(), name='pinned-collection-list'), - path('collections/', MiddleVideoCollectionListView.as_view(), name='collection-list'), + path('categories/', cached_view(VideoCategoryListAPIView.as_view()), name='category-list'), + path('pinned-collections/', cached_view(PinnedVideoCollectionListView.as_view()), name='pinned-collection-list'), + path('collections/', cached_view(MiddleVideoCollectionListView.as_view()), name='collection-list'), - path('playlists/', VideoPlaylistListAPIView.as_view(), name='playlist-list'), + path('playlists/', conditional_cached_view(60 * 30, 'video_playlists')(VideoPlaylistListAPIView.as_view()), name='playlist-list'), re_path(r'playlists/(?P[\w-]+)/$', VideoPlaylistDetailAPIView.as_view(), name='playlist-detail'), # Keep old video endpoints for backward compatibility if needed - path('list/', VideoListAPIView.as_view(), name='video-list'), + path('list/', conditional_cached_view(60 * 30, 'video_list')(VideoListAPIView.as_view()), name='video-list'), re_path(r'detail/(?P[\w-]+)/$', VideoDetailAPIView.as_view(), name='video-detail'), path('resolve-youtube/', ResolveYouTubeStreamURLAPIView.as_view(), name='resolve-youtube'), ] diff --git a/apps/video/views.py b/apps/video/views.py index fef3b10..c6e03cb 100644 --- a/apps/video/views.py +++ b/apps/video/views.py @@ -85,38 +85,18 @@ class PinnedVideoCollectionListView(generics.ListAPIView): return super().get(request, *args, **kwargs) def get_queryset(self): - queryset = PinnedVideoCollection.objects.filter( + return PinnedVideoCollection.objects.filter( status=True, display_position=VideoCollection.DisplayPosition.PINNED, related_playlists__status=True, related_playlists__playlist_items__video__status=True ).distinct().order_by('order', '-created_at') - - # Filter by bookmarks if requested - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.VIDEO_PLAYLIST, - status=True - ).values_list('content_id', flat=True) - - # Only include collections that contain at least one bookmarked playlist - queryset = queryset.filter(related_playlists__id__in=bookmarked_ids).distinct() - - return queryset def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) categories_count = VideoCategory.objects.filter(status=True).count() - from apps.bookmark.models import Bookmark - bookmarks_count = Bookmark.objects.filter( - service=Bookmark.ServiceChoices.VIDEO, - ).count() info = { "categories_count": categories_count, - "bookmarks_count": bookmarks_count, } data = { "count": response.data.get("count"), @@ -167,24 +147,7 @@ class MiddleVideoCollectionListView(generics.ListAPIView): def list(self, request, *args, **kwargs): queryset = self.get_queryset() - - # Pass is_bookmark to serializer context - is_bookmark = request.query_params.get('is_bookmark', '').lower() - - # If is_bookmark=true, we filter the queryset to only include collections that - # have at least one bookmarked playlist - if is_bookmark == 'true' and request.user.is_authenticated: - from apps.bookmark.models import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=request.user, - service=Bookmark.ServiceChoices.VIDEO_PLAYLIST, - status=True - ).values_list('content_id', flat=True) - - # Filter collections that have any of these playlists - queryset = queryset.filter(related_playlists__id__in=bookmarked_ids).distinct() - - serializer = self.get_serializer(queryset, many=True, context={'request': request, 'is_bookmark': is_bookmark}) + serializer = self.get_serializer(queryset, many=True, context={'request': request}) return Response(serializer.data) @@ -261,20 +224,26 @@ class VideoPlaylistListAPIView(generics.ListAPIView): if collection_slug: queryset = queryset.filter(collections__slug=collection_slug) - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true': - # Import Bookmark model here to avoid circular imports + is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower() + if is_bookmark in ('true', '1') and self.request.user.is_authenticated: from apps.bookmark.models import Bookmark + from django.db.models import Q - # Get all bookmarked playlist IDs for the current user - bookmarked_ids = Bookmark.objects.filter( + bookmarked_playlist_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.VIDEO_PLAYLIST, status=True ).values_list('content_id', flat=True) + + bookmarked_video_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.VIDEO, + status=True + ).values_list('content_id', flat=True) - # Filter playlists by these IDs - queryset = queryset.filter(id__in=bookmarked_ids) + queryset = queryset.filter( + Q(id__in=bookmarked_playlist_ids) | Q(playlist_items__video__id__in=bookmarked_video_ids) + ).distinct() sort = self.request.query_params.get('sort', '-created_at') allowed_sorts = [ 'created_at', '-created_at', 'view_count', '-view_count', @@ -373,20 +342,26 @@ class VideoListAPIView(generics.ListAPIView): if collection_slug: queryset = queryset.filter(collections__slug=collection_slug) - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true': - # Import Bookmark model here to avoid circular imports + is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower() + if is_bookmark in ('true', '1') and self.request.user.is_authenticated: from apps.bookmark.models import Bookmark + from django.db.models import Q - # Get all bookmarked playlist IDs for the current user - bookmarked_ids = Bookmark.objects.filter( + bookmarked_playlist_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.VIDEO_PLAYLIST, status=True ).values_list('content_id', flat=True) + + bookmarked_video_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.VIDEO, + status=True + ).values_list('content_id', flat=True) - # Filter playlists by these IDs - queryset = queryset.filter(id__in=bookmarked_ids) + queryset = queryset.filter( + Q(id__in=bookmarked_playlist_ids) | Q(playlist_items__video__id__in=bookmarked_video_ids) + ).distinct() sort = self.request.query_params.get('sort', '-created_at') allowed_sorts = [ 'created_at', '-created_at', 'view_count', '-view_count',