Fixed some vod apis, converted duration to duration_secs, add custom_poperties to vod_episode

This commit is contained in:
SergeantPanda 2025-08-09 16:31:33 -05:00
parent 3ac84e530a
commit 4d7987214b
7 changed files with 238 additions and 131 deletions

View file

@ -1099,7 +1099,7 @@ def xc_get_vod_streams(request, user, category_id=None):
),
#'stream_icon': movie.logo.url if movie.logo else '',
"rating": movie.rating or "0",
"rating_5based": float(movie.rating or 0) / 2 if movie.rating else 0,
"rating_5based": round(float(movie.rating or 0) / 2, 2) if movie.rating else 0,
"added": str(movie.created_at.timestamp()),
"is_adult": 0,
"tmdb_id": movie.tmdb_id or "",
@ -1197,17 +1197,18 @@ def xc_get_series(request, user, category_id=None):
)
),
"plot": series.description or "",
"cast": "",
"director": "",
"cast": series.custom_properties.get('cast', '') if series.custom_properties else "",
"director": series.custom_properties.get('director', '') if series.custom_properties else "",
"genre": series.genre or "",
"release_date": str(series.year) if series.year else "",
"release_date": series.custom_properties.get('release_date', str(series.year) if series.year else "") if series.custom_properties else (str(series.year) if series.year else ""),
"last_modified": int(relation.updated_at.timestamp()),
"rating": series.rating or "0",
"rating_5based": float(series.rating or 0) / 2 if series.rating else 0,
"backdrop_path": [],
"youtube_trailer": "",
"episode_run_time": "",
"rating_5based": round(float(series.rating or 0) / 2, 2) if series.rating else 0,
"backdrop_path": series.custom_properties.get('backdrop_path', []) if series.custom_properties else [],
"youtube_trailer": series.custom_properties.get('youtube_trailer', '') if series.custom_properties else "",
"episode_run_time": series.custom_properties.get('episode_run_time', '') if series.custom_properties else "",
"category_id": str(relation.category.id) if relation.category else "0",
"category_ids": [int(relation.category.id)] if relation.category else [],
})
return series_list
@ -1241,6 +1242,31 @@ def xc_get_series_info(request, user, series_id):
except M3USeriesRelation.DoesNotExist:
raise Http404()
# Check if we need to refresh detailed info (similar to vod api_views pattern)
try:
should_refresh = (
not series_relation.last_episode_refresh or
series_relation.last_episode_refresh < timezone.now() - timedelta(hours=24)
)
# Check if detailed data has been fetched
custom_props = series_relation.custom_properties or {}
episodes_fetched = custom_props.get('episodes_fetched', False)
detailed_fetched = custom_props.get('detailed_fetched', False)
# Force refresh if episodes/details have never been fetched or time interval exceeded
if not episodes_fetched or not detailed_fetched or should_refresh:
from apps.vod.tasks import refresh_series_episodes
account = series_relation.m3u_account
if account and account.is_active:
refresh_series_episodes(account, series, series_relation.external_series_id)
# Refresh objects from database after task completion
series.refresh_from_db()
series_relation.refresh_from_db()
except Exception as e:
logger.error(f"Error refreshing series data for relation {series_relation.id}: {str(e)}")
# Get episodes for this series from the same M3U account
episode_relations = M3UEpisodeRelation.objects.filter(
episode__series=series,
@ -1284,11 +1310,63 @@ def xc_get_series_info(request, user, series_id):
}
})
# Build response
# Build response using potentially refreshed data
series_data = {
'name': series.name,
'description': series.description or '',
'year': series.year,
'genre': series.genre or '',
'rating': series.rating or '0',
'cast': '',
'director': '',
'youtube_trailer': '',
'episode_run_time': '',
'backdrop_path': [],
}
# Add detailed info from custom_properties if available
try:
if series.custom_properties:
custom_data = series.custom_properties
series_data.update({
'cast': custom_data.get('cast', ''),
'director': custom_data.get('director', ''),
'youtube_trailer': custom_data.get('youtube_trailer', ''),
'episode_run_time': custom_data.get('episode_run_time', ''),
'backdrop_path': custom_data.get('backdrop_path', []),
})
# Check relation custom_properties for detailed_info
if series_relation.custom_properties and 'detailed_info' in series_relation.custom_properties:
detailed_info = series_relation.custom_properties['detailed_info']
# Override with detailed_info values where available
for key in ['name', 'description', 'year', 'genre', 'rating']:
if detailed_info.get(key):
series_data[key] = detailed_info[key]
# Handle plot vs description
if detailed_info.get('plot'):
series_data['description'] = detailed_info['plot']
elif detailed_info.get('description'):
series_data['description'] = detailed_info['description']
# Update additional fields from detailed info
series_data.update({
'cast': detailed_info.get('cast', series_data['cast']),
'director': detailed_info.get('director', series_data['director']),
'youtube_trailer': detailed_info.get('youtube_trailer', series_data['youtube_trailer']),
'episode_run_time': detailed_info.get('episode_run_time', series_data['episode_run_time']),
'backdrop_path': detailed_info.get('backdrop_path', series_data['backdrop_path']),
})
except Exception as e:
logger.error(f"Error parsing series custom_properties: {str(e)}")
info = {
"seasons": list(seasons.keys()),
"info": {
"name": series.name,
"name": series_data['name'],
"cover": (
None if not series.logo
else build_absolute_uri_with_port(
@ -1296,17 +1374,17 @@ def xc_get_series_info(request, user, series_id):
reverse("api:channels:logo-cache", args=[series.logo.id])
)
),
"plot": series.description or "",
"cast": "",
"director": "",
"genre": series.genre or "",
"release_date": str(series.year) if series.year else "",
"plot": series_data['description'],
"cast": series_data['cast'],
"director": series_data['director'],
"genre": series_data['genre'],
"release_date": str(series_data['year']) if series_data['year'] else "",
"last_modified": int(series_relation.updated_at.timestamp()),
"rating": series.rating or "0",
"rating_5based": float(series.rating or 0) / 2 if series.rating else 0,
"backdrop_path": [],
"youtube_trailer": "",
"episode_run_time": "",
"rating": series_data['rating'],
"rating_5based": round(float(series_data['rating'] or 0) / 2, 2) if series_data['rating'] else 0,
"backdrop_path": series_data['backdrop_path'],
"youtube_trailer": series_data['youtube_trailer'],
"episode_run_time": series_data['episode_run_time'],
"category_id": str(series_relation.category.id) if series_relation.category else "0",
},
"episodes": dict(seasons)

View file

@ -22,7 +22,7 @@ class SeriesAdmin(admin.ModelAdmin):
@admin.register(Movie)
class MovieAdmin(admin.ModelAdmin):
list_display = ['name', 'year', 'genre', 'duration', 'created_at']
list_display = ['name', 'year', 'genre', 'duration_secs', 'created_at']
list_filter = ['year', 'created_at']
search_fields = ['name', 'description', 'tmdb_id', 'imdb_id']
readonly_fields = ['uuid', 'created_at', 'updated_at']
@ -33,7 +33,7 @@ class MovieAdmin(admin.ModelAdmin):
@admin.register(Episode)
class EpisodeAdmin(admin.ModelAdmin):
list_display = ['name', 'series', 'season_number', 'episode_number', 'duration', 'created_at']
list_display = ['name', 'series', 'season_number', 'episode_number', 'duration_secs', 'created_at']
list_filter = ['series', 'season_number', 'created_at']
search_fields = ['name', 'description', 'series__name']
readonly_fields = ['uuid', 'created_at', 'updated_at']

View file

@ -77,14 +77,6 @@ class MovieViewSet(viewsets.ReadOnlyModelViewSet):
serializer = M3UMovieRelationSerializer(relations, many=True)
return Response(serializer.data)
def _convert_duration_to_minutes(self, duration_secs):
"""Convert duration from seconds to minutes"""
if not duration_secs:
return 0
try:
return int(duration_secs) // 60
except (ValueError, TypeError):
return 0
@action(detail=True, methods=['get'], url_path='provider-info')
def provider_info(self, request, pk=None):
@ -144,8 +136,7 @@ class MovieViewSet(viewsets.ReadOnlyModelViewSet):
'tmdb_id': movie.tmdb_id or info.get('tmdb_id', ''),
'imdb_id': movie.imdb_id or info.get('imdb_id', ''),
'youtube_trailer': (movie.custom_properties or {}).get('youtube_trailer') or info.get('youtube_trailer') or info.get('trailer', ''),
'duration': movie.duration or (int(info.get('duration_secs', 0)) // 60 if info.get('duration_secs') else None),
'duration_secs': info.get('duration_secs', (movie.duration or 0) * 60),
'duration_secs': movie.duration_secs or info.get('duration_secs'),
'age': info.get('age', ''),
'backdrop_path': (movie.custom_properties or {}).get('backdrop_path') or info.get('backdrop_path', []),
'cover': info.get('cover_big', ''),
@ -365,7 +356,7 @@ class SeriesViewSet(viewsets.ReadOnlyModelViewSet):
'description': episode.description,
'air_date': episode.air_date,
'plot': episode.description,
'duration': episode.duration,
'duration_secs': episode.duration_secs,
'rating': episode.rating,
'tmdb_id': episode.tmdb_id,
'imdb_id': episode.imdb_id,

View file

@ -0,0 +1,16 @@
# Generated by Django A.I. on 2025-08-09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vod', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='episode',
name='custom_properties',
field=models.JSONField(blank=True, null=True, help_text='Custom properties for this episode'),
),
]

