Add support for invidious companion

This commit is contained in:
Jakub Filo 2025-03-18 22:56:45 +01:00
parent 3a17cc4dee
commit 5239b36cfe
6 changed files with 79 additions and 11 deletions

View File

@ -10,14 +10,16 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
let apiURLString: String let apiURLString: String
var frontendURL: String? var frontendURL: String?
var proxiesVideos: Bool 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.app = app
self.id = id ?? UUID().uuidString self.id = id ?? UUID().uuidString
self.name = name ?? app.rawValue self.name = name ?? app.rawValue
self.apiURLString = apiURLString self.apiURLString = apiURLString
self.frontendURL = frontendURL self.frontendURL = frontendURL
self.proxiesVideos = proxiesVideos self.proxiesVideos = proxiesVideos
self.invidiousCompanion = invidiousCompanion
} }
var apiURL: URL! { var apiURL: URL! {

View File

@ -16,7 +16,8 @@ struct InstancesBridge: Defaults.Bridge {
"name": value.name, "name": value.name,
"apiURL": value.apiURLString, "apiURL": value.apiURLString,
"frontendURL": value.frontendURL ?? "", "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 name = object["name"] ?? ""
let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"] let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"]
let proxiesVideos = object["proxiesVideos"] == "true" 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)
} }
} }

View File

@ -79,6 +79,17 @@ final class InstancesModel: ObservableObject {
Defaults[.instances][index] = instance 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) { func remove(_ instance: Instance) {
let accounts = accounts(instance.id) let accounts = accounts(instance.id)
if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) { if let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) {

View File

@ -655,21 +655,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
if json["liveNow"].boolValue { if json["liveNow"].boolValue {
return hls return hls
} }
let videoId = json["videoId"].stringValue
return extractFormatStreams(from: json["formatStreams"].arrayValue) + return extractFormatStreams(from: json["formatStreams"].arrayValue, videoId: videoId) +
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue) + extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue, videoId: videoId) +
hls hls
} }
private func extractFormatStreams(from streams: [JSON]) -> [Stream] { private func extractFormatStreams(from streams: [JSON], videoId: String?) -> [Stream] {
streams.compactMap { stream in streams.compactMap { stream in
guard let streamURL = stream["url"].url else { guard let streamURL = stream["url"].url else {
return nil 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( return SingleAssetStream(
instance: account.instance, instance: account.instance,
avAsset: AVURLAsset(url: streamURL), avAsset: AVURLAsset(url: finalURL),
resolution: Stream.Resolution.from(resolution: stream["resolution"].string ?? ""), resolution: Stream.Resolution.from(resolution: stream["resolution"].string ?? ""),
kind: .stream, kind: .stream,
encoding: stream["encoding"].string ?? "" 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 let audioStreams = streams
.filter { $0["type"].stringValue.starts(with: "audio/mp4") } .filter { $0["type"].stringValue.starts(with: "audio/mp4") }
.sorted { .sorted {
@ -692,15 +700,29 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
return videoStreams.compactMap { videoStream in return videoStreams.compactMap { videoStream in
guard let audioAssetURL = audioStream["url"].url, 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 { else {
return nil 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( return Stream(
instance: account.instance, instance: account.instance,
audioAsset: AVURLAsset(url: audioAssetURL), audioAsset: AVURLAsset(url: finalAudioURL),
videoAsset: AVURLAsset(url: videoAssetURL), videoAsset: AVURLAsset(url: finalVideoURL),
resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue), resolution: Stream.Resolution.from(resolution: videoStream["resolution"].stringValue),
kind: .adaptive, kind: .adaptive,
encoding: videoStream["encoding"].string, encoding: videoStream["encoding"].string,
@ -711,6 +733,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
} }
private func extractHLSStreams(from content: JSON) -> [Stream] { private func extractHLSStreams(from content: JSON) -> [Stream] {
if let hlsURL = content.dictionaryValue["hlsUrl"]?.url { if let hlsURL = content.dictionaryValue["hlsUrl"]?.url {
return [Stream(instance: account.instance, hlsURL: hlsURL)] return [Stream(instance: account.instance, hlsURL: hlsURL)]

View File

@ -8,6 +8,7 @@ struct InstanceSettings: View {
@State private var frontendURL = "" @State private var frontendURL = ""
@State private var proxiesVideos = false @State private var proxiesVideos = false
@State private var invidiousCompanion = false
var body: some View { var body: some View {
List { List {
@ -87,6 +88,16 @@ struct InstanceSettings: View {
InstancesModel.shared.setProxiesVideos(instance, newValue) 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) #if os(tvOS)
.frame(maxWidth: 1000) .frame(maxWidth: 1000)
@ -100,6 +111,10 @@ struct InstanceSettings: View {
private var proxiesVideosToggle: some View { private var proxiesVideosToggle: some View {
Toggle("Proxy videos", isOn: $proxiesVideos) Toggle("Proxy videos", isOn: $proxiesVideos)
} }
private var invidiousCompanionToggle: some View {
Toggle("Invidious companion", isOn: $invidiousCompanion)
}
private func removeAccount(_ account: Account) { private func removeAccount(_ account: Account) {
AccountsModel.remove(account) AccountsModel.remove(account)

View File

@ -11,6 +11,7 @@ struct InstancesSettings: View {
@State private var frontendURL = "" @State private var frontendURL = ""
@State private var proxiesVideos = false @State private var proxiesVideos = false
@State private var invidiousCompanion = false
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@ObservedObject private var accounts = AccountsModel.shared @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 { if selectedInstance != nil, !selectedInstance.app.supportsAccounts {
Spacer() Spacer()
Text("Accounts are not supported for the application of this instance") Text("Accounts are not supported for the application of this instance")
@ -191,6 +202,10 @@ struct InstancesSettings: View {
private var proxiesVideosToggle: some View { private var proxiesVideosToggle: some View {
Toggle("Proxy videos", isOn: $proxiesVideos) Toggle("Proxy videos", isOn: $proxiesVideos)
} }
private var invidiousCompanionToggle: some View {
Toggle("Invidious companion", isOn: $invidiousCompanion)
}
} }
struct InstancesSettingsView_Previews: PreviewProvider { struct InstancesSettingsView_Previews: PreviewProvider {