import CoreData import CoreMedia import Defaults import Foundation @objc(Watch) final class Watch: NSManagedObject, Identifiable { @Default(.watchedThreshold) private var watchedThreshold @Default(.saveHistory) private var saveHistory } extension Watch { @nonobjc class func fetchRequest() -> NSFetchRequest { NSFetchRequest(entityName: "Watch") } @nonobjc class func markAsWatched(videoID: String, duration: Double, context: NSManagedObjectContext) { let watchFetchRequest = Watch.fetchRequest() watchFetchRequest.predicate = NSPredicate(format: "videoID = %@", videoID as String) let results = try? context.fetch(watchFetchRequest) context.perform { let watch: Watch? if results?.isEmpty ?? true { watch = Watch(context: context) watch?.videoID = videoID } else { watch = results?.first } guard let watch else { return } watch.videoDuration = duration watch.stoppedAt = duration watch.watchedAt = Date() try? context.save() } } @NSManaged var videoID: String @NSManaged var videoDuration: Double @NSManaged var watchedAt: Date? @NSManaged var stoppedAt: Double var progress: Double { guard videoDuration.isFinite, !videoDuration.isZero else { return 0 } let progress = (stoppedAt / videoDuration) * 100 if progress >= Double(watchedThreshold) { return 100 } return min(max(progress, 0), 100) } var finished: Bool { progress >= Double(watchedThreshold) } var watchedAtString: String? { guard let watchedAt else { return nil } let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .full return formatter.localizedString(for: watchedAt, relativeTo: Date()) } var timeToRestart: CMTime? { finished ? nil : saveHistory ? .secondsInDefaultTimescale(stoppedAt) : nil } var video: Video { if !Video.VideoID.isValid(videoID), let url = URL(string: videoID) { return .local(url) } return Video(videoID: videoID) } }