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
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! {

View File

@ -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)
}
}

View File

@ -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 }) {

View File

@ -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)]

View File

@ -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)

View File

@ -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 {