Browse Source

perf(caching): add redis and conditional caching across apps, fix bookmark filtering

- Add Redis caching (2h TTL) to categories, collections, and pinned collections in video, library, article, podcast, and dobodbi_calendar (6h TTL)
- Implement conditional_cached_view (30m TTL) for list endpoints (books, podcasts, articles, videos, playlists) that caches public queries and bypasses caching when is_bookmark=true or is_bookmarked=true
- Remove bookmark/user count queries from pinned collection list views to avoid user-specific cache pollution
- Prevent user bookmark/rate leakage into public catalog cache in BookListView via AnonymousUser context wrapper
- Fix video bookmark filtering bug in VideoPlaylistListAPIView and VideoListAPIView to accept both is_bookmark and is_bookmarked, and query both VIDEO_PLAYLIST and VIDEO bookmarks
- Add assign_random_videos_to_playlists management command
master
Mohsen Taba 2 weeks ago
parent
commit
59ad3cd064
  1. 28
      apps/article/urls.py
  2. 7
      apps/article/views.py
  3. 12
      apps/dobodbi_calendar/urls.py
  4. 2
      apps/library/serializers.py
  5. 31
      apps/library/urls.py
  6. 23
      apps/library/views.py
  7. 29
      apps/podcast/urls.py
  8. 10
      apps/podcast/views.py
  9. 126
      apps/video/management/commands/assign_random_videos_to_playlists.py
  10. 13
      apps/video/serializers.py
  11. 30
      apps/video/urls.py
  12. 81
      apps/video/views.py

28
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/<str:slug>/', ArticleDetailAPIView.as_view(), name='article-detail'),
re_path(r'detail/(?P<slug>.+)/$', ArticleDetailAPIView.as_view(), name='podcast-detail'),
path('admin/', include(admin_router.urls)),

7
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

12
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())),
]

2
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 {

31
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/<str:slug>/', AuthorDetailView.as_view(), name='author-detail'),
path('authors/<str:slug>/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/<str:slug>/', 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'),

23
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

29
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<slug>[\w-]+)/$', PodcastDetailAPIView.as_view(), name='podcast-detail'),
# User playlist endpoints

10
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

126
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.'
)
)

13
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

30
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<slug>[\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<slug>[\w-]+)/$', VideoDetailAPIView.as_view(), name='video-detail'),
path('resolve-youtube/', ResolveYouTubeStreamURLAPIView.as_view(), name='resolve-youtube'),
]

81
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',

Loading…
Cancel
Save