// // JXPlaybackProgressManager.m // AICity // // Feature: 003-ios-api-https // Task: T036 - iOS播放进度本地存储 // Created on 2025-10-14. // #import "JXPlaybackProgressManager.h" #import "JXPlaybackProgress.h" #import "JXPlaybackProgressModel.h" #import // CoreData实体名称 static NSString * const kPlaybackProgressEntityName = @"PlaybackProgressEntity"; @implementation JXProgressStats @end @interface JXPlaybackProgressManager () @property (nonatomic, strong) NSManagedObjectContext *managedObjectContext; @property (nonatomic, strong) NSManagedObjectModel *managedObjectModel; @property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator; @end @implementation JXPlaybackProgressManager #pragma mark - Singleton + (instancetype)sharedManager { static JXPlaybackProgressManager *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; [sharedInstance setupCoreDataStack]; }); return sharedInstance; } #pragma mark - CoreData Stack - (void)setupCoreDataStack { // 创建托管对象模型 NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"JuXingDataModel" withExtension:@"momd"]; if (!modelURL) { // 如果没有.momd文件,创建程序化模型 [self createProgrammaticModel]; } else { self.managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; } // 创建持久化存储协调器 self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel]; // 添加持久化存储 NSURL *storeURL = [self applicationDocumentsDirectory]; storeURL = [storeURL URLByAppendingPathComponent:@"JuXingDataModel.sqlite"]; NSError *error = nil; NSDictionary *options = @{ NSMigratePersistentStoresAutomaticallyOption: @YES, NSInferMappingModelAutomaticallyOption: @YES }; if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { NSLog(@"CoreData setup error: %@", error.localizedDescription); } // 创建托管对象上下文 self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; self.managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator; } - (void)createProgrammaticModel { // 程序化创建CoreData模型 self.managedObjectModel = [[NSManagedObjectModel alloc] init]; // 创建PlaybackProgressEntity实体 NSEntityDescription *entity = [[NSEntityDescription alloc] init]; entity.name = kPlaybackProgressEntityName; entity.managedObjectClassName = @"NSManagedObject"; // 添加属性 NSMutableArray *properties = [NSMutableArray array]; // jxEpisodeId (主键) NSAttributeDescription *jxEpisodeId = [[NSAttributeDescription alloc] init]; jxEpisodeId.name = @"jxEpisodeId"; jxEpisodeId.attributeType = NSStringAttributeType; jxEpisodeId.optional = NO; [properties addObject:jxEpisodeId]; // jxDramaId NSAttributeDescription *jxDramaId = [[NSAttributeDescription alloc] init]; jxDramaId.name = @"jxDramaId"; jxDramaId.attributeType = NSStringAttributeType; jxDramaId.optional = NO; [properties addObject:jxDramaId]; // position NSAttributeDescription *position = [[NSAttributeDescription alloc] init]; position.name = @"position"; position.attributeType = NSInteger32AttributeType; position.defaultValue = @0; [properties addObject:position]; // duration NSAttributeDescription *duration = [[NSAttributeDescription alloc] init]; duration.name = @"duration"; duration.attributeType = NSInteger32AttributeType; duration.defaultValue = @0; [properties addObject:duration]; // percentage NSAttributeDescription *percentage = [[NSAttributeDescription alloc] init]; percentage.name = @"percentage"; percentage.attributeType = NSInteger32AttributeType; percentage.defaultValue = @0; [properties addObject:percentage]; // isCompleted NSAttributeDescription *isCompleted = [[NSAttributeDescription alloc] init]; isCompleted.name = @"isCompleted"; isCompleted.attributeType = NSBooleanAttributeType; isCompleted.defaultValue = @NO; [properties addObject:isCompleted]; // deviceId NSAttributeDescription *deviceId = [[NSAttributeDescription alloc] init]; deviceId.name = @"deviceId"; deviceId.attributeType = NSStringAttributeType; deviceId.optional = NO; [properties addObject:deviceId]; // deviceType NSAttributeDescription *deviceType = [[NSAttributeDescription alloc] init]; deviceType.name = @"deviceType"; deviceType.attributeType = NSStringAttributeType; deviceType.defaultValue = @"ios"; [properties addObject:deviceType]; // deviceName NSAttributeDescription *deviceName = [[NSAttributeDescription alloc] init]; deviceName.name = @"deviceName"; deviceName.attributeType = NSStringAttributeType; deviceName.optional = NO; [properties addObject:deviceName]; // updatedAt NSAttributeDescription *updatedAt = [[NSAttributeDescription alloc] init]; updatedAt.name = @"updatedAt"; updatedAt.attributeType = NSDateAttributeType; updatedAt.optional = NO; [properties addObject:updatedAt]; // syncedAt NSAttributeDescription *syncedAt = [[NSAttributeDescription alloc] init]; syncedAt.name = @"syncedAt"; syncedAt.attributeType = NSDateAttributeType; syncedAt.optional = YES; [properties addObject:syncedAt]; // syncStatus NSAttributeDescription *syncStatus = [[NSAttributeDescription alloc] init]; syncStatus.name = @"syncStatus"; syncStatus.attributeType = NSStringAttributeType; syncStatus.defaultValue = @"pending"; [properties addObject:syncStatus]; entity.properties = properties; self.managedObjectModel.entities = @[entity]; } - (NSURL *)applicationDocumentsDirectory { return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; } #pragma mark - Device Info - (NSDictionary *)getDeviceInfo { NSString *deviceId = [[[UIDevice currentDevice] identifierForVendor] UUIDString] ?: @"unknown"; NSString *deviceType = @"ios"; NSString *deviceName = [[UIDevice currentDevice] name]; return @{ @"deviceId": deviceId, @"deviceType": deviceType, @"deviceName": deviceName }; } #pragma mark - Save Methods - (void)saveProgressWithDramaId:(NSString *)dramaId episodeId:(NSString *)episodeId position:(NSInteger)position duration:(NSInteger)duration isCompleted:(BOOL)isCompleted completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ // 查找现有记录 NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; NSManagedObject *progressEntity = nil; if (results.count > 0) { progressEntity = results.firstObject; } else { progressEntity = [NSEntityDescription insertNewObjectForEntityForName:kPlaybackProgressEntityName inManagedObjectContext:self.managedObjectContext]; } // 设置属性 NSDictionary *deviceInfo = [self getDeviceInfo]; NSInteger calculatedPercentage = duration > 0 ? MIN(100, MAX(0, (position * 100 / duration))) : 0; [progressEntity setValue:episodeId forKey:@"jxEpisodeId"]; [progressEntity setValue:dramaId forKey:@"jxDramaId"]; [progressEntity setValue:@(position) forKey:@"position"]; [progressEntity setValue:@(duration) forKey:@"duration"]; [progressEntity setValue:@(calculatedPercentage) forKey:@"percentage"]; [progressEntity setValue:@(isCompleted) forKey:@"isCompleted"]; [progressEntity setValue:deviceInfo[@"deviceId"] forKey:@"deviceId"]; [progressEntity setValue:deviceInfo[@"deviceType"] forKey:@"deviceType"]; [progressEntity setValue:deviceInfo[@"deviceName"] forKey:@"deviceName"]; [progressEntity setValue:[NSDate date] forKey:@"updatedAt"]; [progressEntity setValue:@"pending" forKey:@"syncStatus"]; // 保存上下文 NSError *saveError = nil; BOOL success = [self.managedObjectContext save:&saveError]; dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success, saveError); } }); }]; } - (void)saveProgress:(JXPlaybackProgress *)progress completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self saveProgressWithDramaId:progress.jxDramaId episodeId:progress.jxEpisodeId position:progress.position duration:progress.duration isCompleted:progress.isCompleted completion:completion]; } - (void)saveProgressBatch:(NSArray *)progressList completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSError *error = nil; BOOL allSuccess = YES; for (JXPlaybackProgress *progress in progressList) { // 查找现有记录 NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", progress.jxEpisodeId]; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; if (error) { allSuccess = NO; break; } NSManagedObject *progressEntity = nil; if (results.count > 0) { progressEntity = results.firstObject; } else { progressEntity = [NSEntityDescription insertNewObjectForEntityForName:kPlaybackProgressEntityName inManagedObjectContext:self.managedObjectContext]; } // 设置属性 NSDictionary *deviceInfo = [self getDeviceInfo]; NSInteger calculatedPercentage = progress.duration > 0 ? MIN(100, MAX(0, (progress.position * 100 / progress.duration))) : 0; [progressEntity setValue:progress.jxEpisodeId forKey:@"jxEpisodeId"]; [progressEntity setValue:progress.jxDramaId forKey:@"jxDramaId"]; [progressEntity setValue:@(progress.position) forKey:@"position"]; [progressEntity setValue:@(progress.duration) forKey:@"duration"]; [progressEntity setValue:@(calculatedPercentage) forKey:@"percentage"]; [progressEntity setValue:@(progress.isCompleted) forKey:@"isCompleted"]; [progressEntity setValue:deviceInfo[@"deviceId"] forKey:@"deviceId"]; [progressEntity setValue:deviceInfo[@"deviceType"] forKey:@"deviceType"]; [progressEntity setValue:deviceInfo[@"deviceName"] forKey:@"deviceName"]; [progressEntity setValue:[NSDate date] forKey:@"updatedAt"]; [progressEntity setValue:@"pending" forKey:@"syncStatus"]; } if (allSuccess) { allSuccess = [self.managedObjectContext save:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(allSuccess, error); } }); }]; } #pragma mark - Fetch Methods - (void)getProgressWithEpisodeId:(NSString *)episodeId completion:(void(^)(JXPlaybackProgress * _Nullable progress, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId]; fetchRequest.fetchLimit = 1; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; JXPlaybackProgress *progress = nil; if (results.count > 0) { progress = [self convertManagedObjectToProgress:results.firstObject]; } dispatch_async(dispatch_get_main_queue(), ^{ completion(progress, error); }); }]; } - (void)getDramaProgressWithDramaId:(NSString *)dramaId completion:(void(^)(NSArray * _Nullable progressList, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxDramaId == %@", dramaId]; fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; NSMutableArray *progressList = [NSMutableArray array]; for (NSManagedObject *managedObject in results) { JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject]; if (progress) { [progressList addObject:progress]; } } dispatch_async(dispatch_get_main_queue(), ^{ completion([progressList copy], error); }); }]; } - (void)getAllProgressWithCompletion:(void(^)(NSArray * _Nullable progressList, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; NSMutableArray *progressList = [NSMutableArray array]; for (NSManagedObject *managedObject in results) { JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject]; if (progress) { [progressList addObject:progress]; } } dispatch_async(dispatch_get_main_queue(), ^{ completion([progressList copy], error); }); }]; } - (void)getRecentProgressWithLimit:(NSInteger)limit completion:(void(^)(NSArray * _Nullable progressList, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]]; fetchRequest.fetchLimit = limit; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; NSMutableArray *progressList = [NSMutableArray array]; for (NSManagedObject *managedObject in results) { JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject]; if (progress) { [progressList addObject:progress]; } } dispatch_async(dispatch_get_main_queue(), ^{ completion([progressList copy], error); }); }]; } - (void)getCompletedProgressWithCompletion:(void(^)(NSArray * _Nullable progressList, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"isCompleted == YES"]; fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; NSMutableArray *progressList = [NSMutableArray array]; for (NSManagedObject *managedObject in results) { JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject]; if (progress) { [progressList addObject:progress]; } } dispatch_async(dispatch_get_main_queue(), ^{ completion([progressList copy], error); }); }]; } - (void)getUnsyncedProgressWithCompletion:(void(^)(NSArray * _Nullable progressList, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"syncStatus == %@ OR syncStatus == %@", @"pending", @"failed"]; fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:YES]]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; NSMutableArray *progressList = [NSMutableArray array]; for (NSManagedObject *managedObject in results) { JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject]; if (progress) { [progressList addObject:progress]; } } dispatch_async(dispatch_get_main_queue(), ^{ completion([progressList copy], error); }); }]; } #pragma mark - Update Methods - (void)markAsSyncedWithEpisodeId:(NSString *)episodeId completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self updateSyncStatusWithEpisodeId:episodeId status:@"synced" completion:completion]; } - (void)markSyncFailedWithEpisodeId:(NSString *)episodeId completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self updateSyncStatusWithEpisodeId:episodeId status:@"failed" completion:completion]; } - (void)updateSyncStatusWithEpisodeId:(NSString *)episodeId status:(NSString *)status completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId]; fetchRequest.fetchLimit = 1; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; BOOL success = NO; if (results.count > 0) { NSManagedObject *progressEntity = results.firstObject; [progressEntity setValue:status forKey:@"syncStatus"]; [progressEntity setValue:[NSDate date] forKey:@"syncedAt"]; success = [self.managedObjectContext save:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success, error); } }); }]; } - (void)markAsSyncedBatchWithEpisodeIds:(NSArray *)episodeIds completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId IN %@", episodeIds]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; BOOL success = YES; if (!error) { for (NSManagedObject *progressEntity in results) { [progressEntity setValue:@"synced" forKey:@"syncStatus"]; [progressEntity setValue:[NSDate date] forKey:@"syncedAt"]; } success = [self.managedObjectContext save:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success, error); } }); }]; } #pragma mark - Delete Methods - (void)deleteProgressWithEpisodeId:(NSString *)episodeId completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; BOOL success = YES; if (!error) { for (NSManagedObject *progressEntity in results) { [self.managedObjectContext deleteObject:progressEntity]; } success = [self.managedObjectContext save:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success, error); } }); }]; } - (void)deleteDramaProgressWithDramaId:(NSString *)dramaId completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxDramaId == %@", dramaId]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; BOOL success = YES; if (!error) { for (NSManagedObject *progressEntity in results) { [self.managedObjectContext deleteObject:progressEntity]; } success = [self.managedObjectContext save:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success, error); } }); }]; } - (void)cleanupExpiredProgressWithCompletion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ // 90天前的时间 NSDate *expireDate = [NSDate dateWithTimeIntervalSinceNow:-(90 * 24 * 60 * 60)]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"updatedAt < %@", expireDate]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; BOOL success = YES; if (!error) { for (NSManagedObject *progressEntity in results) { [self.managedObjectContext deleteObject:progressEntity]; } success = [self.managedObjectContext save:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success, error); } }); }]; } - (void)deleteAllProgressWithCompletion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; NSError *error = nil; NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error]; BOOL success = YES; if (!error) { for (NSManagedObject *progressEntity in results) { [self.managedObjectContext deleteObject:progressEntity]; } success = [self.managedObjectContext save:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ if (completion) { completion(success, error); } }); }]; } #pragma mark - Statistics Methods - (void)getProgressStatsWithCompletion:(void(^)(JXProgressStats * _Nullable stats, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSError *error = nil; JXProgressStats *stats = [[JXProgressStats alloc] init]; // 总数量 NSFetchRequest *totalRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; stats.totalCount = [self.managedObjectContext countForFetchRequest:totalRequest error:&error]; if (!error) { // 已完成数量 NSFetchRequest *completedRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; completedRequest.predicate = [NSPredicate predicateWithFormat:@"isCompleted == YES"]; stats.completedCount = [self.managedObjectContext countForFetchRequest:completedRequest error:&error]; } if (!error) { // 未同步数量 NSFetchRequest *unsyncedRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; unsyncedRequest.predicate = [NSPredicate predicateWithFormat:@"syncStatus == %@ OR syncStatus == %@", @"pending", @"failed"]; stats.unsyncedCount = [self.managedObjectContext countForFetchRequest:unsyncedRequest error:&error]; } if (!error) { // 最近7天数量 NSDate *sevenDaysAgo = [NSDate dateWithTimeIntervalSinceNow:-(7 * 24 * 60 * 60)]; NSFetchRequest *recentRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; recentRequest.predicate = [NSPredicate predicateWithFormat:@"updatedAt >= %@", sevenDaysAgo]; stats.recentCount = [self.managedObjectContext countForFetchRequest:recentRequest error:&error]; } dispatch_async(dispatch_get_main_queue(), ^{ completion(error ? nil : stats, error); }); }]; } - (void)hasProgressWithEpisodeId:(NSString *)episodeId completion:(void(^)(BOOL hasProgress, NSError * _Nullable error))completion { [self.managedObjectContext performBlock:^{ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName]; fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId]; NSError *error = nil; NSUInteger count = [self.managedObjectContext countForFetchRequest:fetchRequest error:&error]; dispatch_async(dispatch_get_main_queue(), ^{ completion(count > 0, error); }); }]; } - (void)getProgressPercentageWithEpisodeId:(NSString *)episodeId completion:(void(^)(NSInteger percentage, NSError * _Nullable error))completion { [self getProgressWithEpisodeId:episodeId completion:^(JXPlaybackProgress * _Nullable progress, NSError * _Nullable error) { completion(progress ? progress.percentage : 0, error); }]; } - (void)isCompletedWithEpisodeId:(NSString *)episodeId completion:(void(^)(BOOL isCompleted, NSError * _Nullable error))completion { [self getProgressWithEpisodeId:episodeId completion:^(JXPlaybackProgress * _Nullable progress, NSError * _Nullable error) { completion(progress ? progress.isCompleted : NO, error); }]; } #pragma mark - Helper Methods - (JXPlaybackProgress *)convertManagedObjectToProgress:(NSManagedObject *)managedObject { if (!managedObject) return nil; JXPlaybackProgress *progress = [[JXPlaybackProgress alloc] init]; progress.jxEpisodeId = [managedObject valueForKey:@"jxEpisodeId"]; progress.jxDramaId = [managedObject valueForKey:@"jxDramaId"]; progress.position = [[managedObject valueForKey:@"position"] integerValue]; progress.duration = [[managedObject valueForKey:@"duration"] integerValue]; progress.percentage = [[managedObject valueForKey:@"percentage"] integerValue]; progress.isCompleted = [[managedObject valueForKey:@"isCompleted"] boolValue]; progress.deviceId = [managedObject valueForKey:@"deviceId"]; progress.deviceType = [managedObject valueForKey:@"deviceType"]; progress.deviceName = [managedObject valueForKey:@"deviceName"]; // 格式化更新时间 NSDate *updatedAt = [managedObject valueForKey:@"updatedAt"]; if (updatedAt) { NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'"; formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; progress.updatedAt = [formatter stringFromDate:updatedAt]; } return progress; } @end