From 5239b36cfe3e35ad71cd3aa927713dad1dac2475 Mon Sep 17 00:00:00 2001 From: Jakub Filo Date: Tue, 18 Mar 2025 22:56:45 +0100 Subject: [PATCH] Add support for invidious companion --- Model/Accounts/Instance.swift | 4 ++- Model/Accounts/InstancesBridge.swift | 6 ++-- Model/Accounts/InstancesModel.swift | 11 ++++++++ Model/Applications/InvidiousAPI.swift | 39 ++++++++++++++++++++------ Shared/Settings/InstanceSettings.swift | 15 ++++++++++ macOS/InstancesSettings.swift | 15 ++++++++++ 6 files changed, 79 insertions(+), 11 deletions(-) diff --git a/Model/Accounts/Instance.swift b/Model/Accounts/Instance.swift index 4aef5792..3dc05a02 100644 --- a/Model/Accounts/Instance.swift +++ b/Model/Accounts/Instance.swift @@ -10,14 +10,16 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { let apiURLString: String var frontendURL: String? var proxiesVideos: Bool + var invidiousCompanion: Bool - init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false) { + init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false, invidiousCompanion: Bool = false) { self.app = app self.id = id ?? UUID().uuidString self.name = name ?? app.rawValue self.apiURLString = apiURLString self.frontendURL = frontendURL self.proxiesVideos = proxiesVideos + self.invidiousCompanion = invidiousCompanion } var apiURL: URL! { diff --git a/Model/Accounts/InstancesBridge.swift b/Model/Accounts/InstancesBridge.swift index 0e49a019..67abd486 100644 --- a/Model/Accounts/InstancesBridge.swift +++ b/Model/Accounts/InstancesBridge.swift @@ -16,7 +16,8 @@ struct InstancesBridge: Defaults.Bridge { "name": value.name, "apiURL": value.apiURLString, "frontendURL": value.frontendURL ?? "", - "proxiesVideos": value.proxiesVideos ? "true" : "false" + "proxiesVideos": value.proxiesVideos ? "true" : "false", + "invidiousCompanion": value.invidiousCompanion ? "true" : "false" ] } @@ -33,7 +34,8 @@ struct InstancesBridge: Defaults.Bridge { let name = object["name"] ?? "" let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"] let proxiesVideos = object["proxiesVideos"] == "true" + let invidiousCompanion = object["invidiousCompanion"] == "true" - return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos) + return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos, invidiousCompanion: invidiousCompanion) } } diff --git a/Model/Accounts/InstancesModel.swift b/Model/Accounts/InstancesModel.swift index af9e1b01..de16768e 100644 --- a/Model/Accounts/InstancesModel.swift +++ b/Model/Accounts/InstancesModel.swift @@ -79,6 +79,17 @@ final class InstancesModel: ObservableObject { Defaults[.instances][index] = instance } + func setInvidiousCompanion(_ instance: Instance, _ invidiousCompanion: Bool) { + guard let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) else { + return + } + + var instance = Defaults[.instances][index] + instance.invidiousCompanion = invidiousCompanion + + Defaults[.instances][index] = instance + } + func remove(_ instance: Instance) { let accounts = accounts(instance.id) if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) { diff --git a/Model/Applications/InvidiousAPI.swift b/Model/Applications/InvidiousAPI.swift index 1594a860..e2e37443 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -655,21 +655,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { if json["liveNow"].boolValue { return hls } + let videoId = json["videoId"].stringValue - return extractFormatStreams(from: json["formatStreams"].arrayValue) + - extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue) + + return extractFormatStreams(from: json["formatStreams"].arrayValue, videoId: videoId) + + extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue, videoId: videoId) + hls } - private func extractFormatStreams(from streams: [JSON]) -> [Stream] { + private func extractFormatStreams(from streams: [JSON], videoId: String?) -> [Stream] { streams.compactMap { stream in guard let streamURL = stream["url"].url else { return nil } + let finalURL: URL + if let videoId, let itag = stream["itag"].string, account.instance.invidiousCompanion { + let companionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(itag)" + finalURL = URL(string: companionURLString) ?? streamURL + } else { + finalURL = streamURL + } return SingleAssetStream( instance: account.instance, - avAsset: AVURLAsset(url: streamURL), + avAsset: AVURLAsset(url: finalURL), resolution: Stream.Resolution.from(resolution: stream["resolution"].string ?? ""), kind: .stream, encoding: stream["encoding"].string ?? "" @@ -677,7 +685,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { } } - private func extractAdaptiveFormats(from streams: [JSON]) -> [Stream] { + private func extractAdaptiveFormats(from streams: [JSON], videoId: String?) -> [Stream] { let audioStreams = streams .filter { $0["type"].stringValue.starts(with: "audio/mp4") } .sorted { @@ -692,15 +700,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { return videoStreams.compactMap { videoStream in guard let audioAssetURL = audioStream["url"].url, - let videoAssetURL = videoStream["url"].url + let videoAssetURL = videoStream["url"].url, + let audioItag = audioStream["itag"].string, + let videoItag = videoStream["itag"].string else { return nil } + let finalAudioURL: URL + let finalVideoURL: URL + + if let videoId, account.instance.invidiousCompanion { + let audioCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(audioItag)" + let videoCompanionURLString = "\(account.instance.apiURLString)/latest_version?id=\(videoId)&itag=\(videoItag)" + finalAudioURL = URL(string: audioCompanionURLString) ?? audioAssetURL + finalVideoURL = URL(string: videoCompanionURLString) ?? videoAssetURL + } else { + finalAudioURL = audioAssetURL + finalVideoURL = videoAssetURL + } return Stream( instance: account.instance, - audioAsset: AVURLAsset(url: audioAssetURL), - videoAsset: AVURLAsset(url: videoAssetURL), + audioAsset: AVURLAsset(url: finalAudioURL), + videoAsset: AVURLAsset(url: finalVideoURL), resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue), kind: .adaptive, encoding: videoStream["encoding"].string, @@ -711,6 +733,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { } } + private func extractHLSStreams(from content: JSON) -> [Stream] { if let hlsURL = content.dictionaryValue["hlsUrl"]?.url { return [Stream(instance: account.instance, hlsURL: hlsURL)] diff --git a/Shared/Settings/InstanceSettings.swift b/Shared/Settings/InstanceSettings.swift index ff5054e6..e52f3569 100644 --- a/Shared/Settings/InstanceSettings.swift +++ b/Shared/Settings/InstanceSettings.swift @@ -8,6 +8,7 @@ struct InstanceSettings: View { @State private var frontendURL = "" @State private var proxiesVideos = false + @State private var invidiousCompanion = false var body: some View { List { @@ -87,6 +88,16 @@ struct InstanceSettings: View { InstancesModel.shared.setProxiesVideos(instance, newValue) } } + + if instance.app == .invidious { + invidiousCompanionToggle + .onAppear { + invidiousCompanion = instance.invidiousCompanion + } + .onChange(of: invidiousCompanion) { newValue in + InstancesModel.shared.setInvidiousCompanion(instance, newValue) + } + } } #if os(tvOS) .frame(maxWidth: 1000) @@ -100,6 +111,10 @@ struct InstanceSettings: View { private var proxiesVideosToggle: some View { Toggle("Proxy videos", isOn: $proxiesVideos) } + + private var invidiousCompanionToggle: some View { + Toggle("Invidious companion", isOn: $invidiousCompanion) + } private func removeAccount(_ account: Account) { AccountsModel.remove(account) diff --git a/macOS/InstancesSettings.swift b/macOS/InstancesSettings.swift index c813b58a..c46c0b22 100644 --- a/macOS/InstancesSettings.swift +++ b/macOS/InstancesSettings.swift @@ -11,6 +11,7 @@ struct InstancesSettings: View { @State private var frontendURL = "" @State private var proxiesVideos = false + @State private var invidiousCompanion = false @Environment(\.colorScheme) private var colorScheme @ObservedObject private var accounts = AccountsModel.shared @@ -105,6 +106,16 @@ struct InstancesSettings: View { } } + if selectedInstance != nil, selectedInstance.app == .invidious { + invidiousCompanionToggle + .onAppear { + invidiousCompanion = selectedInstance.invidiousCompanion + } + .onChange(of: invidiousCompanion) { newValue in + InstancesModel.shared.setInvidiousCompanion(selectedInstance, newValue) + } + } + if selectedInstance != nil, !selectedInstance.app.supportsAccounts { Spacer() Text("Accounts are not supported for the application of this instance") @@ -191,6 +202,10 @@ struct InstancesSettings: View { private var proxiesVideosToggle: some View { Toggle("Proxy videos", isOn: $proxiesVideos) } + + private var invidiousCompanionToggle: some View { + Toggle("Invidious companion", isOn: $invidiousCompanion) + } } struct InstancesSettingsView_Previews: PreviewProvider {