From 77b09ea8020d5711a039eb11d7523321c3f0e0d5 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Tue, 6 Oct 2026 23:43:03 +0200 Subject: [PATCH] Resolve @handle channel links on Invidious MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invidious' channel endpoints take only UC… IDs and return 500 for a youtube.com/@handle, so channels opened from handle links (and the subscribe link action) failed with "Could not get channel info". Resolve the handle through /api/v1/resolveurl first, cached per instance. --- Yattee/Services/API/InvidiousAPI.swift | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Yattee/Services/API/InvidiousAPI.swift b/Yattee/Services/API/InvidiousAPI.swift index 74ad743a..13dc28c8 100644 --- a/Yattee/Services/API/InvidiousAPI.swift +++ b/Yattee/Services/API/InvidiousAPI.swift @@ -116,7 +116,24 @@ actor InvidiousAPI: InstanceAPI { return response.toVideo(baseURL: instance.url) } + /// `@handle` channel IDs (youtube.com/@name links) resolved to UC… IDs, per instance. + private var resolvedChannelHandles: [String: String] = [:] + + /// Invidious' channel endpoints only take UC… IDs, so a `@handle` is resolved first. + private func channelID(for id: String, instance: Instance) async throws -> String { + guard id.hasPrefix("@") else { return id } + let key = "\(instance.url.absoluteString)|\(id.lowercased())" + if let cached = resolvedChannelHandles[key] { return cached } + + let endpoint = GenericEndpoint.get("/api/v1/resolveurl", query: ["url": "https://www.youtube.com/\(id)"]) + let response: InvidiousResolvedURL = try await httpClient.fetch(endpoint, baseURL: instance.url) + guard let ucid = response.ucid, !ucid.isEmpty else { return id } + resolvedChannelHandles[key] = ucid + return ucid + } + func channel(id: String, instance: Instance) async throws -> Channel { + let id = try await channelID(for: id, instance: instance) let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)") let response: InvidiousChannel = try await httpClient.fetch(endpoint, baseURL: instance.url) return response.toChannel(baseURL: instance.url) @@ -127,6 +144,7 @@ actor InvidiousAPI: InstanceAPI { if let continuation { query["continuation"] = continuation } + let id = try await channelID(for: id, instance: instance) let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/videos", query: query) let response: InvidiousChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelVideosPage( @@ -140,6 +158,7 @@ actor InvidiousAPI: InstanceAPI { if let continuation { query["continuation"] = continuation } + let id = try await channelID(for: id, instance: instance) let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/playlists", query: query) let response: InvidiousChannelPlaylists = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelPlaylistsPage( @@ -153,6 +172,7 @@ actor InvidiousAPI: InstanceAPI { if let continuation { query["continuation"] = continuation } + let id = try await channelID(for: id, instance: instance) let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/shorts", query: query) let response: InvidiousChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelVideosPage( @@ -166,6 +186,7 @@ actor InvidiousAPI: InstanceAPI { if let continuation { query["continuation"] = continuation } + let id = try await channelID(for: id, instance: instance) let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/streams", query: query) let response: InvidiousChannelVideosWithContinuation = try await httpClient.fetch(endpoint, baseURL: instance.url) return ChannelVideosPage( @@ -408,6 +429,7 @@ actor InvidiousAPI: InstanceAPI { } func channelSearch(id: String, query: String, instance: Instance, page: Int) async throws -> ChannelSearchPage { + let id = try await channelID(for: id, instance: instance) let endpoint = GenericEndpoint.get("/api/v1/channels/\(id)/search", query: [ "q": query, "page": String(page) @@ -2085,3 +2107,8 @@ private struct InvidiousSuggestions: Decodable, Sendable { let query: String let suggestions: [String] } + +/// Response of `/api/v1/resolveurl`. +private struct InvidiousResolvedURL: Decodable, Sendable { + let ucid: String? +}