mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Fix VOD page not displaying correct order while changing pages.
This commit is contained in:
parent
f8e91155e2
commit
5e661ea208
6 changed files with 380 additions and 157 deletions
|
|
@ -5,6 +5,7 @@ from .api_views import (
|
||||||
EpisodeViewSet,
|
EpisodeViewSet,
|
||||||
SeriesViewSet,
|
SeriesViewSet,
|
||||||
VODCategoryViewSet,
|
VODCategoryViewSet,
|
||||||
|
UnifiedContentViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
app_name = 'vod'
|
app_name = 'vod'
|
||||||
|
|
@ -14,5 +15,6 @@ router.register(r'movies', MovieViewSet, basename='movie')
|
||||||
router.register(r'episodes', EpisodeViewSet, basename='episode')
|
router.register(r'episodes', EpisodeViewSet, basename='episode')
|
||||||
router.register(r'series', SeriesViewSet, basename='series')
|
router.register(r'series', SeriesViewSet, basename='series')
|
||||||
router.register(r'categories', VODCategoryViewSet, basename='vodcategory')
|
router.register(r'categories', VODCategoryViewSet, basename='vodcategory')
|
||||||
|
router.register(r'all', UnifiedContentViewSet, basename='unified-content')
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls
|
||||||
|
|
|
||||||
|
|
@ -469,3 +469,203 @@ class VODCategoryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
return [perm() for perm in permission_classes_by_action[self.action]]
|
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return [Authenticated()]
|
return [Authenticated()]
|
||||||
|
|
||||||
|
|
||||||
|
class UnifiedContentViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
|
"""ViewSet that combines Movies and Series for unified 'All' view"""
|
||||||
|
queryset = Movie.objects.none() # Empty queryset, we override list method
|
||||||
|
serializer_class = MovieSerializer # Default serializer, overridden in list
|
||||||
|
pagination_class = VODPagination
|
||||||
|
|
||||||
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
|
search_fields = ['name', 'description', 'genre']
|
||||||
|
ordering_fields = ['name', 'year', 'created_at']
|
||||||
|
ordering = ['name']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
try:
|
||||||
|
return [perm() for perm in permission_classes_by_action[self.action]]
|
||||||
|
except KeyError:
|
||||||
|
return [Authenticated()]
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
"""Override list to handle unified content properly - database-level approach"""
|
||||||
|
import logging
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error("=== UnifiedContentViewSet.list() called ===")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get pagination parameters
|
||||||
|
page_size = int(request.query_params.get('page_size', 24))
|
||||||
|
page_number = int(request.query_params.get('page', 1))
|
||||||
|
|
||||||
|
logger.error(f"Page {page_number}, page_size {page_size}")
|
||||||
|
|
||||||
|
# Calculate offset for unified pagination
|
||||||
|
offset = (page_number - 1) * page_size
|
||||||
|
|
||||||
|
# For high page numbers, use raw SQL for efficiency
|
||||||
|
# This avoids loading and sorting massive amounts of data in Python
|
||||||
|
|
||||||
|
search = request.query_params.get('search', '')
|
||||||
|
category = request.query_params.get('category', '')
|
||||||
|
|
||||||
|
# Build WHERE clauses
|
||||||
|
where_conditions = [
|
||||||
|
# Only active content
|
||||||
|
"movies.id IN (SELECT DISTINCT movie_id FROM vod_m3umovierelation mmr JOIN m3u_m3uaccount ma ON mmr.m3u_account_id = ma.id WHERE ma.is_active = true)",
|
||||||
|
"series.id IN (SELECT DISTINCT series_id FROM vod_m3useriesrelation msr JOIN m3u_m3uaccount ma ON msr.m3u_account_id = ma.id WHERE ma.is_active = true)"
|
||||||
|
]
|
||||||
|
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if search:
|
||||||
|
where_conditions[0] += " AND LOWER(movies.name) LIKE %s"
|
||||||
|
where_conditions[1] += " AND LOWER(series.name) LIKE %s"
|
||||||
|
search_param = f"%{search.lower()}%"
|
||||||
|
params.extend([search_param, search_param])
|
||||||
|
|
||||||
|
if category:
|
||||||
|
if '|' in category:
|
||||||
|
cat_name, cat_type = category.split('|', 1)
|
||||||
|
if cat_type == 'movie':
|
||||||
|
where_conditions[0] += " AND movies.id IN (SELECT movie_id FROM vod_m3umovierelation mmr JOIN vod_vodcategory c ON mmr.category_id = c.id WHERE c.name = %s)"
|
||||||
|
where_conditions[1] = "1=0" # Exclude series
|
||||||
|
params.append(cat_name)
|
||||||
|
elif cat_type == 'series':
|
||||||
|
where_conditions[1] += " AND series.id IN (SELECT series_id FROM vod_m3useriesrelation msr JOIN vod_vodcategory c ON msr.category_id = c.id WHERE c.name = %s)"
|
||||||
|
where_conditions[0] = "1=0" # Exclude movies
|
||||||
|
params.append(cat_name)
|
||||||
|
else:
|
||||||
|
where_conditions[0] += " AND movies.id IN (SELECT movie_id FROM vod_m3umovierelation mmr JOIN vod_vodcategory c ON mmr.category_id = c.id WHERE c.name = %s)"
|
||||||
|
where_conditions[1] += " AND series.id IN (SELECT series_id FROM vod_m3useriesrelation msr JOIN vod_vodcategory c ON msr.category_id = c.id WHERE c.name = %s)"
|
||||||
|
params.extend([category, category])
|
||||||
|
|
||||||
|
# Use UNION ALL with ORDER BY and LIMIT/OFFSET for true unified pagination
|
||||||
|
# This is much more efficient than Python sorting
|
||||||
|
sql = f"""
|
||||||
|
WITH unified_content AS (
|
||||||
|
SELECT
|
||||||
|
movies.id,
|
||||||
|
movies.uuid,
|
||||||
|
movies.name,
|
||||||
|
movies.description,
|
||||||
|
movies.year,
|
||||||
|
movies.rating,
|
||||||
|
movies.genre,
|
||||||
|
movies.duration_secs as duration,
|
||||||
|
movies.created_at,
|
||||||
|
movies.updated_at,
|
||||||
|
movies.custom_properties,
|
||||||
|
movies.logo_id,
|
||||||
|
logo.name as logo_name,
|
||||||
|
logo.url as logo_url,
|
||||||
|
'movie' as content_type
|
||||||
|
FROM vod_movie movies
|
||||||
|
LEFT JOIN dispatcharr_channels_logo logo ON movies.logo_id = logo.id
|
||||||
|
WHERE {where_conditions[0]}
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
series.id,
|
||||||
|
series.uuid,
|
||||||
|
series.name,
|
||||||
|
series.description,
|
||||||
|
series.year,
|
||||||
|
series.rating,
|
||||||
|
series.genre,
|
||||||
|
NULL as duration,
|
||||||
|
series.created_at,
|
||||||
|
series.updated_at,
|
||||||
|
series.custom_properties,
|
||||||
|
series.logo_id,
|
||||||
|
logo.name as logo_name,
|
||||||
|
logo.url as logo_url,
|
||||||
|
'series' as content_type
|
||||||
|
FROM vod_series series
|
||||||
|
LEFT JOIN dispatcharr_channels_logo logo ON series.logo_id = logo.id
|
||||||
|
WHERE {where_conditions[1]}
|
||||||
|
)
|
||||||
|
SELECT * FROM unified_content
|
||||||
|
ORDER BY LOWER(name), id
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
params.extend([page_size, offset])
|
||||||
|
|
||||||
|
logger.error(f"Executing SQL with LIMIT {page_size} OFFSET {offset}")
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(sql, params)
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
item_dict = dict(zip(columns, row))
|
||||||
|
|
||||||
|
# Build logo object in the format expected by frontend
|
||||||
|
logo_data = None
|
||||||
|
if item_dict['logo_id']:
|
||||||
|
logo_data = {
|
||||||
|
'id': item_dict['logo_id'],
|
||||||
|
'name': item_dict['logo_name'],
|
||||||
|
'url': item_dict['logo_url'],
|
||||||
|
'cache_url': f"/media/logo_cache/{item_dict['logo_id']}.png" if item_dict['logo_id'] else None,
|
||||||
|
'channel_count': 0, # We don't need this for VOD
|
||||||
|
'is_used': True,
|
||||||
|
'channel_names': [] # We don't need this for VOD
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert to the format expected by frontend
|
||||||
|
formatted_item = {
|
||||||
|
'id': item_dict['id'],
|
||||||
|
'uuid': str(item_dict['uuid']),
|
||||||
|
'name': item_dict['name'],
|
||||||
|
'description': item_dict['description'] or '',
|
||||||
|
'year': item_dict['year'],
|
||||||
|
'rating': float(item_dict['rating']) if item_dict['rating'] else 0.0,
|
||||||
|
'genre': item_dict['genre'] or '',
|
||||||
|
'duration': item_dict['duration'],
|
||||||
|
'created_at': item_dict['created_at'].isoformat() if item_dict['created_at'] else None,
|
||||||
|
'updated_at': item_dict['updated_at'].isoformat() if item_dict['updated_at'] else None,
|
||||||
|
'custom_properties': item_dict['custom_properties'] or {},
|
||||||
|
'logo': logo_data,
|
||||||
|
'content_type': item_dict['content_type']
|
||||||
|
}
|
||||||
|
results.append(formatted_item)
|
||||||
|
|
||||||
|
logger.error(f"Retrieved {len(results)} results via SQL")
|
||||||
|
|
||||||
|
# Get total count estimate (for pagination info)
|
||||||
|
# Use a separate efficient count query
|
||||||
|
count_sql = f"""
|
||||||
|
SELECT COUNT(*) FROM (
|
||||||
|
SELECT 1 FROM vod_movie movies WHERE {where_conditions[0]}
|
||||||
|
UNION ALL
|
||||||
|
SELECT 1 FROM vod_series series WHERE {where_conditions[1]}
|
||||||
|
) as total_count
|
||||||
|
"""
|
||||||
|
|
||||||
|
count_params = params[:-2] # Remove LIMIT and OFFSET params
|
||||||
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(count_sql, count_params)
|
||||||
|
total_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
'count': total_count,
|
||||||
|
'next': offset + page_size < total_count,
|
||||||
|
'previous': page_number > 1,
|
||||||
|
'results': results
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in UnifiedContentViewSet.list(): {e}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response({'error': str(e)}, status=500)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .api_views import MovieViewSet, EpisodeViewSet, SeriesViewSet, VODCategoryViewSet, VODConnectionViewSet
|
from .api_views import MovieViewSet, EpisodeViewSet, SeriesViewSet, VODCategoryViewSet, UnifiedContentViewSet
|
||||||
|
|
||||||
app_name = 'vod'
|
app_name = 'vod'
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ router.register(r'movies', MovieViewSet)
|
||||||
router.register(r'episodes', EpisodeViewSet)
|
router.register(r'episodes', EpisodeViewSet)
|
||||||
router.register(r'series', SeriesViewSet)
|
router.register(r'series', SeriesViewSet)
|
||||||
router.register(r'categories', VODCategoryViewSet)
|
router.register(r'categories', VODCategoryViewSet)
|
||||||
router.register(r'connections', VODConnectionViewSet)
|
router.register(r'all', UnifiedContentViewSet, basename='unified-content')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('api/', include(router.urls)),
|
path('api/', include(router.urls)),
|
||||||
|
|
|
||||||
|
|
@ -2077,7 +2077,14 @@ export default class API {
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorNotification('Failed to retrieve movies', e);
|
// Don't show error notification for "Invalid page" errors as they're handled gracefully
|
||||||
|
const isInvalidPage = e.body?.detail?.includes('Invalid page') ||
|
||||||
|
e.message?.includes('Invalid page');
|
||||||
|
|
||||||
|
if (!isInvalidPage) {
|
||||||
|
errorNotification('Failed to retrieve movies', e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2088,7 +2095,39 @@ export default class API {
|
||||||
);
|
);
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorNotification('Failed to retrieve series', e);
|
// Don't show error notification for "Invalid page" errors as they're handled gracefully
|
||||||
|
const isInvalidPage = e.body?.detail?.includes('Invalid page') ||
|
||||||
|
e.message?.includes('Invalid page');
|
||||||
|
|
||||||
|
if (!isInvalidPage) {
|
||||||
|
errorNotification('Failed to retrieve series', e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getAllContent(params = new URLSearchParams()) {
|
||||||
|
try {
|
||||||
|
console.log('Calling getAllContent with URL:', `${host}/api/vod/all/?${params.toString()}`);
|
||||||
|
const response = await request(
|
||||||
|
`${host}/api/vod/all/?${params.toString()}`
|
||||||
|
);
|
||||||
|
console.log('getAllContent raw response:', response);
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('getAllContent error:', e);
|
||||||
|
console.error('Error status:', e.status);
|
||||||
|
console.error('Error body:', e.body);
|
||||||
|
console.error('Error message:', e.message);
|
||||||
|
|
||||||
|
// Don't show error notification for "Invalid page" errors as they're handled gracefully
|
||||||
|
const isInvalidPage = e.body?.detail?.includes('Invalid page') ||
|
||||||
|
e.message?.includes('Invalid page');
|
||||||
|
|
||||||
|
if (!isInvalidPage) {
|
||||||
|
errorNotification('Failed to retrieve content', e);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -264,8 +264,7 @@ const useCardColumns = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const VODsPage = () => {
|
const VODsPage = () => {
|
||||||
const movies = useVODStore((s) => s.movies);
|
const currentPageContent = useVODStore((s) => s.currentPageContent); // Direct subscription
|
||||||
const series = useVODStore((s) => s.series);
|
|
||||||
const allCategories = useVODStore((s) => s.categories);
|
const allCategories = useVODStore((s) => s.categories);
|
||||||
const filters = useVODStore((s) => s.filters);
|
const filters = useVODStore((s) => s.filters);
|
||||||
const currentPage = useVODStore((s) => s.currentPage);
|
const currentPage = useVODStore((s) => s.currentPage);
|
||||||
|
|
@ -288,8 +287,7 @@ const VODsPage = () => {
|
||||||
setPageSize(Number(value));
|
setPageSize(Number(value));
|
||||||
localStorage.setItem('vodsPageSize', value);
|
localStorage.setItem('vodsPageSize', value);
|
||||||
};
|
};
|
||||||
const fetchMovies = useVODStore((s) => s.fetchMovies);
|
const fetchContent = useVODStore((s) => s.fetchContent);
|
||||||
const fetchSeries = useVODStore((s) => s.fetchSeries);
|
|
||||||
const fetchCategories = useVODStore((s) => s.fetchCategories);
|
const fetchCategories = useVODStore((s) => s.fetchCategories);
|
||||||
|
|
||||||
// const showVideo = useVideoStore((s) => s.showVideo); - removed as unused
|
// const showVideo = useVideoStore((s) => s.showVideo); - removed as unused
|
||||||
|
|
@ -307,36 +305,10 @@ const VODsPage = () => {
|
||||||
|
|
||||||
// Helper function to get display data based on current filters
|
// Helper function to get display data based on current filters
|
||||||
const getDisplayData = () => {
|
const getDisplayData = () => {
|
||||||
if (filters.type === 'series') {
|
return (currentPageContent || []).map((item) => ({
|
||||||
return Object.values(series).map((item) => ({
|
...item,
|
||||||
...item,
|
_vodType: item.contentType === 'movie' ? 'movie' : 'series',
|
||||||
_vodType: 'series',
|
}));
|
||||||
}));
|
|
||||||
} else if (filters.type === 'movies') {
|
|
||||||
return Object.values(movies).map((item) => ({
|
|
||||||
...item,
|
|
||||||
_vodType: 'movie',
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// 'all' - combine movies and series, tagging each with its type, then sort alphabetically by name/title
|
|
||||||
const combined = [
|
|
||||||
...Object.values(movies).map((item) => ({
|
|
||||||
...item,
|
|
||||||
_vodType: 'movie',
|
|
||||||
})),
|
|
||||||
...Object.values(series).map((item) => ({
|
|
||||||
...item,
|
|
||||||
_vodType: 'series',
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
return combined.sort((a, b) => {
|
|
||||||
const nameA = (a.name || a.title || '').toLowerCase();
|
|
||||||
const nameB = (b.name || b.title || '').toLowerCase();
|
|
||||||
if (nameA < nameB) return -1;
|
|
||||||
if (nameA > nameB) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -360,17 +332,8 @@ const VODsPage = () => {
|
||||||
}, [fetchCategories]);
|
}, [fetchCategories]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (filters.type === 'series') {
|
fetchContent().finally(() => setInitialLoad(false));
|
||||||
fetchSeries().finally(() => setInitialLoad(false));
|
}, [filters, currentPage, pageSize, fetchContent]);
|
||||||
} else if (filters.type === 'movies') {
|
|
||||||
fetchMovies().finally(() => setInitialLoad(false));
|
|
||||||
} else {
|
|
||||||
// 'all': fetch both movies and series
|
|
||||||
Promise.all([fetchMovies(), fetchSeries()]).finally(() =>
|
|
||||||
setInitialLoad(false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [filters, currentPage, pageSize, fetchMovies, fetchSeries]);
|
|
||||||
|
|
||||||
const handleVODCardClick = (vod) => {
|
const handleVODCardClick = (vod) => {
|
||||||
setSelectedVOD(vod);
|
setSelectedVOD(vod);
|
||||||
|
|
@ -464,46 +427,25 @@ const VODsPage = () => {
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{filters.type === 'series' ? (
|
<Grid gutter="md">
|
||||||
<Grid gutter="md">
|
{getDisplayData().map((item) => (
|
||||||
{Object.values(series).map((seriesItem) => (
|
<Grid.Col
|
||||||
<Grid.Col
|
span={12 / columns}
|
||||||
span={12 / columns}
|
key={`${item.contentType}_${item.id}`}
|
||||||
key={seriesItem.id}
|
style={{
|
||||||
style={{
|
minWidth: MIN_CARD_WIDTH,
|
||||||
minWidth: MIN_CARD_WIDTH,
|
maxWidth: MAX_CARD_WIDTH,
|
||||||
maxWidth: MAX_CARD_WIDTH,
|
margin: '0 auto',
|
||||||
margin: '0 auto',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{item.contentType === 'series' ? (
|
||||||
<SeriesCard
|
<SeriesCard series={item} onClick={handleSeriesClick} />
|
||||||
series={seriesItem}
|
) : (
|
||||||
onClick={handleSeriesClick}
|
<VODCard vod={item} onClick={handleVODCardClick} />
|
||||||
/>
|
)}
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
) : (
|
|
||||||
<Grid gutter="md">
|
|
||||||
{getDisplayData().map((item) => (
|
|
||||||
<Grid.Col
|
|
||||||
span={12 / columns}
|
|
||||||
key={item.id}
|
|
||||||
style={{
|
|
||||||
minWidth: MIN_CARD_WIDTH,
|
|
||||||
maxWidth: MAX_CARD_WIDTH,
|
|
||||||
margin: '0 auto',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item._vodType === 'series' ? (
|
|
||||||
<SeriesCard series={item} onClick={handleSeriesClick} />
|
|
||||||
) : (
|
|
||||||
<VODCard vod={item} onClick={handleVODCardClick} />
|
|
||||||
)}
|
|
||||||
</Grid.Col>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { create } from 'zustand';
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
const useVODStore = create((set, get) => ({
|
const useVODStore = create((set, get) => ({
|
||||||
movies: {},
|
content: {}, // Store for individual content details (when fetching movie/series details)
|
||||||
series: {},
|
currentPageContent: [], // Store the current page's results
|
||||||
episodes: {},
|
episodes: {},
|
||||||
categories: {},
|
categories: {},
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
@ -34,12 +34,12 @@ const useVODStore = create((set, get) => ({
|
||||||
currentPage: 1, // Reset to first page when page size changes
|
currentPage: 1, // Reset to first page when page size changes
|
||||||
})),
|
})),
|
||||||
|
|
||||||
fetchMovies: async () => {
|
fetchContent: async () => {
|
||||||
try {
|
try {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
const state = get();
|
const state = get();
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
params.append('page', state.currentPage);
|
params.append('page', state.currentPage);
|
||||||
params.append('page_size', state.pageSize);
|
params.append('page_size', state.pageSize);
|
||||||
|
|
||||||
|
|
@ -51,60 +51,62 @@ const useVODStore = create((set, get) => ({
|
||||||
params.append('category', state.filters.category);
|
params.append('category', state.filters.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await api.getMovies(params);
|
let allResults = [];
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
// Handle both paginated and non-paginated responses
|
if (state.filters.type === 'movies') {
|
||||||
const results = response.results || response;
|
// Fetch only movies
|
||||||
const count = response.count || results.length;
|
const response = await api.getMovies(params);
|
||||||
|
const results = response.results || response;
|
||||||
|
allResults = results.map((item) => ({ ...item, contentType: 'movie' }));
|
||||||
|
totalCount = response.count || results.length;
|
||||||
|
} else if (state.filters.type === 'series') {
|
||||||
|
// Fetch only series
|
||||||
|
const response = await api.getSeries(params);
|
||||||
|
const results = response.results || response;
|
||||||
|
allResults = results.map((item) => ({
|
||||||
|
...item,
|
||||||
|
contentType: 'series',
|
||||||
|
}));
|
||||||
|
totalCount = response.count || results.length;
|
||||||
|
} else {
|
||||||
|
// Use the new unified backend endpoint for 'all' view
|
||||||
|
const response = await api.getAllContent(params);
|
||||||
|
console.log('getAllContent response:', response);
|
||||||
|
console.log('response type:', typeof response);
|
||||||
|
console.log(
|
||||||
|
'response keys:',
|
||||||
|
response ? Object.keys(response) : 'no response'
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = response.results || response;
|
||||||
|
console.log('results:', results);
|
||||||
|
console.log('results type:', typeof results);
|
||||||
|
console.log('results is array:', Array.isArray(results));
|
||||||
|
|
||||||
|
// Check if results is actually an array before calling map
|
||||||
|
if (!Array.isArray(results)) {
|
||||||
|
console.error('Results is not an array:', results);
|
||||||
|
throw new Error('Invalid response format - results is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// The backend already provides content_type and proper sorting/pagination
|
||||||
|
allResults = results.map((item) => ({
|
||||||
|
...item,
|
||||||
|
contentType: item.content_type, // Backend provides this field
|
||||||
|
}));
|
||||||
|
totalCount = response.count || results.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the current page results directly (don't accumulate all pages)
|
||||||
set({
|
set({
|
||||||
movies: results.reduce((acc, movie) => {
|
currentPageContent: allResults, // This is the paginated data for current page
|
||||||
acc[movie.id] = movie;
|
totalCount,
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
totalCount: count,
|
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch movies:', error);
|
console.error('Failed to fetch content:', error);
|
||||||
set({ error: 'Failed to load movies.', loading: false });
|
set({ error: 'Failed to load content.', loading: false });
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchSeries: async () => {
|
|
||||||
set({ loading: true, error: null });
|
|
||||||
try {
|
|
||||||
const state = get();
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
params.append('page', state.currentPage);
|
|
||||||
params.append('page_size', state.pageSize);
|
|
||||||
|
|
||||||
if (state.filters.search) {
|
|
||||||
params.append('search', state.filters.search);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.filters.category) {
|
|
||||||
params.append('category', state.filters.category);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await api.getSeries(params);
|
|
||||||
|
|
||||||
// Handle both paginated and non-paginated responses
|
|
||||||
const results = response.results || response;
|
|
||||||
const count = response.count || results.length;
|
|
||||||
|
|
||||||
set({
|
|
||||||
series: results.reduce((acc, series) => {
|
|
||||||
acc[series.id] = series;
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
totalCount: count,
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch series:', error);
|
|
||||||
set({ error: 'Failed to load series.', loading: false });
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -158,9 +160,12 @@ const useVODStore = create((set, get) => ({
|
||||||
};
|
};
|
||||||
console.log('Fetched Movie Details:', movieDetails);
|
console.log('Fetched Movie Details:', movieDetails);
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
movies: {
|
content: {
|
||||||
...state.movies,
|
...state.content,
|
||||||
[movieDetails.id]: movieDetails,
|
[`movie_${movieDetails.id}`]: {
|
||||||
|
...movieDetails,
|
||||||
|
contentType: 'movie',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
}));
|
}));
|
||||||
|
|
@ -261,36 +266,48 @@ const useVODStore = create((set, get) => ({
|
||||||
|
|
||||||
addMovie: (movie) =>
|
addMovie: (movie) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
movies: { ...state.movies, [movie.id]: movie },
|
content: {
|
||||||
|
...state.content,
|
||||||
|
[`movie_${movie.id}`]: { ...movie, contentType: 'movie' },
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
|
|
||||||
updateMovie: (movie) =>
|
updateMovie: (movie) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
movies: { ...state.movies, [movie.id]: movie },
|
content: {
|
||||||
|
...state.content,
|
||||||
|
[`movie_${movie.id}`]: { ...movie, contentType: 'movie' },
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
|
|
||||||
removeMovie: (movieId) =>
|
removeMovie: (movieId) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const updatedMovies = { ...state.movies };
|
const updatedContent = { ...state.content };
|
||||||
delete updatedMovies[movieId];
|
delete updatedContent[`movie_${movieId}`];
|
||||||
return { movies: updatedMovies };
|
return { content: updatedContent };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
addSeries: (series) =>
|
addSeries: (series) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
series: { ...state.series, [series.id]: series },
|
content: {
|
||||||
|
...state.content,
|
||||||
|
[`series_${series.id}`]: { ...series, contentType: 'series' },
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
|
|
||||||
updateSeries: (series) =>
|
updateSeries: (series) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
series: { ...state.series, [series.id]: series },
|
content: {
|
||||||
|
...state.content,
|
||||||
|
[`series_${series.id}`]: { ...series, contentType: 'series' },
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
|
|
||||||
removeSeries: (seriesId) =>
|
removeSeries: (seriesId) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const updatedSeries = { ...state.series };
|
const updatedContent = { ...state.content };
|
||||||
delete updatedSeries[seriesId];
|
delete updatedContent[`series_${seriesId}`];
|
||||||
return { series: updatedSeries };
|
return { content: updatedContent };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
fetchSeriesInfo: async (seriesId) => {
|
fetchSeriesInfo: async (seriesId) => {
|
||||||
|
|
@ -369,9 +386,9 @@ const useVODStore = create((set, get) => ({
|
||||||
}
|
}
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
series: {
|
content: {
|
||||||
...state.series,
|
...state.content,
|
||||||
[seriesInfo.id]: seriesInfo,
|
[`series_${seriesInfo.id}`]: { ...seriesInfo, contentType: 'series' },
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
}));
|
}));
|
||||||
|
|
@ -387,6 +404,29 @@ const useVODStore = create((set, get) => ({
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Helper methods for getting filtered content
|
||||||
|
getFilteredContent: () => {
|
||||||
|
const state = get();
|
||||||
|
// Return the current page content directly - backend handles all filtering/pagination
|
||||||
|
return state.currentPageContent;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMovies: () => {
|
||||||
|
const state = get();
|
||||||
|
return Object.values(state.content).filter(
|
||||||
|
(item) => item.contentType === 'movie'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getSeries: () => {
|
||||||
|
const state = get();
|
||||||
|
return Object.values(state.content).filter(
|
||||||
|
(item) => item.contentType === 'series'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearContent: () => set({ content: {}, totalCount: 0 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default useVODStore;
|
export default useVODStore;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue