| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760 |
- //
- // 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 <UIKit/UIKit.h>
- // 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<JXPlaybackProgress *> *)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<JXPlaybackProgress *> * _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<JXPlaybackProgress *> * _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<JXPlaybackProgress *> * _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<JXPlaybackProgress *> * _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<JXPlaybackProgress *> * _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<NSString *> *)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
|