Resolve poster URL and/or Logo id for a recording program. Callers should enrich the program dict via _match_epg_program_by_timeslot before invoking this function so that EPG data is already available. Pipeline: EPG images → VOD logo → TMDB/OMDb → TVMaze/iTunes → direct program fie
(channel_name, program, channel_logo_id=None)
| 2984 | _ws('error', {"reason": str(e)}) |
| 2985 | return f"error:{e}" |
| 2986 | def _resolve_poster_for_program(channel_name, program, channel_logo_id=None): |
| 2987 | """Resolve poster URL and/or Logo id for a recording program. |
| 2988 | |
| 2989 | Callers should enrich the program dict via _match_epg_program_by_timeslot |
| 2990 | before invoking this function so that EPG data is already available. |
| 2991 | |
| 2992 | Pipeline: EPG images → VOD logo → TMDB/OMDb → TVMaze/iTunes → |
| 2993 | direct program fields → Logo table → Logo creation → channel logo. |
| 2994 | Returns (poster_logo_id, poster_url) where either may be None. |
| 2995 | """ |
| 2996 | poster_logo_id = None |
| 2997 | poster_url = None |
| 2998 | epg_props = None |
| 2999 | |
| 3000 | _title = ((program.get("title") if isinstance(program, dict) else None) or "").strip() or None |
| 3001 | |
| 3002 | # Guard: if the "title" is really just the channel name (common when EPG |
| 3003 | # has no real program data), don't use it for external API searches — |
| 3004 | # those queries produce false-positive artwork from unrelated shows. |
| 3005 | _title_is_channel_name = False |
| 3006 | if _title and channel_name: |
| 3007 | def _norm_channel(s): |
| 3008 | return s.lower().replace("*", "").replace("-", " ").strip() |
| 3009 | _title_is_channel_name = _norm_channel(_title) == _norm_channel(channel_name) |
| 3010 | |
| 3011 | # Stage 1: EPG Program images/icon (with URL validation) |
| 3012 | try: |
| 3013 | from apps.epg.models import ProgramData |
| 3014 | prog_id = program.get("id") if isinstance(program, dict) else None |
| 3015 | if prog_id: |
| 3016 | epg_program = ProgramData.objects.filter(id=prog_id).only("custom_properties").first() |
| 3017 | if epg_program and epg_program.custom_properties: |
| 3018 | epg_props = epg_program.custom_properties or {} |
| 3019 | poster_url = _pick_best_image_from_epg_props(epg_props) |
| 3020 | if poster_url and not _validate_url(poster_url): |
| 3021 | poster_url = None |
| 3022 | if not poster_url: |
| 3023 | icon = epg_props.get("icon") |
| 3024 | if isinstance(icon, str) and icon and _validate_url(icon): |
| 3025 | poster_url = icon |
| 3026 | except Exception: |
| 3027 | pass |
| 3028 | |
| 3029 | # Stage 2: VOD logo fallback by title |
| 3030 | if not poster_url and not poster_logo_id and _title and not _title_is_channel_name: |
| 3031 | try: |
| 3032 | from apps.vod.models import Movie, Series |
| 3033 | vod_logo = None |
| 3034 | movie = Movie.objects.filter(name__iexact=_title).select_related("logo").first() |
| 3035 | if movie and movie.logo: |
| 3036 | vod_logo = movie.logo |
| 3037 | if not vod_logo: |
| 3038 | series = Series.objects.filter(name__iexact=_title).select_related("logo").first() |
| 3039 | if series and series.logo: |
| 3040 | vod_logo = series.logo |
| 3041 | if vod_logo: |
| 3042 | poster_logo_id = vod_logo.id |
| 3043 | except Exception: |