JXPlaybackProgressStorage.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. //
  2. // JXPlaybackProgressStorage.m
  3. // AICity
  4. //
  5. // Feature: 003-ios-api-https
  6. // 播放进度本地存储管理器实现
  7. //
  8. #import "JXPlaybackProgressStorage.h"
  9. @interface JXPlaybackProgressStorage ()
  10. @property (nonatomic, strong) NSPersistentContainer *persistentContainer;
  11. @property (nonatomic, strong) NSManagedObjectContext *context;
  12. @end
  13. @implementation JXPlaybackProgressStorage
  14. + (instancetype)sharedStorage {
  15. static JXPlaybackProgressStorage *instance = nil;
  16. static dispatch_once_t onceToken;
  17. dispatch_once(&onceToken, ^{
  18. instance = [[self alloc] init];
  19. });
  20. return instance;
  21. }
  22. - (instancetype)init {
  23. self = [super init];
  24. if (self) {
  25. [self setupCoreDataStack];
  26. }
  27. return self;
  28. }
  29. #pragma mark - Core Data Stack
  30. - (void)setupCoreDataStack {
  31. // 创建NSManagedObjectModel
  32. NSManagedObjectModel *model = [self createManagedObjectModel];
  33. // 创建NSPersistentStoreCoordinator
  34. NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
  35. // 获取存储路径
  36. NSURL *storeURL = [self storeURL];
  37. // 添加持久化存储
  38. NSError *error = nil;
  39. NSDictionary *options = @{
  40. NSMigratePersistentStoresAutomaticallyOption: @YES,
  41. NSInferMappingModelAutomaticallyOption: @YES
  42. };
  43. if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType
  44. configuration:nil
  45. URL:storeURL
  46. options:options
  47. error:&error]) {
  48. NSLog(@"Error adding persistent store: %@", error);
  49. // 如果添加失败,尝试删除旧数据库并重新创建
  50. [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
  51. [coordinator addPersistentStoreWithType:NSSQLiteStoreType
  52. configuration:nil
  53. URL:storeURL
  54. options:options
  55. error:nil];
  56. }
  57. // 创建NSManagedObjectContext
  58. self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
  59. self.context.persistentStoreCoordinator = coordinator;
  60. }
  61. - (NSManagedObjectModel *)createManagedObjectModel {
  62. NSManagedObjectModel *model = [[NSManagedObjectModel alloc] init];
  63. // 创建实体描述
  64. NSEntityDescription *entity = [[NSEntityDescription alloc] init];
  65. [entity setName:@"JXPlaybackProgress"];
  66. [entity setManagedObjectClassName:@"JXPlaybackProgressModel"];
  67. // 定义属性
  68. NSMutableArray *properties = [NSMutableArray array];
  69. // episodeId (String, 主键)
  70. NSAttributeDescription *episodeIdAttr = [[NSAttributeDescription alloc] init];
  71. [episodeIdAttr setName:@"episodeId"];
  72. [episodeIdAttr setAttributeType:NSStringAttributeType];
  73. [episodeIdAttr setOptional:NO];
  74. [properties addObject:episodeIdAttr];
  75. // dramaId (String)
  76. NSAttributeDescription *dramaIdAttr = [[NSAttributeDescription alloc] init];
  77. [dramaIdAttr setName:@"dramaId"];
  78. [dramaIdAttr setAttributeType:NSStringAttributeType];
  79. [dramaIdAttr setOptional:NO];
  80. [properties addObject:dramaIdAttr];
  81. // position (Integer64)
  82. NSAttributeDescription *positionAttr = [[NSAttributeDescription alloc] init];
  83. [positionAttr setName:@"position"];
  84. [positionAttr setAttributeType:NSInteger64AttributeType];
  85. [positionAttr setOptional:NO];
  86. [properties addObject:positionAttr];
  87. // duration (Integer64)
  88. NSAttributeDescription *durationAttr = [[NSAttributeDescription alloc] init];
  89. [durationAttr setName:@"duration"];
  90. [durationAttr setAttributeType:NSInteger64AttributeType];
  91. [durationAttr setOptional:NO];
  92. [properties addObject:durationAttr];
  93. // progress (Float)
  94. NSAttributeDescription *progressAttr = [[NSAttributeDescription alloc] init];
  95. [progressAttr setName:@"progress"];
  96. [progressAttr setAttributeType:NSFloatAttributeType];
  97. [progressAttr setOptional:NO];
  98. [properties addObject:progressAttr];
  99. // isCompleted (Boolean)
  100. NSAttributeDescription *isCompletedAttr = [[NSAttributeDescription alloc] init];
  101. [isCompletedAttr setName:@"isCompleted"];
  102. [isCompletedAttr setAttributeType:NSBooleanAttributeType];
  103. [isCompletedAttr setOptional:NO];
  104. [properties addObject:isCompletedAttr];
  105. // updatedAt (Date)
  106. NSAttributeDescription *updatedAtAttr = [[NSAttributeDescription alloc] init];
  107. [updatedAtAttr setName:@"updatedAt"];
  108. [updatedAtAttr setAttributeType:NSDateAttributeType];
  109. [updatedAtAttr setOptional:NO];
  110. [properties addObject:updatedAtAttr];
  111. // synced (Boolean)
  112. NSAttributeDescription *syncedAttr = [[NSAttributeDescription alloc] init];
  113. [syncedAttr setName:@"synced"];
  114. [syncedAttr setAttributeType:NSBooleanAttributeType];
  115. [syncedAttr setOptional:NO];
  116. [properties addObject:syncedAttr];
  117. // 设置属性到实体
  118. [entity setProperties:properties];
  119. // 添加实体到模型
  120. [model setEntities:@[entity]];
  121. return model;
  122. }
  123. - (NSURL *)storeURL {
  124. NSURL *documentsURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
  125. return [documentsURL URLByAppendingPathComponent:@"JXPlaybackProgress.sqlite"];
  126. }
  127. #pragma mark - Public Methods
  128. - (void)saveProgressWithEpisodeId:(NSString *)episodeId
  129. dramaId:(NSString *)dramaId
  130. position:(int64_t)position
  131. duration:(int64_t)duration
  132. isCompleted:(BOOL)isCompleted
  133. synced:(BOOL)synced
  134. completion:(void (^)(BOOL, NSError * _Nullable))completion {
  135. // 计算进度百分比
  136. float progress = (duration > 0) ? (float)position / (float)duration : 0.0f;
  137. if (progress < 0.0f) progress = 0.0f;
  138. if (progress > 1.0f) progress = 1.0f;
  139. // 查找或创建实体
  140. JXPlaybackProgressModel *progressModel = [self getProgressWithEpisodeId:episodeId];
  141. if (!progressModel) {
  142. progressModel = [NSEntityDescription insertNewObjectForEntityForName:@"JXPlaybackProgress"
  143. inManagedObjectContext:self.context];
  144. progressModel.episodeId = episodeId;
  145. }
  146. // 更新属性
  147. progressModel.dramaId = dramaId;
  148. progressModel.position = position;
  149. progressModel.duration = duration;
  150. progressModel.progress = progress;
  151. progressModel.isCompleted = isCompleted;
  152. progressModel.updatedAt = [NSDate date];
  153. progressModel.synced = synced;
  154. // 保存上下文
  155. NSError *error = nil;
  156. BOOL success = [self.context save:&error];
  157. if (completion) {
  158. completion(success, error);
  159. }
  160. if (!success) {
  161. NSLog(@"Error saving progress: %@", error);
  162. }
  163. }
  164. - (JXPlaybackProgressModel *)getProgressWithEpisodeId:(NSString *)episodeId {
  165. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"];
  166. request.predicate = [NSPredicate predicateWithFormat:@"episodeId == %@", episodeId];
  167. request.fetchLimit = 1;
  168. NSError *error = nil;
  169. NSArray *results = [self.context executeFetchRequest:request error:&error];
  170. if (error) {
  171. NSLog(@"Error fetching progress: %@", error);
  172. return nil;
  173. }
  174. return results.firstObject;
  175. }
  176. - (NSArray<JXPlaybackProgressModel *> *)getProgressByDramaId:(NSString *)dramaId {
  177. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"];
  178. request.predicate = [NSPredicate predicateWithFormat:@"dramaId == %@", dramaId];
  179. request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]];
  180. NSError *error = nil;
  181. NSArray *results = [self.context executeFetchRequest:request error:&error];
  182. if (error) {
  183. NSLog(@"Error fetching progress by drama: %@", error);
  184. return @[];
  185. }
  186. return results ?: @[];
  187. }
  188. - (NSArray<JXPlaybackProgressModel *> *)getUnsyncedProgresses {
  189. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"];
  190. request.predicate = [NSPredicate predicateWithFormat:@"synced == NO"];
  191. request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:YES]];
  192. NSError *error = nil;
  193. NSArray *results = [self.context executeFetchRequest:request error:&error];
  194. if (error) {
  195. NSLog(@"Error fetching unsynced progress: %@", error);
  196. return @[];
  197. }
  198. return results ?: @[];
  199. }
  200. - (void)markAsSyncedWithEpisodeId:(NSString *)episodeId {
  201. JXPlaybackProgressModel *progressModel = [self getProgressWithEpisodeId:episodeId];
  202. if (progressModel) {
  203. progressModel.synced = YES;
  204. [self.context save:nil];
  205. }
  206. }
  207. - (void)deleteProgressWithEpisodeId:(NSString *)episodeId {
  208. JXPlaybackProgressModel *progressModel = [self getProgressWithEpisodeId:episodeId];
  209. if (progressModel) {
  210. [self.context deleteObject:progressModel];
  211. [self.context save:nil];
  212. }
  213. }
  214. - (void)deleteProgressByDramaId:(NSString *)dramaId {
  215. NSArray *progresses = [self getProgressByDramaId:dramaId];
  216. for (JXPlaybackProgressModel *progress in progresses) {
  217. [self.context deleteObject:progress];
  218. }
  219. [self.context save:nil];
  220. }
  221. - (void)clearAll {
  222. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"];
  223. NSBatchDeleteRequest *deleteRequest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:request];
  224. NSError *error = nil;
  225. [self.context.persistentStoreCoordinator executeRequest:deleteRequest withContext:self.context error:&error];
  226. if (error) {
  227. NSLog(@"Error clearing all progress: %@", error);
  228. }
  229. }
  230. - (void)cleanupOldRecords {
  231. // 删除7天前的记录
  232. NSDate *sevenDaysAgo = [NSDate dateWithTimeIntervalSinceNow:-(7 * 24 * 60 * 60)];
  233. NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"JXPlaybackProgress"];
  234. request.predicate = [NSPredicate predicateWithFormat:@"updatedAt < %@", sevenDaysAgo];
  235. NSError *error = nil;
  236. NSArray *oldRecords = [self.context executeFetchRequest:request error:&error];
  237. if (!error) {
  238. for (JXPlaybackProgressModel *record in oldRecords) {
  239. [self.context deleteObject:record];
  240. }
  241. [self.context save:nil];
  242. }
  243. }
  244. @end