View file

@ -0,0 +1,31 @@
# Generated by Django A.I. on 2025-08-09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vod', '0002_episode_custom_properties'),
]
operations = [
migrations.RenameField(
model_name='movie',
old_name='duration',
new_name='duration_secs',
),
migrations.AlterField(
model_name='movie',
name='duration_secs',
field=models.IntegerField(blank=True, null=True, help_text='Duration in seconds'),
),
migrations.RenameField(
model_name='episode',
old_name='duration',
new_name='duration_secs',
),
migrations.AlterField(
model_name='episode',
name='duration_secs',
field=models.IntegerField(blank=True, null=True, help_text='Duration in seconds'),
),
]

View file

@ -78,7 +78,7 @@ class Movie(models.Model):
year = models.IntegerField(blank=True, null=True)
rating = models.CharField(max_length=10, blank=True, null=True)
genre = models.CharField(max_length=255, blank=True, null=True)
duration = models.IntegerField(blank=True, null=True, help_text="Duration in minutes")
duration_secs = models.IntegerField(blank=True, null=True, help_text="Duration in seconds")
logo = models.ForeignKey(Logo, on_delete=models.SET_NULL, null=True, blank=True)
# Metadata IDs for deduplication
@ -113,7 +113,7 @@ class Episode(models.Model):
description = models.TextField(blank=True, null=True)
air_date = models.DateField(blank=True, null=True)
rating = models.CharField(max_length=10, blank=True, null=True)
duration = models.IntegerField(blank=True, null=True, help_text="Duration in minutes")
duration_secs = models.IntegerField(blank=True, null=True, help_text="Duration in seconds")
# Episode specific fields
series = models.ForeignKey(Series, on_delete=models.CASCADE, related_name='episodes')
@ -124,6 +124,9 @@ class Episode(models.Model):
tmdb_id = models.CharField(max_length=50, blank=True, null=True, help_text="TMDB ID for metadata", db_index=True)
imdb_id = models.CharField(max_length=50, blank=True, null=True, help_text="IMDB ID for metadata", db_index=True)
# Custom properties for episode
custom_properties = models.JSONField(blank=True, null=True, help_text="Custom properties for this episode")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

