Skip to content

Commit 8f330a0

Browse files
committed
Fall back to the plain detail layout when the carousel is confirmed empty
- carousel_supported now also checks a cheap cache peek (confirmed_empty): once a prior fetch found no trailer/photos for an item, later views skip the carousel grid entirely and render the same plain poster+text layout used by media types that never support a carousel (e.g. books), instead of reserving space for content that isn't coming. - On a cold cache (first-ever view), the carousel grid still renders and lazy-loads as before; a CSS fallback collapses it to the same poster-beside-text arrangement if that first fetch turns up empty. - Fixed a stray whitespace text node after the carousel fragment's {% endif %} that was defeating the CSS :empty check.
1 parent dc7a48a commit 8f330a0

7 files changed

Lines changed: 67 additions & 2 deletions

File tree

src/app/carousel.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
trailer or photo gallery.
99
"""
1010

11+
from django.core.cache import cache
12+
1113
from app.models import MediaTypes, Sources
1214
from app.providers import tmdb
1315

@@ -21,6 +23,22 @@ def carousel_supported(media_type, source):
2123
return bool(source == Sources.IGDB.value and media_type == MediaTypes.GAME.value)
2224

2325

26+
def confirmed_empty(media_type, source, media_id, *, season_number=None) -> bool:
27+
"""Return True if a prior fetch already confirmed there's no trailer/photos.
28+
29+
Cache peek only (never fetches), so a page whose carousel was already
30+
found empty on an earlier view can render the plain, non-carousel layout
31+
up front instead of paying for the lazy carousel round trip again.
32+
"""
33+
if source == Sources.TMDB.value and media_type in _TMDB_CAROUSEL_TYPES:
34+
data = tmdb.peek_carousel_media(media_type, media_id, season_number=season_number)
35+
elif source == Sources.IGDB.value and media_type == MediaTypes.GAME.value:
36+
data = cache.get(f"igdb_carousel_v2_{media_id}")
37+
else:
38+
return False
39+
return data is not None and not data["video"] and not data["photos"]
40+
41+
2442
def resolve_carousel_media(media_type, source, media_id, *, season_number=None) -> dict | None:
2543
"""Return {"video": {...}|None, "photos": [{"url", "thumb_url"}, ...]} or None."""
2644
if source == Sources.TMDB.value and media_type in _TMDB_CAROUSEL_TYPES:

src/app/media_details_views.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,10 @@ def media_details(
187187
)
188188

189189
detail_view_started_at = time.perf_counter()
190-
carousel_supported = carousel_media.carousel_supported(media_type, source)
190+
carousel_supported = carousel_media.carousel_supported(
191+
media_type,
192+
source,
193+
) and not carousel_media.confirmed_empty(media_type, source, media_id)
191194
render_secondary_only = (
192195
request.GET.get("fragment") == DETAIL_SECONDARY_FRAGMENT
193196
and media_type != MediaTypes.PODCAST.value

src/app/providers/tmdb.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,17 @@ def _parse_carousel_photos(response):
11331133
]
11341134

11351135

1136+
def peek_carousel_media(media_type, media_id, season_number=None):
1137+
"""Return the cached carousel payload, or None if nothing is cached yet.
1138+
1139+
Never triggers a provider fetch -- callers use this to check whether a
1140+
prior ``carousel_media`` call already confirmed there's no trailer/photos,
1141+
so the page can render the plain layout up front instead of paying for
1142+
the lazy carousel round trip again.
1143+
"""
1144+
return cache.get(_carousel_cache_key(media_type, media_id, season_number))
1145+
1146+
11361147
def carousel_media(media_type, media_id, season_number=None, language=None):
11371148
"""Return {"video": {...}|None, "photos": [...]} for the details carousel.
11381149

src/app/season_details_views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ def season_details(
8989
detail_view_started_at = time.perf_counter()
9090
carousel_supported = carousel_media.carousel_supported(
9191
MediaTypes.SEASON.value, source
92+
) and not carousel_media.confirmed_empty(
93+
MediaTypes.SEASON.value,
94+
source,
95+
media_id,
96+
season_number=season_number,
9297
)
9398
render_secondary_only = request.GET.get("fragment") == DETAIL_SECONDARY_FRAGMENT
9499
defer_detail_secondary = not render_secondary_only

src/static/css/input.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,22 @@ html.light .theme-toggle-icon-moon {
661661
.detail-carousel-grid__header {
662662
margin-top: 1.5rem;
663663
}
664+
665+
/* No trailer/photos to show (fragment swapped in empty): fall back to the
666+
pre-carousel arrangement, poster beside the title/chips/synopsis column
667+
instead of a carousel next to it, rather than stacking full-width rows
668+
below the poster and leaving its column dead. */
669+
.detail-carousel-grid:has(#detail-carousel-wrap:empty) {
670+
grid-template-areas: "poster header" "poster chips" "poster synopsis";
671+
}
672+
673+
.detail-carousel-grid:has(#detail-carousel-wrap:empty) .detail-carousel-grid__carousel {
674+
display: none;
675+
}
676+
677+
.detail-carousel-grid:has(#detail-carousel-wrap:empty) .detail-carousel-grid__header {
678+
margin-top: 0;
679+
}
664680
}
665681

666682
/* Mobile default: active playback card spans all columns on very small screens */

src/static/css/main.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2817,6 +2817,9 @@
28172817
.pl-5 {
28182818
padding-left: calc(var(--spacing) * 5);
28192819
}
2820+
.pl-6 {
2821+
padding-left: calc(var(--spacing) * 6);
2822+
}
28202823
.pl-7 {
28212824
padding-left: calc(var(--spacing) * 7);
28222825
}
@@ -6074,6 +6077,15 @@ html.light .theme-toggle-icon-moon {
60746077
.detail-carousel-grid__header {
60756078
margin-top: 1.5rem;
60766079
}
6080+
.detail-carousel-grid:has(#detail-carousel-wrap:empty) {
6081+
grid-template-areas: "poster header" "poster chips" "poster synopsis";
6082+
}
6083+
.detail-carousel-grid:has(#detail-carousel-wrap:empty) .detail-carousel-grid__carousel {
6084+
display: none;
6085+
}
6086+
.detail-carousel-grid:has(#detail-carousel-wrap:empty) .detail-carousel-grid__header {
6087+
margin-top: 0;
6088+
}
60776089
}
60786090
[data-active-playback-card] {
60796091
grid-column: span 3 !important;

src/templates/app/components/detail_carousel_fragment.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,4 @@
157157
</template>
158158
</div>
159159
</div>
160-
{% endif %}
160+
{% endif %}

0 commit comments

Comments
 (0)