From 93886b20ea2bf02335634260b42db7a5d494250f Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 17 Sep 2026 23:34:15 +0200 Subject: [PATCH] Surface video detail load failures in the error view for placeholder videos Opening a members-only video from a history or feed row got a 500 from the videos API and left the detail view blank: the retry error view only covered the videoID init mode, while the details fetch for a loaded video failed silently. Record the details-fetch error and route it into the same error view when the displayed video is a bare placeholder (no author, no thumbnails), show a spinner while that fetch is in flight, and make retry re-run both loaders. Videos with real partial metadata keep rendering as before. --- Yattee/Views/Video/VideoInfoView.swift | 29 ++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index 6cdf221e..4761f066 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -108,6 +108,7 @@ struct VideoInfoView: View { // Video details cache - stores full video details loaded from API @State private var loadedVideoDetails: [String: Video] = [:] @State private var isLoadingVideoDetails = false + @State private var videoDetailsLoadError: String? /// Combined video list including originally loaded videos and extended videos from continuation private var allVideos: [Video]? { guard let originalList = videoQueueContext?.videoList else { return nil } @@ -133,6 +134,14 @@ struct VideoInfoView: View { return loadedVideoDetails[base.id.videoID] ?? base } + /// Whether the displayed video is only an ID standing in for metadata + /// (a server-history row whose hydration failed), so a failed details + /// fetch leaves nothing worth rendering. + private var displayedVideoIsPlaceholder: Bool { + guard let video = displayedVideo else { return false } + return video.author.name.isEmpty && video.thumbnails.isEmpty + } + private var accentColor: Color { appEnvironment?.settingsManager.resolvedAccentColor ?? .accentColor } @@ -251,11 +260,13 @@ struct VideoInfoView: View { var body: some View { Group { - if isLoadingInitialVideo { + if isLoadingInitialVideo || (displayedVideoIsPlaceholder && isLoadingVideoDetails) { ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let error = initialVideoLoadError { videoLoadErrorView(error) + } else if let error = videoDetailsLoadError, displayedVideoIsPlaceholder { + videoLoadErrorView(error) } else if displayedVideo != nil { videoContent } @@ -326,6 +337,7 @@ struct VideoInfoView: View { comments = [] commentsState = .idle commentsContinuation = nil + videoDetailsLoadError = nil scrollViewID = UUID() // Reset scroll position // Don't reset scrollOffset here - let the new scroll view report its geometry // Resetting to 0 causes the blur to jump to an incorrect position briefly @@ -775,7 +787,9 @@ struct VideoInfoView: View { } #endif - /// Error view shown when video fails to load (videoID init mode only). + /// Error view shown when the video fails to load — either the initial + /// fetch in videoID mode, or the details fetch for a placeholder video + /// that has no metadata of its own to fall back on. @ViewBuilder private func videoLoadErrorView(_ message: String) -> some View { ContentUnavailableView { @@ -786,6 +800,7 @@ struct VideoInfoView: View { Button(String(localized: "common.retry")) { Task { await loadInitialVideoIfNeeded() + await loadVideoDetails() } } } @@ -2168,6 +2183,7 @@ struct VideoInfoView: View { } isLoadingVideoDetails = true + videoDetailsLoadError = nil do { // Use extractURL method - just use the video part let (fullVideo, _, _) = try await contentService.extractURL(originalURL, instance: instance) @@ -2176,7 +2192,9 @@ struct VideoInfoView: View { loadedVideoDetails[videoID] = fullVideo CachedChannelData.cacheAuthor(fullVideo.author) } catch { - // Fail silently - use partial video data we have + // Use the partial video data we have; the body surfaces the + // error only when the video is a bare placeholder. + videoDetailsLoadError = error.localizedDescription } isLoadingVideoDetails = false return @@ -2191,6 +2209,7 @@ struct VideoInfoView: View { } isLoadingVideoDetails = true + videoDetailsLoadError = nil do { let fullVideo = try await contentService.video( @@ -2202,7 +2221,9 @@ struct VideoInfoView: View { loadedVideoDetails[videoID] = fullVideo CachedChannelData.cacheAuthor(fullVideo.author) } catch { - // Fail silently - just use the partial video data we have + // Use the partial video data we have; the body surfaces the + // error only when the video is a bare placeholder. + videoDetailsLoadError = error.localizedDescription } isLoadingVideoDetails = false