mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
301 lines
9.3 KiB
Swift
301 lines
9.3 KiB
Swift
//
|
|
// Bookmark.swift
|
|
// Yattee
|
|
//
|
|
// SwiftData model for bookmarked/favorited videos.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
|
|
/// Represents a bookmarked video for later viewing.
|
|
@Model
|
|
final class Bookmark {
|
|
// MARK: - Video Identity
|
|
|
|
/// The video ID string (YouTube ID or PeerTube UUID).
|
|
var videoID: String = ""
|
|
|
|
/// The content source raw value for encoding ("global", "federated", "extracted").
|
|
var sourceRawValue: String = "global"
|
|
|
|
/// For global sources: the provider name (e.g., "youtube", "dailymotion").
|
|
var globalProvider: String?
|
|
|
|
/// For PeerTube: the instance URL string.
|
|
var instanceURLString: String?
|
|
|
|
/// For PeerTube: the UUID.
|
|
var peertubeUUID: String?
|
|
|
|
/// For external sources: the extractor name (e.g., "vimeo", "twitter").
|
|
var externalExtractor: String?
|
|
|
|
/// For external sources: the original URL for re-extraction.
|
|
var externalURLString: String?
|
|
|
|
// MARK: - Video Metadata (cached for offline display)
|
|
|
|
/// The video title.
|
|
var title: String = ""
|
|
|
|
/// The channel/author name.
|
|
var authorName: String = ""
|
|
|
|
/// The channel/author ID.
|
|
var authorID: String = ""
|
|
|
|
/// Video duration in seconds.
|
|
var duration: TimeInterval = 0
|
|
|
|
/// Thumbnail URL string.
|
|
var thumbnailURLString: String?
|
|
|
|
/// Whether this is a live stream.
|
|
var isLive: Bool = false
|
|
|
|
/// View count if available.
|
|
var viewCount: Int?
|
|
|
|
/// When the video was published.
|
|
var publishedAt: Date?
|
|
|
|
/// Human-readable published date from the API.
|
|
var publishedText: String?
|
|
|
|
// MARK: - Bookmark Metadata
|
|
|
|
/// When this bookmark was created.
|
|
var createdAt: Date = Date()
|
|
|
|
/// Optional user note/comment.
|
|
var note: String?
|
|
|
|
/// When the note was last modified.
|
|
var noteModifiedAt: Date?
|
|
|
|
/// User-defined tags for categorizing the bookmark.
|
|
var tags: [String] = []
|
|
|
|
/// When the tags were last modified.
|
|
var tagsModifiedAt: Date?
|
|
|
|
/// Sort order for manual ordering.
|
|
var sortOrder: Int = 0
|
|
|
|
// MARK: - Initialization
|
|
|
|
init(
|
|
videoID: String,
|
|
sourceRawValue: String,
|
|
globalProvider: String? = nil,
|
|
instanceURLString: String? = nil,
|
|
peertubeUUID: String? = nil,
|
|
externalExtractor: String? = nil,
|
|
externalURLString: String? = nil,
|
|
title: String,
|
|
authorName: String,
|
|
authorID: String,
|
|
duration: TimeInterval,
|
|
thumbnailURLString: String? = nil,
|
|
isLive: Bool = false,
|
|
viewCount: Int? = nil,
|
|
publishedAt: Date? = nil,
|
|
publishedText: String? = nil,
|
|
note: String? = nil,
|
|
noteModifiedAt: Date? = nil,
|
|
tags: [String] = [],
|
|
tagsModifiedAt: Date? = nil,
|
|
sortOrder: Int = 0
|
|
) {
|
|
self.videoID = videoID
|
|
self.sourceRawValue = sourceRawValue
|
|
self.globalProvider = globalProvider
|
|
self.instanceURLString = instanceURLString
|
|
self.peertubeUUID = peertubeUUID
|
|
self.externalExtractor = externalExtractor
|
|
self.externalURLString = externalURLString
|
|
self.title = title
|
|
self.authorName = authorName
|
|
self.authorID = authorID
|
|
self.duration = duration
|
|
self.thumbnailURLString = thumbnailURLString
|
|
self.isLive = isLive
|
|
self.viewCount = viewCount
|
|
self.publishedAt = publishedAt
|
|
self.publishedText = publishedText
|
|
self.note = note
|
|
self.noteModifiedAt = noteModifiedAt
|
|
self.tags = tags
|
|
self.tagsModifiedAt = tagsModifiedAt
|
|
self.sortOrder = sortOrder
|
|
self.createdAt = Date()
|
|
}
|
|
|
|
// MARK: - Computed Properties
|
|
|
|
/// The content source for this bookmark.
|
|
var contentSource: ContentSource {
|
|
if sourceRawValue == "global" {
|
|
return .global(provider: globalProvider ?? ContentSource.youtubeProvider)
|
|
} else if sourceRawValue == "federated",
|
|
let urlString = instanceURLString,
|
|
let url = URL(string: urlString) {
|
|
return .federated(provider: ContentSource.peertubeProvider, instance: url)
|
|
} else if sourceRawValue == "extracted",
|
|
let extractor = externalExtractor,
|
|
let urlString = externalURLString,
|
|
let url = URL(string: urlString) {
|
|
return .extracted(extractor: extractor, originalURL: url)
|
|
}
|
|
return .global(provider: globalProvider ?? ContentSource.youtubeProvider)
|
|
}
|
|
|
|
/// The thumbnail URL if available.
|
|
var thumbnailURL: URL? {
|
|
thumbnailURLString.flatMap { URL(string: $0) }
|
|
}
|
|
|
|
/// Formatted duration string.
|
|
var formattedDuration: String {
|
|
guard !isLive else { return String(localized: "video.badge.live") }
|
|
guard duration > 0 else { return "" }
|
|
|
|
let hours = Int(duration) / 3600
|
|
let minutes = (Int(duration) % 3600) / 60
|
|
let seconds = Int(duration) % 60
|
|
|
|
if hours > 0 {
|
|
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
|
|
} else {
|
|
return String(format: "%d:%02d", minutes, seconds)
|
|
}
|
|
}
|
|
|
|
/// Formatted view count string.
|
|
var formattedViewCount: String? {
|
|
guard let viewCount else { return nil }
|
|
return CountFormatter.compact(viewCount)
|
|
}
|
|
|
|
/// Formatted published date, preferring parsed Date over API-provided text.
|
|
var formattedPublishedDate: String? {
|
|
if let publishedAt {
|
|
let formatter = RelativeDateTimeFormatter()
|
|
formatter.unitsStyle = .full
|
|
return formatter.localizedString(for: publishedAt, relativeTo: Date())
|
|
}
|
|
return publishedText
|
|
}
|
|
|
|
/// Converts this bookmark to a Video model for playback.
|
|
func toVideo() -> Video {
|
|
let videoIDObj: VideoID
|
|
switch contentSource {
|
|
case .global(let provider):
|
|
videoIDObj = VideoID(source: .global(provider: provider), videoID: videoID)
|
|
case .federated(let provider, let instance):
|
|
videoIDObj = VideoID(source: .federated(provider: provider, instance: instance), videoID: videoID, uuid: peertubeUUID)
|
|
case .extracted(let extractor, let originalURL):
|
|
videoIDObj = VideoID(source: .extracted(extractor: extractor, originalURL: originalURL), videoID: videoID)
|
|
}
|
|
|
|
let author = Author(
|
|
id: authorID,
|
|
name: authorName,
|
|
thumbnailURL: nil,
|
|
subscriberCount: nil
|
|
)
|
|
|
|
return Video(
|
|
id: videoIDObj,
|
|
title: title,
|
|
description: nil,
|
|
author: author,
|
|
duration: duration,
|
|
publishedAt: publishedAt,
|
|
publishedText: publishedText,
|
|
viewCount: viewCount,
|
|
likeCount: nil,
|
|
thumbnails: thumbnailURL.map { [Thumbnail(url: $0, width: nil, height: nil)] } ?? [],
|
|
isLive: isLive,
|
|
isUpcoming: false,
|
|
scheduledStartTime: nil
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Factory Methods
|
|
|
|
extension Bookmark {
|
|
/// Creates a Bookmark from a Video model.
|
|
static func from(video: Video, tags: [String] = [], tagsModifiedAt: Date? = nil, sortOrder: Int = 0) -> Bookmark {
|
|
let sourceRaw: String
|
|
var provider: String?
|
|
var instanceURL: String?
|
|
var uuid: String?
|
|
var extractor: String?
|
|
var externalURL: String?
|
|
|
|
switch video.id.source {
|
|
case .global(let prov):
|
|
sourceRaw = "global"
|
|
provider = prov
|
|
case .federated(_, let instance):
|
|
sourceRaw = "federated"
|
|
instanceURL = instance.absoluteString
|
|
uuid = video.id.uuid
|
|
case .extracted(let ext, let originalURL):
|
|
sourceRaw = "extracted"
|
|
extractor = ext
|
|
externalURL = originalURL.absoluteString
|
|
}
|
|
|
|
return Bookmark(
|
|
videoID: video.id.videoID,
|
|
sourceRawValue: sourceRaw,
|
|
globalProvider: provider,
|
|
instanceURLString: instanceURL,
|
|
peertubeUUID: uuid,
|
|
externalExtractor: extractor,
|
|
externalURLString: externalURL,
|
|
title: video.title,
|
|
authorName: video.author.name,
|
|
authorID: video.author.id,
|
|
duration: video.duration,
|
|
thumbnailURLString: video.bestThumbnail?.url.absoluteString,
|
|
isLive: video.isLive,
|
|
viewCount: video.viewCount,
|
|
publishedAt: video.publishedAt,
|
|
publishedText: video.publishedText,
|
|
tags: tags,
|
|
tagsModifiedAt: tagsModifiedAt,
|
|
sortOrder: sortOrder
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview Support
|
|
|
|
extension Bookmark {
|
|
/// A sample bookmark for SwiftUI previews.
|
|
static var preview: Bookmark {
|
|
Bookmark(
|
|
videoID: "dQw4w9WgXcQ",
|
|
sourceRawValue: "global",
|
|
globalProvider: "youtube",
|
|
title: "Sample Video Title",
|
|
authorName: "Sample Channel",
|
|
authorID: "UC123",
|
|
duration: 212,
|
|
thumbnailURLString: "https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
|
|
isLive: false,
|
|
viewCount: 1_234_567,
|
|
publishedAt: Date().addingTimeInterval(-86400 * 3),
|
|
publishedText: "3 days ago",
|
|
note: "Great video about SwiftUI patterns and best practices",
|
|
tags: ["Swift", "iOS", "Tutorial", "SwiftUI", "Xcode"]
|
|
)
|
|
}
|
|
}
|