Files
yattee/Yattee/Services/Subscriptions/InvidiousSubscriptionProvider.swift
2026-02-08 18:33:56 +01:00

140 lines
5.0 KiB
Swift

//
// InvidiousSubscriptionProvider.swift
// Yattee
//
// Invidious subscription provider that manages subscriptions on an Invidious instance.
// Uses in-memory cache only - does NOT persist to SwiftData to avoid corrupting local subscriptions.
//
import Foundation
/// Invidious subscription provider that syncs with an Invidious instance account.
/// Subscriptions are managed on the Invidious server and kept in memory only.
/// This provider does NOT write to SwiftData to preserve local subscriptions.
@MainActor
final class InvidiousSubscriptionProvider: SubscriptionProvider {
// MARK: - Properties
let accountType: SubscriptionAccountType = .invidious
private let invidiousAPI: InvidiousAPI
private let credentialsManager: InvidiousCredentialsManager
private let instancesManager: InstancesManager
private let settingsManager: SettingsManager
/// In-memory cache of subscribed channel IDs for fast lookup.
private var cachedChannelIDs: Set<String> = []
/// In-memory cache of full channel data for display.
private var cachedChannels: [Channel] = []
/// Whether the cache has been populated from the server.
private var cachePopulated = false
// MARK: - Initialization
init(
invidiousAPI: InvidiousAPI,
credentialsManager: InvidiousCredentialsManager,
instancesManager: InstancesManager,
settingsManager: SettingsManager
) {
self.invidiousAPI = invidiousAPI
self.credentialsManager = credentialsManager
self.instancesManager = instancesManager
self.settingsManager = settingsManager
}
// MARK: - SubscriptionProvider
func fetchSubscriptions() async throws -> [Channel] {
let (instance, sid) = try await getAuthenticatedInstance()
// Fetch subscriptions from Invidious
let subscriptions = try await invidiousAPI.subscriptions(instance: instance, sid: sid)
// Update in-memory cache
cachedChannelIDs = Set(subscriptions.map(\.authorId))
cachedChannels = subscriptions.map { $0.toChannel(baseURL: instance.url) }
cachePopulated = true
LoggingService.shared.info("Fetched \(subscriptions.count) Invidious subscriptions", category: .api)
return cachedChannels
}
func subscribe(to channel: Channel) async throws {
let (instance, sid) = try await getAuthenticatedInstance()
// Subscribe on Invidious server
try await invidiousAPI.subscribe(to: channel.id.channelID, instance: instance, sid: sid)
// Update in-memory cache only
cachedChannelIDs.insert(channel.id.channelID)
if !cachedChannels.contains(where: { $0.id.channelID == channel.id.channelID }) {
cachedChannels.append(channel)
}
LoggingService.shared.info("Subscribed to \(channel.name) on Invidious", category: .api)
}
func unsubscribe(from channelID: String) async throws {
let (instance, sid) = try await getAuthenticatedInstance()
// Unsubscribe on Invidious server
try await invidiousAPI.unsubscribe(from: channelID, instance: instance, sid: sid)
// Update in-memory cache only
cachedChannelIDs.remove(channelID)
cachedChannels.removeAll { $0.id.channelID == channelID }
LoggingService.shared.info("Unsubscribed from \(channelID) on Invidious", category: .api)
}
func isSubscribed(to channelID: String) async -> Bool {
// If cache isn't populated yet, try to refresh it
if !cachePopulated {
do {
_ = try await fetchSubscriptions()
} catch {
// Can't determine subscription status without server connection
return false
}
}
return cachedChannelIDs.contains(channelID)
}
func refreshCache() async throws {
// Simply fetch subscriptions again - this updates in-memory cache
_ = try await fetchSubscriptions()
}
// MARK: - Private Helpers
/// Gets the authenticated Invidious instance and session ID from account settings.
private func getAuthenticatedInstance() async throws -> (Instance, String) {
// Get the instance ID from subscription account settings
let account = settingsManager.subscriptionAccount
let instance: Instance?
if let instanceID = account.instanceID {
// Use the specific instance from account settings
instance = instancesManager.instances.first { $0.id == instanceID && $0.isEnabled }
} else {
// Fallback to first enabled Invidious instance
instance = instancesManager.instances.first { $0.type == .invidious && $0.isEnabled }
}
guard let instance else {
throw SubscriptionProviderError.instanceNotConfigured
}
// Get session ID
guard let sid = credentialsManager.sid(for: instance) else {
throw SubscriptionProviderError.notAuthenticated
}
return (instance, sid)
}
}