From b0dfd2f9d259ac13e0b92f3077b6a8e0ac08123a Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 22 Nov 2025 19:42:18 +0100 Subject: [PATCH] Add experimental setting to hide videos without duration in Invidious This adds a new instance setting for Invidious that filters out videos without duration information from feeds, popular, trending, search results, and channel pages. This can be used to hide YouTube Shorts. The setting is labeled as "Experimental: Hide videos without duration" and includes an explanation that it can be used to hide shorts. Key changes: - Added hideVideosWithoutDuration property to Instance model - Updated InstancesBridge to serialize/deserialize the new setting - Added UI toggle in InstanceSettings with explanatory footer text - Implemented filtering in InvidiousAPI for: * Popular videos * Trending videos * Search results * Subscription feed * Channel content - Videos accessed directly by URL are not filtered --- Model/Accounts/Instance.swift | 4 +++- Model/Accounts/InstancesBridge.swift | 6 ++++-- Model/Accounts/InstancesModel.swift | 11 +++++++++++ Model/Applications/InvidiousAPI.swift | 21 ++++++++++++++++----- Model/Search/SearchModel.swift | 6 ++---- Shared/Settings/InstanceSettings.swift | 15 +++++++++++++++ 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/Model/Accounts/Instance.swift b/Model/Accounts/Instance.swift index 3dc05a02..84a7eb4e 100644 --- a/Model/Accounts/Instance.swift +++ b/Model/Accounts/Instance.swift @@ -11,8 +11,9 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { var frontendURL: String? var proxiesVideos: Bool var invidiousCompanion: Bool + var hideVideosWithoutDuration: Bool - init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false, invidiousCompanion: Bool = false) { + init(app: VideosApp, id: String? = nil, name: String? = nil, apiURLString: String, frontendURL: String? = nil, proxiesVideos: Bool = false, invidiousCompanion: Bool = false, hideVideosWithoutDuration: Bool = false) { self.app = app self.id = id ?? UUID().uuidString self.name = name ?? app.rawValue @@ -20,6 +21,7 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable { self.frontendURL = frontendURL self.proxiesVideos = proxiesVideos self.invidiousCompanion = invidiousCompanion + self.hideVideosWithoutDuration = hideVideosWithoutDuration } var apiURL: URL! { diff --git a/Model/Accounts/InstancesBridge.swift b/Model/Accounts/InstancesBridge.swift index 67abd486..06faf7d8 100644 --- a/Model/Accounts/InstancesBridge.swift +++ b/Model/Accounts/InstancesBridge.swift @@ -17,7 +17,8 @@ struct InstancesBridge: Defaults.Bridge { "apiURL": value.apiURLString, "frontendURL": value.frontendURL ?? "", "proxiesVideos": value.proxiesVideos ? "true" : "false", - "invidiousCompanion": value.invidiousCompanion ? "true" : "false" + "invidiousCompanion": value.invidiousCompanion ? "true" : "false", + "hideVideosWithoutDuration": value.hideVideosWithoutDuration ? "true" : "false" ] } @@ -35,7 +36,8 @@ struct InstancesBridge: Defaults.Bridge { let frontendURL: String? = object["frontendURL"]!.isEmpty ? nil : object["frontendURL"] let proxiesVideos = object["proxiesVideos"] == "true" let invidiousCompanion = object["invidiousCompanion"] == "true" + let hideVideosWithoutDuration = object["hideVideosWithoutDuration"] == "true" - return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos, invidiousCompanion: invidiousCompanion) + return Instance(app: app, id: id, name: name, apiURLString: apiURL, frontendURL: frontendURL, proxiesVideos: proxiesVideos, invidiousCompanion: invidiousCompanion, hideVideosWithoutDuration: hideVideosWithoutDuration) } } diff --git a/Model/Accounts/InstancesModel.swift b/Model/Accounts/InstancesModel.swift index de16768e..4bc2d04b 100644 --- a/Model/Accounts/InstancesModel.swift +++ b/Model/Accounts/InstancesModel.swift @@ -90,6 +90,17 @@ final class InstancesModel: ObservableObject { Defaults[.instances][index] = instance } + func setHideVideosWithoutDuration(_ instance: Instance, _ hideVideosWithoutDuration: Bool) { + guard let index = Defaults[.instances].firstIndex(where: { $0.id == instance.id }) else { + return + } + + var instance = Defaults[.instances][index] + instance.hideVideosWithoutDuration = hideVideosWithoutDuration + + 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 710123b6..6bf75f81 100644 --- a/Model/Applications/InvidiousAPI.swift +++ b/Model/Applications/InvidiousAPI.swift @@ -52,11 +52,13 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { } configureTransformer(pathPattern("popular"), requestMethods: [.get]) { (content: Entity) -> [Video] in - content.json.arrayValue.map(self.extractVideo) + let videos = content.json.arrayValue.map(self.extractVideo) + return self.account.instance.hideVideosWithoutDuration ? videos.filter { $0.length > 0 } : videos } configureTransformer(pathPattern("trending"), requestMethods: [.get]) { (content: Entity) -> [Video] in - content.json.arrayValue.map(self.extractVideo) + let videos = content.json.arrayValue.map(self.extractVideo) + return self.account.instance.hideVideosWithoutDuration ? videos.filter { $0.length > 0 } : videos } configureTransformer(pathPattern("search"), requestMethods: [.get]) { (content: Entity) -> SearchPage in @@ -70,7 +72,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { return ContentItem(playlist: self.extractChannelPlaylist(from: json)) } if type == "video" { - return ContentItem(video: self.extractVideo(from: json)) + let video = self.extractVideo(from: json) + if self.account.instance.hideVideosWithoutDuration, video.length == 0 { + return nil + } + return ContentItem(video: video) } return nil @@ -101,7 +107,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { configureTransformer(pathPattern("auth/feed"), requestMethods: [.get]) { (content: Entity) -> [Video] in if let feedVideos = content.json.dictionaryValue["videos"] { - return feedVideos.arrayValue.map(self.extractVideo) + let videos = feedVideos.arrayValue.map(self.extractVideo) + return self.account.instance.hideVideosWithoutDuration ? videos.filter { $0.length > 0 } : videos } return [] @@ -875,7 +882,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI { return ContentItem(playlist: extractChannelPlaylist(from: json)) } if type == "video" { - return ContentItem(video: extractVideo(from: json)) + let video = extractVideo(from: json) + if account.instance.hideVideosWithoutDuration, video.length == 0 { + return nil + } + return ContentItem(video: video) } return nil diff --git a/Model/Search/SearchModel.swift b/Model/Search/SearchModel.swift index e5d1b13b..ce830c3e 100644 --- a/Model/Search/SearchModel.swift +++ b/Model/Search/SearchModel.swift @@ -25,11 +25,9 @@ final class SearchModel: ObservableObject { var accounts: AccountsModel { .shared } private var resource: Resource! - init() { - } + init() {} - deinit { - } + deinit {} var isLoading: Bool { resource?.isLoading ?? false diff --git a/Shared/Settings/InstanceSettings.swift b/Shared/Settings/InstanceSettings.swift index 9a761caf..df64efc8 100644 --- a/Shared/Settings/InstanceSettings.swift +++ b/Shared/Settings/InstanceSettings.swift @@ -9,6 +9,7 @@ struct InstanceSettings: View { @State private var frontendURL = "" @State private var proxiesVideos = false @State private var invidiousCompanion = false + @State private var hideVideosWithoutDuration = false var body: some View { List { @@ -97,6 +98,16 @@ struct InstanceSettings: View { .onChange(of: invidiousCompanion) { newValue in InstancesModel.shared.setInvidiousCompanion(instance, newValue) } + + Section(footer: Text("This can be used to hide shorts".localized())) { + hideVideosWithoutDurationToggle + .onAppear { + hideVideosWithoutDuration = instance.hideVideosWithoutDuration + } + .onChange(of: hideVideosWithoutDuration) { newValue in + InstancesModel.shared.setHideVideosWithoutDuration(instance, newValue) + } + } } } #if os(tvOS) @@ -116,6 +127,10 @@ struct InstanceSettings: View { Toggle("Invidious companion", isOn: $invidiousCompanion) } + private var hideVideosWithoutDurationToggle: some View { + Toggle("Experimental: Hide videos without duration", isOn: $hideVideosWithoutDuration) + } + private func removeAccount(_ account: Account) { AccountsModel.remove(account) accountsChanged.toggle()