View file

@ -120,26 +120,26 @@ def process_movie_basic(client, account, movie_data, category):
# Extract trailer
trailer = movie_data.get('trailer') or movie_data.get('youtube_trailer') or ''
duration_minutes = None
duration_secs = None
# Try to extract duration from various possible fields
if movie_data.get('duration_secs'):
duration_minutes = convert_duration_to_minutes(movie_data.get('duration_secs'))
duration_secs = int(movie_data.get('duration_secs'))
elif movie_data.get('duration'):
# Handle duration that might be in different formats
duration_str = str(movie_data.get('duration'))
if duration_str.isdigit():
duration_minutes = int(duration_str) # Assume minutes if just a number
duration_secs = int(duration_str) * 60 # Assume minutes if just a number
else:
# Try to parse time format like "01:30:00"
try:
time_parts = duration_str.split(':')
if len(time_parts) == 3:
hours, minutes, seconds = map(int, time_parts)
duration_minutes = (hours * 60) + minutes
duration_secs = (hours * 3600) + (minutes * 60) + seconds
elif len(time_parts) == 2:
minutes, seconds = map(int, time_parts)
duration_minutes = minutes
duration_secs = minutes * 60 + seconds
except (ValueError, AttributeError):
pass
@ -148,7 +148,7 @@ def process_movie_basic(client, account, movie_data, category):
'plot': description,
'rating': rating,
'genre': genre,
'duration_secs': movie_data.get('duration_secs'),
'duration_secs': duration_secs,
'trailer': trailer,
}
@ -213,35 +213,15 @@ def process_series_basic(client, account, series_data, category):
tmdb_id = series_data.get('tmdb') or series_data.get('tmdb_id')
imdb_id = series_data.get('imdb') or series_data.get('imdb_id')
# Extract additional metadata that matches the actual API response
description = series_data.get('plot') or series_data.get('description') or series_data.get('overview') or ''
rating = series_data.get('rating') or series_data.get('vote_average') or ''
genre = series_data.get('genre') or ''
# Extract trailer
youtube_trailer = series_data.get('trailer') or series_data.get('youtube_trailer') or ''
# Extract backdrop path
backdrop_path = series_data.get('backdrop_path') or ''
# Build info dict with all extracted data
info = {
'plot': description,
'rating': rating,
'genre': genre,
'youtube_trailer': youtube_trailer,
'backdrop_path': backdrop_path,
}
# Use find_or_create_series to handle duplicates properly
logger.debug(f"Processing series: {name} ({year})")
series = find_or_create_series(
name=name,
year=year,
tmdb_id=tmdb_id,
imdb_id=imdb_id,
info=info
info=series_data
)
# Handle logo from basic data if available
if series_data.get('cover'):
logo, _ = Logo.objects.get_or_create(
@ -362,7 +342,7 @@ def process_episode(account, series, episode_data, season_number):
'description': description,
'rating': rating,
'air_date': air_date,
'duration': convert_duration_to_minutes(info.get('duration_secs')),
'duration_secs': info.get('duration_secs'),
'tmdb_id': info.get('tmdb_id'),
'imdb_id': info.get('imdb_id'),
}
@ -433,9 +413,9 @@ def find_or_create_movie(name, year, tmdb_id, imdb_id, info):
movie.imdb_id = imdb_id
updated = True
duration = convert_duration_to_minutes(info.get('duration_secs'))
if duration and duration != movie.duration:
movie.duration = duration
duration_secs = info.get('duration_secs')
if duration_secs and duration_secs != movie.duration_secs:
movie.duration_secs = duration_secs
updated = True
# Update custom_properties with trailer and other metadata
@ -471,7 +451,7 @@ def find_or_create_movie(name, year, tmdb_id, imdb_id, info):
description=info.get('plot') or info.get('description', ''),
rating=info.get('rating', ''),
genre=info.get('genre', ''),
duration=convert_duration_to_minutes(info.get('duration_secs')),
duration_secs=info.get('duration_secs'),
custom_properties=custom_props if custom_props else None
)
@ -479,7 +459,7 @@ def find_or_create_movie(name, year, tmdb_id, imdb_id, info):
def find_or_create_series(name, year, tmdb_id, imdb_id, info):
"""Find existing series or create new one based on metadata"""
series = None
updated = False
# Try to find by TMDB ID first
if tmdb_id:
series = Series.objects.filter(tmdb_id=tmdb_id).first()
@ -496,56 +476,9 @@ def find_or_create_series(name, year, tmdb_id, imdb_id, info):
if not series:
series = Series.objects.filter(name=name).first()
# If we found an existing series, update it
if series:
updated = False
if info.get('plot') and info.get('plot') != series.description:
series.description = info.get('plot')
updated = True
if info.get('rating') and info.get('rating') != series.rating:
series.rating = info.get('rating')
updated = True
if info.get('genre') and info.get('genre') != series.genre:
series.genre = info.get('genre')
updated = True
if year and year != series.year:
series.year = year
updated = True
if tmdb_id and tmdb_id != series.tmdb_id:
series.tmdb_id = tmdb_id
updated = True
if imdb_id and imdb_id != series.imdb_id:
series.imdb_id = imdb_id
updated = True
# Update custom_properties with trailer and other metadata
custom_props = series.custom_properties or {}
custom_props_updated = False
if info.get('trailer') and info.get('trailer') != custom_props.get('youtube_trailer'):
custom_props['youtube_trailer'] = info.get('trailer')
custom_props_updated = True
if info.get('youtube_trailer') and info.get('youtube_trailer') != custom_props.get('youtube_trailer'):
custom_props['youtube_trailer'] = info.get('youtube_trailer')
custom_props_updated = True
if info.get('backdrop_path') and info.get('backdrop_path') != custom_props.get('backdrop_path'):
custom_props['backdrop_path'] = info.get('backdrop_path')
custom_props_updated = True
if custom_props_updated:
series.custom_properties = custom_props
updated = True
if updated:
series.save()
return series
# Create new series if not found
custom_props = {}
if info.get('youtube_trailer'):
custom_props['youtube_trailer'] = info.get('youtube_trailer')
if info.get('backdrop_path'):
custom_props['backdrop_path'] = info.get('backdrop_path')
return Series.objects.create(
# If still not found, create a new series
if not series:
series = Series.objects.create(
name=name,
year=year,
tmdb_id=tmdb_id,
@ -553,9 +486,72 @@ def find_or_create_series(name, year, tmdb_id, imdb_id, info):
description=info.get('plot', ''),
rating=info.get('rating', ''),
genre=info.get('genre', ''),
custom_properties=custom_props if custom_props else None
)
# Update series metadata
if info.get('plot') and info.get('plot') != series.description:
series.description = info.get('plot')
updated = True
if info.get('rating') and info.get('rating') != series.rating:
series.rating = info.get('rating')
updated = True
if info.get('genre') and info.get('genre') != series.genre:
series.genre = info.get('genre')
updated = True
if year and year != series.year:
series.year = year
updated = True
if tmdb_id and tmdb_id != series.tmdb_id:
series.tmdb_id = tmdb_id
updated = True
if imdb_id and imdb_id != series.imdb_id:
series.imdb_id = imdb_id
updated = True
# Update custom_properties with trailer and other metadata
custom_props = series.custom_properties or {}
custom_props_updated = False
if info.get('trailer') and info.get('trailer') != custom_props.get('youtube_trailer'):
custom_props['youtube_trailer'] = info.get('trailer')
custom_props_updated = True
if info.get('youtube_trailer') and info.get('youtube_trailer') != custom_props.get('youtube_trailer'):
custom_props['youtube_trailer'] = info.get('youtube_trailer')
custom_props_updated = True
if info.get('backdrop_path') and info.get('backdrop_path') != custom_props.get('backdrop_path'):
custom_props['backdrop_path'] = info.get('backdrop_path')
custom_props_updated = True
if info.get('episode_run_time') and info.get('episode_run_time') != custom_props.get('episode_run_time'):
custom_props['episode_run_time'] = info.get('episode_run_time')
custom_props_updated = True
if info.get('cast') and info.get('cast') != custom_props.get('cast'):
custom_props['cast'] = info.get('cast')
custom_props_updated = True
if info.get('director') and info.get('director') != custom_props.get('director'):
custom_props['director'] = info.get('director')
custom_props_updated = True
if (
(info.get('release_date') and info.get('release_date') != custom_props.get('release_date')) or
(info.get('releaseDate') and info.get('releaseDate') != custom_props.get('release_date')) or
(info.get('releasedate') and info.get('releasedate') != custom_props.get('release_date'))
):
# Prefer release_date, then releaseDate, then releasedate
release_date_val = (
info.get('release_date') or
info.get('releaseDate') or
info.get('releasedate')
)
custom_props['release_date'] = release_date_val
custom_props_updated = True
if not year and custom_props.get('release_date'):
year = extract_year(custom_props.get('release_date'))
updated = True
if custom_props_updated:
series.custom_properties = custom_props
updated = True
if updated:
series.save()
return series
def extract_year(date_string):
"""Extract year from date string"""
@ -628,14 +624,6 @@ def extract_year_from_data(data, title_key='name'):
return None
def convert_duration_to_minutes(duration_secs):
"""Convert duration from seconds to minutes"""
if not duration_secs:
return None
try:
return int(duration_secs) // 60
except (ValueError, TypeError):
return None
@shared_task
def cleanup_orphaned_vod_content():
@ -727,9 +715,9 @@ def refresh_movie_advanced_data(m3u_movie_relation_id, force_refresh=False):
movie.genre = info.get('genre')
updated = True
if info.get('duration_secs'):
duration = int(info.get('duration_secs')) // 60
if duration != movie.duration:
movie.duration = duration
duration_secs = int(info.get('duration_secs'))
if duration_secs != movie.duration_secs:
movie.duration_secs = duration_secs
updated = True
# Check for releasedate or release_date
release_date_value = info.get('releasedate') or info.get('release_date')