From f08a30e303f1684de7ad1954c491c415810e7a8d Mon Sep 17 00:00:00 2001 From: nemesbak Date: Thu, 28 May 2026 13:39:05 +0200 Subject: [PATCH 1/3] fix(vod): import director, actors and release_date from XC provider sync get_vod_streams on XC providers supplies director, cast/actors and release_date for each movie, but process_movie_batch was only persisting trailer in custom_properties. As a result those fields were always empty in Dispatcharr's own get_vod_info / provider-info responses even when the upstream provider returned correct data. Changes: - Extract director, actors (also mapped from 'cast') and release_date from movie_data during the batch-import phase and store them in custom_properties alongside the existing trailer field. - Fix the update path to merge incoming custom_properties into the existing dict (using {**existing, **incoming}) rather than overwriting it wholesale, so that detailed_info and other keys written by the advanced refresh task are preserved across subsequent basic syncs. Fixes #1228 Co-Authored-By: Claude Sonnet 4.6 --- apps/vod/tasks.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/apps/vod/tasks.py b/apps/vod/tasks.py index ff195fc9..78ad7389 100644 --- a/apps/vod/tasks.py +++ b/apps/vod/tasks.py @@ -439,6 +439,28 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N trailer = extract_string_from_array_or_string(trailer_raw) if trailer_raw else None logo_url = movie_data.get('stream_icon') or '' + # Extract optional metadata fields supplied by the XC provider in + # get_vod_streams. Stored in custom_properties so that the + # provider-info endpoint (and the XC vod_info passthrough) can + # return them without a separate per-movie API call. + director = extract_string_from_array_or_string( + movie_data.get('director') or '' + ) + actors = extract_string_from_array_or_string( + movie_data.get('actors') or movie_data.get('cast') or '' + ) + release_date = movie_data.get('release_date') or movie_data.get('releasedate') or '' + + custom_props = {} + if trailer: + custom_props['trailer'] = trailer + if director: + custom_props['director'] = director + if actors: + custom_props['actors'] = actors + if release_date: + custom_props['release_date'] = release_date + movie_props = { 'name': name, 'year': year, @@ -448,7 +470,7 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N 'rating': rating, 'genre': genre, 'duration_secs': duration_secs, - 'custom_properties': {'trailer': trailer} if trailer else None, + 'custom_properties': custom_props or None, } movie_keys[movie_key] = { @@ -548,8 +570,14 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N for field, value in movie_props.items(): if field == 'custom_properties': - if value != movie.custom_properties: - movie.custom_properties = value + # Merge incoming props into the existing dict so that + # detailed_info and other keys written by advanced refresh + # are not lost when a basic sync runs afterwards. + existing_cp = movie.custom_properties or {} + incoming_cp = value or {} + merged = {**existing_cp, **incoming_cp} + if merged != existing_cp: + movie.custom_properties = merged updated = True elif getattr(movie, field) != value: setattr(movie, field, value) From ac84916974745ac4a7ff37c952b3ca8cbbca93d0 Mon Sep 17 00:00:00 2001 From: nemesbak Date: Sat, 30 May 2026 01:06:58 +0200 Subject: [PATCH 2/3] fix(vod): preserve advanced-refresh metadata and fix youtube_trailer key - Use 'youtube_trailer' key (matching api_views.py and advanced refresh) instead of orphaned 'trailer' key that was never consumed. - On basic sync, only write director/actors/release_date to custom_properties when the field is currently empty. This prevents basic sync from overwriting richer data previously stored by refresh_movie_advanced_data, while still populating those fields for movies that have never had an advanced refresh run. Tested: logic verified against all three scenarios (preserve existing rich data, populate empty fields, youtube_trailer key propagation). Co-Authored-By: Claude Sonnet 4.6 --- apps/vod/tasks.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/vod/tasks.py b/apps/vod/tasks.py index 78ad7389..b74ffcba 100644 --- a/apps/vod/tasks.py +++ b/apps/vod/tasks.py @@ -453,7 +453,7 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N custom_props = {} if trailer: - custom_props['trailer'] = trailer + custom_props['youtube_trailer'] = trailer if director: custom_props['director'] = director if actors: @@ -573,9 +573,18 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N # Merge incoming props into the existing dict so that # detailed_info and other keys written by advanced refresh # are not lost when a basic sync runs afterwards. + # For metadata fields (director/actors/release_date), only + # write if the field is empty — advanced refresh may have + # stored richer data that basic sync must not overwrite. existing_cp = movie.custom_properties or {} incoming_cp = value or {} - merged = {**existing_cp, **incoming_cp} + merged = dict(existing_cp) + for k, v in incoming_cp.items(): + if k in ('director', 'actors', 'release_date'): + if not existing_cp.get(k): + merged[k] = v + else: + merged[k] = v if merged != existing_cp: movie.custom_properties = merged updated = True From fc867b7f7779596ebbdeab2b6d10df26c67139d2 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Fri, 29 May 2026 21:18:16 -0500 Subject: [PATCH 3/3] fix(vod): enhance metadata extraction for actors and preserve existing values --- apps/vod/tasks.py | 49 ++++++++++++++++++++++------------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/apps/vod/tasks.py b/apps/vod/tasks.py index b74ffcba..367baff3 100644 --- a/apps/vod/tasks.py +++ b/apps/vod/tasks.py @@ -439,16 +439,14 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N trailer = extract_string_from_array_or_string(trailer_raw) if trailer_raw else None logo_url = movie_data.get('stream_icon') or '' - # Extract optional metadata fields supplied by the XC provider in - # get_vod_streams. Stored in custom_properties so that the - # provider-info endpoint (and the XC vod_info passthrough) can - # return them without a separate per-movie API call. director = extract_string_from_array_or_string( movie_data.get('director') or '' ) - actors = extract_string_from_array_or_string( - movie_data.get('actors') or movie_data.get('cast') or '' - ) + actors_raw = movie_data.get('actors') or movie_data.get('cast') or '' + if isinstance(actors_raw, list): + actors = ', '.join(s.strip() for s in actors_raw if s and str(s).strip()) or None + else: + actors = actors_raw.strip() if actors_raw else None release_date = movie_data.get('release_date') or movie_data.get('releasedate') or '' custom_props = {} @@ -570,12 +568,7 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N for field, value in movie_props.items(): if field == 'custom_properties': - # Merge incoming props into the existing dict so that - # detailed_info and other keys written by advanced refresh - # are not lost when a basic sync runs afterwards. - # For metadata fields (director/actors/release_date), only - # write if the field is empty — advanced refresh may have - # stored richer data that basic sync must not overwrite. + # Merge: preserve advanced-refresh keys; don't overwrite director/actors/release_date if already set. existing_cp = movie.custom_properties or {} incoming_cp = value or {} merged = dict(existing_cp) @@ -601,8 +594,6 @@ def process_movie_batch(account, batch, categories, relations, scan_start_time=N movie._logo_to_update = new_logo logo_updated = True elif movie.logo_id: - # Logo URL exists but logo creation failed or logo not found - # Clear the orphaned logo reference logger.warning(f"Logo URL provided but logo not found in database for movie '{movie.name}', clearing logo reference") movie._logo_to_update = None logo_updated = True @@ -821,7 +812,14 @@ def process_series_batch(account, batch, categories, relations, scan_start_time= value = series_data.get(key) if value: # For string-like fields that might be arrays, extract clean strings - if key in ['poster_path', 'youtube_trailer', 'cast', 'director']: + if key == 'cast': + if isinstance(value, list): + clean_value = ', '.join(s.strip() for s in value if s and str(s).strip()) or None + else: + clean_value = extract_string_from_array_or_string(value) + if clean_value: + additional_metadata[key] = clean_value + elif key in ['poster_path', 'youtube_trailer', 'director']: clean_value = extract_string_from_array_or_string(value) if clean_value: additional_metadata[key] = clean_value @@ -2193,26 +2191,25 @@ def refresh_movie_advanced_data(m3u_movie_relation_id, force_refresh=False): movie.imdb_id = imdb_id_to_set updated = True logger.debug(f"Set imdb_id {imdb_id_to_set} on movie {movie.id}") - # Only update trailer if we have a non-empty value and either no existing value or existing value is empty if should_update_field(custom_props.get('youtube_trailer'), info.get('trailer')): custom_props['youtube_trailer'] = extract_string_from_array_or_string(info.get('trailer')) updated = True if should_update_field(custom_props.get('youtube_trailer'), info.get('youtube_trailer')): custom_props['youtube_trailer'] = extract_string_from_array_or_string(info.get('youtube_trailer')) updated = True - # Only update backdrop_path if we have a non-empty value and either no existing value or existing value is empty if should_update_field(custom_props.get('backdrop_path'), info.get('backdrop_path')): backdrop_url = extract_string_from_array_or_string(info.get('backdrop_path')) custom_props['backdrop_path'] = [backdrop_url] if backdrop_url else None updated = True - # Only update actors if we have a non-empty value and either no existing value or existing value is empty - if should_update_field(custom_props.get('actors'), info.get('actors')): - custom_props['actors'] = extract_string_from_array_or_string(info.get('actors')) - updated = True - if should_update_field(custom_props.get('actors'), info.get('cast')): - custom_props['actors'] = extract_string_from_array_or_string(info.get('cast')) - updated = True - # Only update director if we have a non-empty value and either no existing value or existing value is empty + for actors_key in ('actors', 'cast'): + actors_raw = info.get(actors_key) + if should_update_field(custom_props.get('actors'), actors_raw): + if isinstance(actors_raw, list): + custom_props['actors'] = ', '.join(s.strip() for s in actors_raw if s and str(s).strip()) or None + else: + custom_props['actors'] = extract_string_from_array_or_string(actors_raw) + updated = True + break if should_update_field(custom_props.get('director'), info.get('director')): custom_props['director'] = extract_string_from_array_or_string(info.get('director')) updated = True