mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
289 lines
9.5 KiB
Swift
289 lines
9.5 KiB
Swift
//
|
|
// SubscriptionService.swift
|
|
// Yattee
|
|
//
|
|
// Service for managing subscriptions through the selected provider.
|
|
// Supports local (iCloud) and Invidious account subscriptions.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Errors that can occur during subscription operations.
|
|
enum SubscriptionError: LocalizedError, Sendable {
|
|
case alreadySubscribed
|
|
case notSubscribed
|
|
case providerNotAvailable
|
|
case operationFailed(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .alreadySubscribed:
|
|
return String(localized: "subscription.error.alreadySubscribed")
|
|
case .notSubscribed:
|
|
return String(localized: "subscription.error.notSubscribed")
|
|
case .providerNotAvailable:
|
|
return String(localized: "subscription.error.providerNotAvailable")
|
|
case .operationFailed(let message):
|
|
return message
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Service that manages subscriptions through the selected subscription provider.
|
|
/// The provider is determined by the user's subscription account setting.
|
|
@MainActor
|
|
@Observable
|
|
final class SubscriptionService {
|
|
// MARK: - Dependencies
|
|
|
|
private let dataManager: DataManager
|
|
private let settingsManager: SettingsManager
|
|
private let instancesManager: InstancesManager
|
|
private let invidiousCredentialsManager: InvidiousCredentialsManager
|
|
private let pipedCredentialsManager: PipedCredentialsManager
|
|
private let invidiousAPI: InvidiousAPI
|
|
private let pipedAPI: PipedAPI
|
|
|
|
// MARK: - Providers
|
|
|
|
private var localProvider: LocalSubscriptionProvider?
|
|
private var invidiousProvider: InvidiousSubscriptionProvider?
|
|
private var pipedProvider: PipedSubscriptionProvider?
|
|
|
|
// MARK: - State
|
|
|
|
/// Whether a subscription operation is in progress.
|
|
private(set) var isLoading = false
|
|
|
|
/// The last error that occurred.
|
|
private(set) var lastError: Error?
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(
|
|
dataManager: DataManager,
|
|
settingsManager: SettingsManager,
|
|
instancesManager: InstancesManager,
|
|
invidiousCredentialsManager: InvidiousCredentialsManager,
|
|
pipedCredentialsManager: PipedCredentialsManager,
|
|
invidiousAPI: InvidiousAPI,
|
|
pipedAPI: PipedAPI
|
|
) {
|
|
self.dataManager = dataManager
|
|
self.settingsManager = settingsManager
|
|
self.instancesManager = instancesManager
|
|
self.invidiousCredentialsManager = invidiousCredentialsManager
|
|
self.pipedCredentialsManager = pipedCredentialsManager
|
|
self.invidiousAPI = invidiousAPI
|
|
self.pipedAPI = pipedAPI
|
|
|
|
// Initialize providers
|
|
self.localProvider = LocalSubscriptionProvider(dataManager: dataManager)
|
|
self.invidiousProvider = InvidiousSubscriptionProvider(
|
|
invidiousAPI: invidiousAPI,
|
|
credentialsManager: invidiousCredentialsManager,
|
|
instancesManager: instancesManager,
|
|
settingsManager: settingsManager
|
|
)
|
|
self.pipedProvider = PipedSubscriptionProvider(
|
|
pipedAPI: pipedAPI,
|
|
credentialsManager: pipedCredentialsManager,
|
|
instancesManager: instancesManager,
|
|
settingsManager: settingsManager
|
|
)
|
|
}
|
|
|
|
// MARK: - Current Provider
|
|
|
|
/// Returns the current subscription provider based on the user's account setting.
|
|
var currentProvider: (any SubscriptionProvider)? {
|
|
let account = settingsManager.subscriptionAccount
|
|
|
|
switch account.type {
|
|
case .local:
|
|
return localProvider
|
|
case .invidious:
|
|
return invidiousProvider
|
|
case .piped:
|
|
return pipedProvider
|
|
}
|
|
}
|
|
|
|
/// The current subscription account type.
|
|
var currentAccountType: SubscriptionAccountType {
|
|
settingsManager.subscriptionAccount.type
|
|
}
|
|
|
|
// MARK: - Subscribe
|
|
|
|
/// Subscribes to a channel using the current provider.
|
|
/// - Parameter channel: The channel to subscribe to.
|
|
/// - Throws: SubscriptionProviderError if the operation fails.
|
|
func subscribe(to channel: Channel) async throws {
|
|
guard let provider = currentProvider else {
|
|
throw SubscriptionError.providerNotAvailable
|
|
}
|
|
|
|
isLoading = true
|
|
lastError = nil
|
|
|
|
do {
|
|
try await provider.subscribe(to: channel)
|
|
LoggingService.shared.info(
|
|
"Subscribed to \(channel.name) via \(provider.accountType)",
|
|
category: .general
|
|
)
|
|
} catch {
|
|
lastError = error
|
|
LoggingService.shared.error(
|
|
"Failed to subscribe to \(channel.name): \(error.localizedDescription)",
|
|
category: .general
|
|
)
|
|
throw error
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
|
|
/// Subscribes to a channel from an Author.
|
|
/// - Parameters:
|
|
/// - author: The author/channel to subscribe to.
|
|
/// - source: The content source.
|
|
func subscribe(to author: Author, source: ContentSource) async throws {
|
|
let channel = Channel(
|
|
id: ChannelID(source: source, channelID: author.id),
|
|
name: author.name,
|
|
subscriberCount: author.subscriberCount,
|
|
thumbnailURL: author.thumbnailURL
|
|
)
|
|
try await subscribe(to: channel)
|
|
}
|
|
|
|
// MARK: - Unsubscribe
|
|
|
|
/// Unsubscribes from a channel using the current provider.
|
|
/// - Parameter channelID: The channel ID to unsubscribe from.
|
|
/// - Throws: SubscriptionProviderError if the operation fails.
|
|
func unsubscribe(from channelID: String) async throws {
|
|
guard let provider = currentProvider else {
|
|
throw SubscriptionError.providerNotAvailable
|
|
}
|
|
|
|
isLoading = true
|
|
lastError = nil
|
|
|
|
do {
|
|
try await provider.unsubscribe(from: channelID)
|
|
LoggingService.shared.info(
|
|
"Unsubscribed from \(channelID) via \(provider.accountType)",
|
|
category: .general
|
|
)
|
|
} catch {
|
|
lastError = error
|
|
LoggingService.shared.error(
|
|
"Failed to unsubscribe from \(channelID): \(error.localizedDescription)",
|
|
category: .general
|
|
)
|
|
throw error
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
|
|
// MARK: - Query
|
|
|
|
/// Checks if subscribed to a channel.
|
|
/// - Parameter channelID: The channel ID to check.
|
|
/// - Returns: `true` if subscribed, `false` otherwise.
|
|
func isSubscribed(to channelID: String) async -> Bool {
|
|
guard let provider = currentProvider else {
|
|
return false
|
|
}
|
|
return await provider.isSubscribed(to: channelID)
|
|
}
|
|
|
|
/// Fetches all subscriptions from the current provider.
|
|
/// - Returns: Array of subscribed channels.
|
|
func fetchSubscriptions() async throws -> [Channel] {
|
|
guard let provider = currentProvider else {
|
|
throw SubscriptionError.providerNotAvailable
|
|
}
|
|
|
|
isLoading = true
|
|
lastError = nil
|
|
|
|
do {
|
|
let channels = try await provider.fetchSubscriptions()
|
|
isLoading = false
|
|
return channels
|
|
} catch {
|
|
lastError = error
|
|
isLoading = false
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/// Synchronously fetches subscriptions for local provider.
|
|
/// Returns nil if the current provider is not local (requires async fetch).
|
|
func fetchSubscriptionsSync() -> [Channel]? {
|
|
guard currentAccountType == .local else {
|
|
return nil
|
|
}
|
|
|
|
return dataManager.subscriptions().map { subscription in
|
|
Channel(
|
|
id: ChannelID(source: subscription.contentSource, channelID: subscription.channelID),
|
|
name: subscription.name,
|
|
description: subscription.channelDescription,
|
|
subscriberCount: subscription.subscriberCount,
|
|
thumbnailURL: subscription.avatarURL,
|
|
bannerURL: subscription.bannerURL,
|
|
isVerified: subscription.isVerified
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Refreshes the subscription cache from the current provider.
|
|
func refreshCache() async throws {
|
|
guard let provider = currentProvider else {
|
|
throw SubscriptionError.providerNotAvailable
|
|
}
|
|
|
|
isLoading = true
|
|
lastError = nil
|
|
|
|
do {
|
|
try await provider.refreshCache()
|
|
isLoading = false
|
|
} catch {
|
|
lastError = error
|
|
isLoading = false
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// MARK: - Synchronous Helpers (for backwards compatibility)
|
|
|
|
/// Synchronously checks if subscribed to a channel.
|
|
/// Uses cached data from DataManager for instant response.
|
|
/// - Parameter channelID: The channel ID to check.
|
|
/// - Returns: `true` if subscribed (based on local cache), `false` otherwise.
|
|
func isSubscribedSync(to channelID: String) -> Bool {
|
|
dataManager.isSubscribed(to: channelID)
|
|
}
|
|
|
|
/// Synchronously subscribes to a channel (local provider only).
|
|
/// For Invidious provider, this will only update local cache.
|
|
/// - Parameter channel: The channel to subscribe to.
|
|
func subscribeSync(to channel: Channel) {
|
|
dataManager.subscribe(to: channel)
|
|
}
|
|
|
|
/// Synchronously unsubscribes from a channel (local provider only).
|
|
/// For Invidious provider, this will only update local cache.
|
|
/// - Parameter channelID: The channel ID to unsubscribe from.
|
|
func unsubscribeSync(from channelID: String) {
|
|
dataManager.unsubscribe(from: channelID)
|
|
}
|
|
}
|