mirror of
https://github.com/yattee/yattee.git
synced 2026-08-06 15:21:29 +00:00
When an Invidious/Piped account is active, several views read the local SwiftData subscription store instead of the account, so counts contradicted each other (Home tile said 10 while the Channels list showed the server's 2): - Home Channels tile now uses the provider's count via new SubscriptionService.cachedSubscriptionCount (fetched once in the background when the server cache isn't populated yet) - Export footer and export content use the active account's list; export runs async with a spinner and surfaces fetch errors as a toast - CSV/OPML import routes through SubscriptionService.importSubscriptions, subscribing on the server for server accounts instead of silently writing to the invisible local store - ChannelView subscribe-state is corrected via the provider after the optimistic local-store read; unused *Sync write helpers removed - Server-account subscribe/unsubscribe/import now post subscriptionsDidChange so other views refresh - New "Delete Local Subscription Data" section in Subscriptions settings (visible with a server account) clears the local store and queues CloudKit deletions so iCloud doesn't restore it
144 lines
5.0 KiB
Swift
144 lines
5.0 KiB
Swift
//
|
|
// PipedSubscriptionProvider.swift
|
|
// Yattee
|
|
//
|
|
// Piped subscription provider that manages subscriptions on a Piped instance.
|
|
// Uses in-memory cache only - does NOT persist to SwiftData to avoid corrupting local subscriptions.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Piped subscription provider that syncs with a Piped instance account.
|
|
/// Subscriptions are managed on the Piped server and kept in memory only.
|
|
/// This provider does NOT write to SwiftData to preserve local subscriptions.
|
|
@MainActor
|
|
final class PipedSubscriptionProvider: SubscriptionProvider {
|
|
// MARK: - Properties
|
|
|
|
let accountType: SubscriptionAccountType = .piped
|
|
|
|
private let pipedAPI: PipedAPI
|
|
private let credentialsManager: PipedCredentialsManager
|
|
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(
|
|
pipedAPI: PipedAPI,
|
|
credentialsManager: PipedCredentialsManager,
|
|
instancesManager: InstancesManager,
|
|
settingsManager: SettingsManager
|
|
) {
|
|
self.pipedAPI = pipedAPI
|
|
self.credentialsManager = credentialsManager
|
|
self.instancesManager = instancesManager
|
|
self.settingsManager = settingsManager
|
|
}
|
|
|
|
// MARK: - SubscriptionProvider
|
|
|
|
func fetchSubscriptions() async throws -> [Channel] {
|
|
let (instance, authToken) = try await getAuthenticatedInstance()
|
|
|
|
// Fetch subscriptions from Piped
|
|
let subscriptions = try await pipedAPI.subscriptions(instance: instance, authToken: authToken)
|
|
|
|
// Update in-memory cache
|
|
cachedChannelIDs = Set(subscriptions.map(\.channelId))
|
|
cachedChannels = subscriptions.map { $0.toChannel() }
|
|
cachePopulated = true
|
|
|
|
LoggingService.shared.info("Fetched \(subscriptions.count) Piped subscriptions", category: .api)
|
|
|
|
return cachedChannels
|
|
}
|
|
|
|
func subscribe(to channel: Channel) async throws {
|
|
let (instance, authToken) = try await getAuthenticatedInstance()
|
|
|
|
// Subscribe on Piped server
|
|
try await pipedAPI.subscribe(channelID: channel.id.channelID, instance: instance, authToken: authToken)
|
|
|
|
// 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 Piped", category: .api)
|
|
}
|
|
|
|
func unsubscribe(from channelID: String) async throws {
|
|
let (instance, authToken) = try await getAuthenticatedInstance()
|
|
|
|
// Unsubscribe on Piped server
|
|
try await pipedAPI.unsubscribe(channelID: channelID, instance: instance, authToken: authToken)
|
|
|
|
// Update in-memory cache only
|
|
cachedChannelIDs.remove(channelID)
|
|
cachedChannels.removeAll { $0.id.channelID == channelID }
|
|
|
|
LoggingService.shared.info("Unsubscribed from \(channelID) on Piped", 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()
|
|
}
|
|
|
|
var cachedSubscriptionCount: Int? {
|
|
cachePopulated ? cachedChannels.count : nil
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
/// Gets the authenticated Piped instance and auth token 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 Piped instance
|
|
instance = instancesManager.instances.first { $0.type == .piped && $0.isEnabled }
|
|
}
|
|
|
|
guard let instance else {
|
|
throw SubscriptionProviderError.instanceNotConfigured
|
|
}
|
|
|
|
// Get auth token
|
|
guard let authToken = credentialsManager.credential(for: instance) else {
|
|
throw SubscriptionProviderError.notAuthenticated
|
|
}
|
|
|
|
return (instance, authToken)
|
|
}
|
|
}
|