// // JXPlaybackProgressStorage.m // AICity // // Feature: 003-ios-api-https // 播放进度本地存储管理器实现 // #import "JXPlaybackProgressStorage.h" @interface JXPlaybackProgressStorage () @property (nonatomic, strong) NSPersistentContainer *persistentContainer; @property (nonatomic, strong) NSManagedObjectContext *context; @end @implementation JXPlaybackProgressStorage + (instancetype)sharedStorage { static JXPlaybackProgressStorage *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (instancetype)init { self = [super init]; if (self) { [self setupCoreDataStack]; } return self; } #pragma mark - Core Data Stack - (void)setupCoreDataStack { // 创建NSManagedObjectModel NSManagedObjectModel *model = [self createManagedObjectModel]; // 创建NSPersistentStoreCoordinator NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; // 获取存储路径 NSURL *storeURL = [self storeURL]; // 添加持久化存储 NSError *error = nil; NSDictionary *options = @{ NSMigratePersistentStoresAutomaticallyOption: @YES, NSInferMappingModelAutomaticallyOption: @YES }; if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) { NSLog(@"Error adding persistent store: %@", error); // 如果添加失败,尝试删除旧数据库并重新创建 [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]; [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:nil]; } // 创建NSManagedObjectContext self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; self.context.persistentStoreCoordinator = coordinator; } - (NSManagedObjectModel *)createManagedObjectModel { NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init]; // 创建实体描述 NSEntityDescription *entity = [[NSEntityDescription alloc] init]; [entity setName:@"JXPlaybackProgress"]; [entity setManagedObjectClassName:@"JXPlaybackProgressModel"]; // 定义属性 NSMutableArray *properties = [NSMutableArray array]; // episodeId (String, 主键) NSAttributeDescription *episodeIdAttr = [[NSAttributeDescription alloc] init]; [episodeIdAttr setName:@"episodeId"]; [episodeIdAttr setAttributeType:NSStringAttributeType]; [episodeIdAttr setOptional:NO]; [properties addObject:episodeIdAttr]; // dramaId (String) NSAttributeDescription *dramaIdAttr = [[NSAttributeDescription alloc] init]; [dramaIdAttr setName:@"dramaId"]; [dramaIdAttr setAttributeType:NSStringAttributeType]; [dramaIdAttr setOptional:NO]; [properties addObject:dramaIdAttr]; // position (Integer64) NSAttributeDescription *positionAttr = [[NSAttributeDescription alloc] init]; [positionAttr setName:@"position"]; [positionAttr setAttributeType:NSInteger64AttributeType]; [positionAttr setOptional:NO]; [properties addObject:positionAttr]; // duration (Integer64) NSAttributeDescription *durationAttr = [[NSAttributeDescription alloc] init]; [durationAttr setName:@"duration"]; [durationAttr setAttributeType:NSInteger64AttributeType]; [durationAttr setOptional:NO]; [properties addObject:durationAttr]; // progress (Float) NSAttributeDescription *progressAttr = [[NSAttributeDescription alloc] init]; [progressAttr setName:@"progress"]; [progressAttr setAttributeType:NSFloatAttributeType]; [progressAttr setOptional:NO]; [properties addObject:progressAttr]; // isCompleted (Boolean) NSAttributeDescription *isCompletedAttr = [[NSAttributeDescription alloc] init]; [isCompletedAttr setName:@"isCompleted"]; [isCompletedAttr setAttributeType:NSBooleanAttributeType]; [isCompletedAttr setOptional:NO]; [properties addObject:isCompletedAttr]; // updatedAt (Date) NSAttributeDescription *updatedAtAttr = [[NSAttributeDescription alloc] init]; [updatedAtAttr setName:@"updatedAt"]; [updatedAtAttr setAttributeType:NSDateAttributeType]; [updatedAtAttr setOptional:NO]; [properties addObject:updatedAtAttr]; // synced (Boolean) NSAttributeDescription *syncedAttr = [[NSAttributeDescription alloc] init]; [syncedAttr setName:@"synced"]; [syncedAttr setAttributeType:NSBooleanAttributeType]; [syncedAttr setOptional:NO]; [properties addObject:syncedAttr]; // 设置属性到实体 [entity setProperties:properties]; // 添加实体到模型 [model setEntities:@[entity]]; return model; } - (NSURL *)storeURL { NSURL *documentsURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; return [documentsURL URLByAppendingPathComponent:@"JXPlaybackProgress.sqlite"]; } #pragma mark - Public Methods - (void)saveProgressWithEpisodeId:(NSString *)episodeId dramaId:(NSString *)dramaId position:(int64_t)position duration:(int64_t)duration isCompleted:(BOOL)isCompleted synced:(BOOL)synced completion:(void (^)(BOOL, NSError * _Nullable))completion { // 计算进度百分比 float progress = (duration > 0) ? (float)position / (float)duration : 0.0f; if (progress < 0.0f) progress = 0.0f; if (progress > 1.0f) progress = 1.0f; // 查找或创建实体 JXPlaybackProgressModel *progressModel = [self getProgressWithEpisodeId:episodeId]; if (!progressModel) { progressModel = [NSEntityDescription insertNewObjectForEntityForName:@"JXPlaybackProgress" inManagedObjectContext:self.context]; progressModel.episodeId = episodeId; } // 更新属性 progressModel.dramaId = dramaId; progressModel.position = position; progressModel.duration = duration; progressModel.progress = progress; progressModel.isCompleted = isCompleted; progressModel.updatedAt = [NSDate date]; progressModel.synced = synced; // 保存上下文 NSError *error = nil; BOOL success = [self.context save:&error]; if (completion) { completion(success, error); } if (!success) { NSLog(@"Error saving progress: %@", error); } } - (JXPlaybackProgressModel *)getProgressWithEpisodeId:(NSString *)episodeId { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"]; request.predicate = [NSPredicate predicateWithFormat:@"episodeId == %@", episodeId]; request.fetchLimit = 1; NSError *error = nil; NSArray *results = [self.context executeFetchRequest:request error:&error]; if (error) { NSLog(@"Error fetching progress: %@", error); return nil; } return results.firstObject; } - (NSArray *)getProgressByDramaId:(NSString *)dramaId { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"]; request.predicate = [NSPredicate predicateWithFormat:@"dramaId == %@", dramaId]; request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]]; NSError *error = nil; NSArray *results = [self.context executeFetchRequest:request error:&error]; if (error) { NSLog(@"Error fetching progress by drama: %@", error); return @[]; } return results ?: @[]; } - (NSArray *)getUnsyncedProgresses { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"]; request.predicate = [NSPredicate predicateWithFormat:@"synced == NO"]; request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:YES]]; NSError *error = nil; NSArray *results = [self.context executeFetchRequest:request error:&error]; if (error) { NSLog(@"Error fetching unsynced progress: %@", error); return @[]; } return results ?: @[]; } - (void)markAsSyncedWithEpisodeId:(NSString *)episodeId { JXPlaybackProgressModel *progressModel = [self getProgressWithEpisodeId:episodeId]; if (progressModel) { progressModel.synced = YES; [self.context save:nil]; } } - (void)deleteProgressWithEpisodeId:(NSString *)episodeId { JXPlaybackProgressModel *progressModel = [self getProgressWithEpisodeId:episodeId]; if (progressModel) { [self.context deleteObject:progressModel]; [self.context save:nil]; } } - (void)deleteProgressByDramaId:(NSString *)dramaId { NSArray *progresses = [self getProgressByDramaId:dramaId]; for (JXPlaybackProgressModel *progress in progresses) { [self.context deleteObject:progress]; } [self.context save:nil]; } - (void)clearAll { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"]; NSBatchDeleteRequest *deleteRequest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request]; NSError *error = nil; [self.context.persistentStoreCoordinator executeRequest:deleteRequest withContext:self.context error:&error]; if (error) { NSLog(@"Error clearing all progress: %@", error); } } - (void)cleanupOldRecords { // 删除7天前的记录 NSDate *sevenDaysAgo = [NSDate dateWithTimeIntervalSinceNow:-(7 * 24 * 60 * 60)]; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"]; request.predicate = [NSPredicate predicateWithFormat:@"updatedAt < %@", sevenDaysAgo]; NSError *error = nil; NSArray *oldRecords = [self.context executeFetchRequest:request error:&error]; if (!error) { for (JXPlaybackProgressModel *record in oldRecords) { [self.context deleteObject:record]; } [self.context save:nil]; } } @end