|
47 | 47 | "season/{season}/watch/providers", |
48 | 48 | ) |
49 | 49 | TMDB_SEASON_CACHE_VERSION = 4 |
| 50 | +# Media-details carousel (trailer + photos): fetched lazily, its own cache |
| 51 | +# entry, independent of the movie/tv/season append_to_response payloads above. |
| 52 | +CAROUSEL_CACHE_TTL_SUCCESS = 60 * 60 * 24 * 7 |
| 53 | +CAROUSEL_CACHE_TTL_ABSENT = 60 * 60 * 24 |
50 | 54 |
|
51 | 55 |
|
52 | 56 | def base_params(language=None): |
@@ -1092,6 +1096,90 @@ def tv(media_id, language=None): |
1092 | 1096 | return data |
1093 | 1097 |
|
1094 | 1098 |
|
| 1099 | +def _carousel_cache_key(media_type, media_id, season_number=None): |
| 1100 | + """Return the cache key for a media-details carousel payload.""" |
| 1101 | + key = f"tmdb_carousel_{media_type}_{media_id}" |
| 1102 | + if season_number is not None: |
| 1103 | + key += f"_s{season_number}" |
| 1104 | + return key |
| 1105 | + |
| 1106 | + |
| 1107 | +def _parse_carousel_video(response): |
| 1108 | + """Return the single best YouTube trailer from a TMDB videos append, if any.""" |
| 1109 | + videos = response.get("videos", {}).get("results", []) or [] |
| 1110 | + youtube_videos = [v for v in videos if v.get("site") == "YouTube" and v.get("key")] |
| 1111 | + trailers = [v for v in youtube_videos if v.get("type") == "Trailer"] |
| 1112 | + video = next(iter(trailers), None) or next(iter(youtube_videos), None) |
| 1113 | + if not video: |
| 1114 | + return None |
| 1115 | + return { |
| 1116 | + "key": video["key"], |
| 1117 | + "name": video.get("name", ""), |
| 1118 | + "type": video.get("type", ""), |
| 1119 | + } |
| 1120 | + |
| 1121 | + |
| 1122 | +def _parse_carousel_photos(response): |
| 1123 | + """Return normalized backdrop photos from a TMDB images append.""" |
| 1124 | + backdrops = response.get("images", {}).get("backdrops", []) or [] |
| 1125 | + return [ |
| 1126 | + { |
| 1127 | + "file_path": photo["file_path"], |
| 1128 | + "width": photo.get("width"), |
| 1129 | + "height": photo.get("height"), |
| 1130 | + } |
| 1131 | + for photo in backdrops |
| 1132 | + if photo.get("file_path") |
| 1133 | + ] |
| 1134 | + |
| 1135 | + |
| 1136 | +def carousel_media(media_type, media_id, season_number=None, language=None): |
| 1137 | + """Return {"video": {...}|None, "photos": [...]} for the details carousel. |
| 1138 | +
|
| 1139 | + Fetched lazily via its own request/cache entry, never folded into the |
| 1140 | + movie/tv/season append_to_response calls (those are already close to |
| 1141 | + TMDB_APPEND_TO_RESPONSE_MAX_REMOTE_CALLS). |
| 1142 | + """ |
| 1143 | + cache_key = _carousel_cache_key(media_type, media_id, season_number) |
| 1144 | + data = cache.get(cache_key) |
| 1145 | + if data is not None: |
| 1146 | + return data |
| 1147 | + |
| 1148 | + if media_type == MediaTypes.MOVIE.value: |
| 1149 | + url = f"{base_url}/movie/{media_id}" |
| 1150 | + elif media_type == MediaTypes.SEASON.value: |
| 1151 | + url = f"{base_url}/tv/{media_id}/season/{season_number}" |
| 1152 | + else: |
| 1153 | + url = f"{base_url}/tv/{media_id}" |
| 1154 | + |
| 1155 | + params = { |
| 1156 | + **base_params(language), |
| 1157 | + "append_to_response": "videos,images", |
| 1158 | + } |
| 1159 | + |
| 1160 | + try: |
| 1161 | + response = services.api_request( |
| 1162 | + Sources.TMDB.value, |
| 1163 | + "GET", |
| 1164 | + url, |
| 1165 | + params=params, |
| 1166 | + ) |
| 1167 | + except requests.exceptions.HTTPError as error: |
| 1168 | + handle_error(error) |
| 1169 | + |
| 1170 | + data = { |
| 1171 | + "video": _parse_carousel_video(response), |
| 1172 | + "photos": _parse_carousel_photos(response), |
| 1173 | + } |
| 1174 | + ttl = ( |
| 1175 | + CAROUSEL_CACHE_TTL_SUCCESS |
| 1176 | + if data["video"] or data["photos"] |
| 1177 | + else CAROUSEL_CACHE_TTL_ABSENT |
| 1178 | + ) |
| 1179 | + cache.set(cache_key, data, ttl) |
| 1180 | + return data |
| 1181 | + |
| 1182 | + |
1095 | 1183 | def _build_tv_crew(response): |
1096 | 1184 | """Build sorted crew list for a TV show, prepending created_by as Creators.""" |
1097 | 1185 | crew = get_crew_credits(response.get("aggregate_credits", {}), is_aggregate=True) |
@@ -1566,6 +1654,13 @@ def get_profile_image_url(path, size="w185"): |
1566 | 1654 | return settings.IMG_NONE |
1567 | 1655 |
|
1568 | 1656 |
|
| 1657 | +def get_carousel_image_url(path, size="w1280"): |
| 1658 | + """Return a media-details carousel image URL (backdrops/photos).""" |
| 1659 | + if path: |
| 1660 | + return f"https://image.tmdb.org/t/p/{size}{path}" |
| 1661 | + return settings.IMG_NONE |
| 1662 | + |
| 1663 | + |
1569 | 1664 | def _upgrade_person_image_url(url): |
1570 | 1665 | """Upgrade legacy person profile URLs to a higher-resolution size.""" |
1571 | 1666 | if not url: |
|
0 commit comments