mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
300
Yattee/Data/Bookmark.swift
Normal file
300
Yattee/Data/Bookmark.swift
Normal file
@@ -0,0 +1,300 @@
|
||||
//
|
||||
// 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"]
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user