mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
206 lines
6.3 KiB
Swift
206 lines
6.3 KiB
Swift
//
|
|
// Subscription.swift
|
|
// Yattee
|
|
//
|
|
// SwiftData model for channel subscriptions.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
/// Represents a subscribed channel.
|
|
@Model
|
|
final class Subscription {
|
|
// MARK: - Channel Identity
|
|
|
|
/// The channel ID string.
|
|
var channelID: String = ""
|
|
|
|
/// The content source raw value.
|
|
var sourceRawValue: String = "youtube"
|
|
|
|
/// For PeerTube: the instance URL string.
|
|
var instanceURLString: String?
|
|
|
|
// MARK: - Channel Metadata
|
|
|
|
/// The channel name.
|
|
var name: String = ""
|
|
|
|
/// Channel description.
|
|
var channelDescription: String?
|
|
|
|
/// Subscriber count (if known).
|
|
var subscriberCount: Int?
|
|
|
|
/// Avatar/thumbnail URL string.
|
|
var avatarURLString: String?
|
|
|
|
/// Banner URL string.
|
|
var bannerURLString: String?
|
|
|
|
/// Whether the channel is verified.
|
|
var isVerified: Bool = false
|
|
|
|
// MARK: - Subscription Metadata
|
|
|
|
/// When the subscription was created.
|
|
var subscribedAt: Date = Date()
|
|
|
|
/// When channel info was last updated.
|
|
var lastUpdatedAt: Date = Date()
|
|
|
|
/// When the channel's most recent video was published (for sorting).
|
|
var lastVideoPublishedAt: Date?
|
|
|
|
// MARK: - Server Sync (Yattee Server)
|
|
|
|
/// The server's subscription ID (for deletion via server API).
|
|
var serverSubscriptionID: Int?
|
|
|
|
/// The provider name (e.g., "youtube", "peertube") for server sync.
|
|
/// Used as the `site` field in server API calls.
|
|
var providerName: String?
|
|
|
|
/// The channel URL for external/extracted sources (required for feed fetching).
|
|
var channelURLString: String?
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(
|
|
channelID: String,
|
|
sourceRawValue: String,
|
|
instanceURLString: String? = nil,
|
|
name: String,
|
|
channelDescription: String? = nil,
|
|
subscriberCount: Int? = nil,
|
|
avatarURLString: String? = nil,
|
|
bannerURLString: String? = nil,
|
|
isVerified: Bool = false,
|
|
channelURLString: String? = nil
|
|
) {
|
|
self.channelID = channelID
|
|
self.sourceRawValue = sourceRawValue
|
|
self.instanceURLString = instanceURLString
|
|
self.name = name
|
|
self.channelDescription = channelDescription
|
|
self.subscriberCount = subscriberCount
|
|
self.avatarURLString = avatarURLString
|
|
self.bannerURLString = bannerURLString
|
|
self.isVerified = isVerified
|
|
self.channelURLString = channelURLString
|
|
self.subscribedAt = Date()
|
|
self.lastUpdatedAt = Date()
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
/// The content source for this subscription.
|
|
var contentSource: ContentSource {
|
|
let provider = providerName ?? ContentSource.youtubeProvider
|
|
|
|
if sourceRawValue == "global" {
|
|
return .global(provider: provider)
|
|
} else if sourceRawValue == "federated",
|
|
let urlString = instanceURLString,
|
|
let url = URL(string: urlString) {
|
|
return .federated(provider: providerName ?? ContentSource.peertubeProvider, instance: url)
|
|
}
|
|
return .global(provider: provider)
|
|
}
|
|
|
|
/// The site value for server API calls (same as provider).
|
|
var site: String {
|
|
providerName ?? contentSource.provider
|
|
}
|
|
|
|
/// The avatar URL if available.
|
|
var avatarURL: URL? {
|
|
avatarURLString.flatMap { URL(string: $0) }
|
|
}
|
|
|
|
/// The banner URL if available.
|
|
var bannerURL: URL? {
|
|
bannerURLString.flatMap { URL(string: $0) }
|
|
}
|
|
|
|
/// Formatted subscriber count.
|
|
var formattedSubscriberCount: String? {
|
|
guard let count = subscriberCount else { return nil }
|
|
return CountFormatter.compact(count)
|
|
}
|
|
|
|
// MARK: - Methods
|
|
|
|
/// Updates the channel metadata from fresh data.
|
|
/// Uses a merge strategy: only updates optional fields if the new value is non-nil,
|
|
/// preventing nil values from overwriting valid cached data.
|
|
func update(from channel: Channel) {
|
|
name = channel.name
|
|
isVerified = channel.isVerified
|
|
lastUpdatedAt = Date()
|
|
|
|
// Only update optional fields if new value is non-nil
|
|
if let desc = channel.description {
|
|
channelDescription = desc
|
|
}
|
|
if let count = channel.subscriberCount {
|
|
subscriberCount = count
|
|
}
|
|
if let thumb = channel.thumbnailURL {
|
|
avatarURLString = thumb.absoluteString
|
|
}
|
|
if let banner = channel.bannerURL {
|
|
bannerURLString = banner.absoluteString
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Factory Methods
|
|
|
|
extension Subscription {
|
|
/// Creates a Subscription from a Channel model.
|
|
static func from(channel: Channel) -> Subscription {
|
|
let sourceRaw: String
|
|
var instanceURL: String?
|
|
var channelURL: String?
|
|
let provider = channel.id.source.provider
|
|
|
|
switch channel.id.source {
|
|
case .global(let prov):
|
|
sourceRaw = "global"
|
|
// Construct YouTube channel URL
|
|
if prov == ContentSource.youtubeProvider {
|
|
if channel.id.channelID.hasPrefix("@") {
|
|
channelURL = "https://www.youtube.com/\(channel.id.channelID)"
|
|
} else {
|
|
channelURL = "https://www.youtube.com/channel/\(channel.id.channelID)"
|
|
}
|
|
}
|
|
case .federated(_, let instance):
|
|
sourceRaw = "federated"
|
|
instanceURL = instance.absoluteString
|
|
// Construct PeerTube channel URL
|
|
channelURL = instance.appendingPathComponent("video-channels/\(channel.id.channelID)").absoluteString
|
|
case .extracted(_, let originalURL):
|
|
sourceRaw = "extracted"
|
|
channelURL = originalURL.absoluteString
|
|
}
|
|
|
|
let subscription = Subscription(
|
|
channelID: channel.id.channelID,
|
|
sourceRawValue: sourceRaw,
|
|
instanceURLString: instanceURL,
|
|
name: channel.name,
|
|
channelDescription: channel.description,
|
|
subscriberCount: channel.subscriberCount,
|
|
avatarURLString: channel.thumbnailURL?.absoluteString,
|
|
bannerURLString: channel.bannerURL?.absoluteString,
|
|
isVerified: channel.isVerified,
|
|
channelURLString: channelURL
|
|
)
|
|
subscription.providerName = provider
|
|
return subscription
|
|
}
|
|
}
|