From 93240b43142a1f855a2a809e8406ce41a22755b8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 4 May 2026 08:01:51 +0200 Subject: [PATCH] Wire Yattee Server playback through /proxy/relay when proxiesVideos is on The Sources -> Edit Source -> Proxy toggle now renders for Yattee Server entries (supportsVideoProxying gains .yatteeServer). When the toggle is on, playback fetches go through videoWithProxyStreamsAndCaptionsAndStoryboards with mode .relay, so the server returns signed /proxy/relay URLs (byte-relay, supports HTTP Range, no on-disk caching). Downloads keep going through mode .download so the server-side /proxy/fast/ flow continues to cache files for repeat use. InvidiousAPI.proxyStreamsIfNeeded early-returns for .yatteeServer since proxying is now done at fetch time via ?proxy=true rather than client-side host rewriting. --- Yattee/Models/Instance.swift | 2 +- Yattee/Services/API/ContentService.swift | 23 +++++++++++----- Yattee/Services/API/InvidiousAPI.swift | 4 +++ Yattee/Services/API/YatteeServerAPI.swift | 33 ++++++++++++++++------- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/Yattee/Models/Instance.swift b/Yattee/Models/Instance.swift index 90ebfa96..6d81a2de 100644 --- a/Yattee/Models/Instance.swift +++ b/Yattee/Models/Instance.swift @@ -180,7 +180,7 @@ extension Instance { /// Whether this instance supports proxying video streams through itself. var supportsVideoProxying: Bool { - type == .invidious || type == .piped + type == .invidious || type == .piped || type == .yatteeServer } } diff --git a/Yattee/Services/API/ContentService.swift b/Yattee/Services/API/ContentService.swift index 2eabd494..a163839e 100644 --- a/Yattee/Services/API/ContentService.swift +++ b/Yattee/Services/API/ContentService.swift @@ -243,21 +243,23 @@ actor ContentService: ContentServiceProtocol { try await api(for: instance).channelSearch(id: id, query: query, instance: instance, page: page) } - /// Fetches streams with proxy URLs for faster LAN downloads (Yattee Server only). - /// For other backends, returns regular streams. + /// Fetches streams with proxy URLs for downloads (Yattee Server: hits /proxy/fast/ + /// so the server caches the file on disk; other backends apply Invidious URL rewriting). func proxyStreams(videoID: String, instance: Instance) async throws -> [Stream] { if instance.type == .yatteeServer { - return try await yatteeServerAPI(for: instance).proxyStreams(videoID: videoID, instance: instance) + return try await yatteeServerAPI(for: instance) + .proxyStreams(videoID: videoID, instance: instance, mode: .download) } let fetchedStreams = try await streams(videoID: videoID, instance: instance) return await InvidiousAPI.proxyStreamsIfNeeded(fetchedStreams, instance: instance) } - /// Fetches video details, proxy streams, captions, and storyboards (Yattee Server only). - /// For other backends, applies Invidious proxy rewriting if enabled. + /// Fetches video details + streams for downloads. For Yattee Server this routes + /// through `/proxy/fast/`, which caches the file on disk on the server. func videoWithProxyStreamsAndCaptionsAndStoryboards(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { if instance.type == .yatteeServer { - return try await yatteeServerAPI(for: instance).videoWithProxyStreamsAndCaptionsAndStoryboards(id: id, instance: instance) + return try await yatteeServerAPI(for: instance) + .videoWithProxyStreamsAndCaptionsAndStoryboards(id: id, instance: instance, mode: .download) } var result = try await videoWithStreamsAndCaptionsAndStoryboards(id: id, instance: instance) result.streams = await InvidiousAPI.proxyStreamsIfNeeded(result.streams, instance: instance) @@ -278,6 +280,15 @@ actor ContentService: ContentServiceProtocol { case .invidious: return try await invidiousAPI(for: instance).videoWithStreamsAndCaptionsAndStoryboards(id: id, instance: instance) case .yatteeServer: + // When the user has opted into proxying for this Yattee Server source, + // fetch playback streams through the server's /proxy/relay byte-relay + // (Range-friendly, fast TTFB). When off, leave routing to the server's + // per-site proxy_streaming flag — the server can still proxy by default, + // but the client doesn't force it on top. + if instance.proxiesVideos { + return try await yatteeServerAPI(for: instance) + .videoWithProxyStreamsAndCaptionsAndStoryboards(id: id, instance: instance, mode: .relay) + } return try await yatteeServerAPI(for: instance).videoWithStreamsAndCaptionsAndStoryboards(id: id, instance: instance) case .piped: // Piped fallback - make separate calls (no storyboard support) diff --git a/Yattee/Services/API/InvidiousAPI.swift b/Yattee/Services/API/InvidiousAPI.swift index 1f72f01e..f0303ef9 100644 --- a/Yattee/Services/API/InvidiousAPI.swift +++ b/Yattee/Services/API/InvidiousAPI.swift @@ -597,6 +597,10 @@ extension InvidiousAPI { /// - Returns: Streams with YouTube CDN URLs rewritten to go through the instance static func proxyStreamsIfNeeded(_ streams: [Stream], instance: Instance) async -> [Stream] { guard instance.supportsVideoProxying else { return streams } + // Yattee Server does proxying server-side via the ?proxy=true query + // on the videos endpoint (the converter mints signed /proxy/relay + // URLs). Don't second-guess that here by host-rewriting CDN URLs. + if instance.type == .yatteeServer { return streams } // Find first YouTube CDN URL for 403 detection let firstCDNURL = streams.first(where: { isYouTubeCDNURL($0.url) })?.url diff --git a/Yattee/Services/API/YatteeServerAPI.swift b/Yattee/Services/API/YatteeServerAPI.swift index 0c0fdcc2..8648e2ce 100644 --- a/Yattee/Services/API/YatteeServerAPI.swift +++ b/Yattee/Services/API/YatteeServerAPI.swift @@ -254,21 +254,36 @@ actor YatteeServerAPI: InstanceAPI { ) } - // MARK: - Proxy Streams for Downloads + // MARK: - Proxy Streams - /// Fetches streams with URLs that proxy through the Yattee Server for faster LAN downloads. - /// The proxy URLs point to the server's /proxy/fast/{video_id}?itag=X endpoint instead of - /// directly to YouTube CDN, allowing the server to download at full speed and serve locally. - func proxyStreams(videoID: String, instance: Instance) async throws -> [Stream] { - let endpoint = GenericEndpoint.get("/api/v1/videos/\(videoID)", query: ["proxy": "true"]) + /// Selects which yattee-server proxy endpoint stream URLs point at. + /// + /// - `relay`: signed `/proxy/relay` byte-relay — fast TTFB, supports HTTP + /// Range, no on-disk caching. Right shape for playback. + /// - `download`: legacy `/proxy/fast/{video_id}` — runs yt-dlp on the + /// server, writes a file to `/downloads`, then tails it. Right shape + /// for downloads since the cached file accelerates repeated requests. + enum ProxyMode: String { + case relay + case download + } + + /// Fetches streams with URLs that proxy through the Yattee Server. + func proxyStreams(videoID: String, instance: Instance, mode: ProxyMode) async throws -> [Stream] { + let endpoint = GenericEndpoint.get( + "/api/v1/videos/\(videoID)", + query: ["proxy": "true", "proxy_mode": mode.rawValue] + ) let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toStreams() } /// Fetches video details, proxy streams, captions, and storyboards in a single API call. - /// Use this for downloads to get streams that route through the server. - func videoWithProxyStreamsAndCaptionsAndStoryboards(id: String, instance: Instance) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { - let endpoint = GenericEndpoint.get("/api/v1/videos/\(id)", query: ["proxy": "true"]) + func videoWithProxyStreamsAndCaptionsAndStoryboards(id: String, instance: Instance, mode: ProxyMode) async throws -> (video: Video, streams: [Stream], captions: [Caption], storyboards: [Storyboard]) { + let endpoint = GenericEndpoint.get( + "/api/v1/videos/\(id)", + query: ["proxy": "true", "proxy_mode": mode.rawValue] + ) let response: YatteeVideoDetails = try await httpClient.fetch(endpoint, baseURL: instance.url) return ( video: response.toVideo(),