JXPlaybackProgressManager.m 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. //
  2. // JXPlaybackProgressManager.m
  3. // AICity
  4. //
  5. // Feature: 003-ios-api-https
  6. // Task: T036 - iOS播放进度本地存储
  7. // Created on 2025-10-14.
  8. //
  9. #import "JXPlaybackProgressManager.h"
  10. #import "JXPlaybackProgress.h"
  11. #import "JXPlaybackProgressModel.h"
  12. #import <UIKit/UIKit.h>
  13. // CoreData实体名称
  14. static NSString * const kPlaybackProgressEntityName = @"PlaybackProgressEntity";
  15. @implementation JXProgressStats
  16. @end
  17. @interface JXPlaybackProgressManager ()
  18. @property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
  19. @property (nonatomic, strong) NSManagedObjectModel *managedObjectModel;
  20. @property (nonatomic, strong) NSPersistentStoreCoordinator *persistentStoreCoordinator;
  21. @end
  22. @implementation JXPlaybackProgressManager
  23. #pragma mark - Singleton
  24. + (instancetype)sharedManager {
  25. static JXPlaybackProgressManager *sharedInstance = nil;
  26. static dispatch_once_t onceToken;
  27. dispatch_once(&onceToken, ^{
  28. sharedInstance = [[self alloc] init];
  29. [sharedInstance setupCoreDataStack];
  30. });
  31. return sharedInstance;
  32. }
  33. #pragma mark - CoreData Stack
  34. - (void)setupCoreDataStack {
  35. // 创建托管对象模型
  36. NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"JuXingDataModel" withExtension:@"momd"];
  37. if (!modelURL) {
  38. // 如果没有.momd文件,创建程序化模型
  39. [self createProgrammaticModel];
  40. } else {
  41. self.managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
  42. }
  43. // 创建持久化存储协调器
  44. self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
  45. // 添加持久化存储
  46. NSURL *storeURL = [self applicationDocumentsDirectory];
  47. storeURL = [storeURL URLByAppendingPathComponent:@"JuXingDataModel.sqlite"];
  48. NSError *error = nil;
  49. NSDictionary *options = @{
  50. NSMigratePersistentStoresAutomaticallyOption: @YES,
  51. NSInferMappingModelAutomaticallyOption: @YES
  52. };
  53. if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
  54. configuration:nil
  55. URL:storeURL
  56. options:options
  57. error:&error]) {
  58. NSLog(@"CoreData setup error: %@", error.localizedDescription);
  59. }
  60. // 创建托管对象上下文
  61. self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
  62. self.managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
  63. }
  64. - (void)createProgrammaticModel {
  65. // 程序化创建CoreData模型
  66. self.managedObjectModel = [[NSManagedObjectModel alloc] init];
  67. // 创建PlaybackProgressEntity实体
  68. NSEntityDescription *entity = [[NSEntityDescription alloc] init];
  69. entity.name = kPlaybackProgressEntityName;
  70. entity.managedObjectClassName = @"NSManagedObject";
  71. // 添加属性
  72. NSMutableArray *properties = [NSMutableArray array];
  73. // jxEpisodeId (主键)
  74. NSAttributeDescription *jxEpisodeId = [[NSAttributeDescription alloc] init];
  75. jxEpisodeId.name = @"jxEpisodeId";
  76. jxEpisodeId.attributeType = NSStringAttributeType;
  77. jxEpisodeId.optional = NO;
  78. [properties addObject:jxEpisodeId];
  79. // jxDramaId
  80. NSAttributeDescription *jxDramaId = [[NSAttributeDescription alloc] init];
  81. jxDramaId.name = @"jxDramaId";
  82. jxDramaId.attributeType = NSStringAttributeType;
  83. jxDramaId.optional = NO;
  84. [properties addObject:jxDramaId];
  85. // position
  86. NSAttributeDescription *position = [[NSAttributeDescription alloc] init];
  87. position.name = @"position";
  88. position.attributeType = NSInteger32AttributeType;
  89. position.defaultValue = @0;
  90. [properties addObject:position];
  91. // duration
  92. NSAttributeDescription *duration = [[NSAttributeDescription alloc] init];
  93. duration.name = @"duration";
  94. duration.attributeType = NSInteger32AttributeType;
  95. duration.defaultValue = @0;
  96. [properties addObject:duration];
  97. // percentage
  98. NSAttributeDescription *percentage = [[NSAttributeDescription alloc] init];
  99. percentage.name = @"percentage";
  100. percentage.attributeType = NSInteger32AttributeType;
  101. percentage.defaultValue = @0;
  102. [properties addObject:percentage];
  103. // isCompleted
  104. NSAttributeDescription *isCompleted = [[NSAttributeDescription alloc] init];
  105. isCompleted.name = @"isCompleted";
  106. isCompleted.attributeType = NSBooleanAttributeType;
  107. isCompleted.defaultValue = @NO;
  108. [properties addObject:isCompleted];
  109. // deviceId
  110. NSAttributeDescription *deviceId = [[NSAttributeDescription alloc] init];
  111. deviceId.name = @"deviceId";
  112. deviceId.attributeType = NSStringAttributeType;
  113. deviceId.optional = NO;
  114. [properties addObject:deviceId];
  115. // deviceType
  116. NSAttributeDescription *deviceType = [[NSAttributeDescription alloc] init];
  117. deviceType.name = @"deviceType";
  118. deviceType.attributeType = NSStringAttributeType;
  119. deviceType.defaultValue = @"ios";
  120. [properties addObject:deviceType];
  121. // deviceName
  122. NSAttributeDescription *deviceName = [[NSAttributeDescription alloc] init];
  123. deviceName.name = @"deviceName";
  124. deviceName.attributeType = NSStringAttributeType;
  125. deviceName.optional = NO;
  126. [properties addObject:deviceName];
  127. // updatedAt
  128. NSAttributeDescription *updatedAt = [[NSAttributeDescription alloc] init];
  129. updatedAt.name = @"updatedAt";
  130. updatedAt.attributeType = NSDateAttributeType;
  131. updatedAt.optional = NO;
  132. [properties addObject:updatedAt];
  133. // syncedAt
  134. NSAttributeDescription *syncedAt = [[NSAttributeDescription alloc] init];
  135. syncedAt.name = @"syncedAt";
  136. syncedAt.attributeType = NSDateAttributeType;
  137. syncedAt.optional = YES;
  138. [properties addObject:syncedAt];
  139. // syncStatus
  140. NSAttributeDescription *syncStatus = [[NSAttributeDescription alloc] init];
  141. syncStatus.name = @"syncStatus";
  142. syncStatus.attributeType = NSStringAttributeType;
  143. syncStatus.defaultValue = @"pending";
  144. [properties addObject:syncStatus];
  145. entity.properties = properties;
  146. self.managedObjectModel.entities = @[entity];
  147. }
  148. - (NSURL *)applicationDocumentsDirectory {
  149. return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
  150. inDomains:NSUserDomainMask] lastObject];
  151. }
  152. #pragma mark - Device Info
  153. - (NSDictionary *)getDeviceInfo {
  154. NSString *deviceId = [[[UIDevice currentDevice] identifierForVendor] UUIDString] ?: @"unknown";
  155. NSString *deviceType = @"ios";
  156. NSString *deviceName = [[UIDevice currentDevice] name];
  157. return @{
  158. @"deviceId": deviceId,
  159. @"deviceType": deviceType,
  160. @"deviceName": deviceName
  161. };
  162. }
  163. #pragma mark - Save Methods
  164. - (void)saveProgressWithDramaId:(NSString *)dramaId
  165. episodeId:(NSString *)episodeId
  166. position:(NSInteger)position
  167. duration:(NSInteger)duration
  168. isCompleted:(BOOL)isCompleted
  169. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  170. [self.managedObjectContext performBlock:^{
  171. // 查找现有记录
  172. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  173. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId];
  174. NSError *error = nil;
  175. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  176. NSManagedObject *progressEntity = nil;
  177. if (results.count > 0) {
  178. progressEntity = results.firstObject;
  179. } else {
  180. progressEntity = [NSEntityDescription insertNewObjectForEntityForName:kPlaybackProgressEntityName
  181. inManagedObjectContext:self.managedObjectContext];
  182. }
  183. // 设置属性
  184. NSDictionary *deviceInfo = [self getDeviceInfo];
  185. NSInteger calculatedPercentage = duration > 0 ? MIN(100, MAX(0, (position * 100 / duration))) : 0;
  186. [progressEntity setValue:episodeId forKey:@"jxEpisodeId"];
  187. [progressEntity setValue:dramaId forKey:@"jxDramaId"];
  188. [progressEntity setValue:@(position) forKey:@"position"];
  189. [progressEntity setValue:@(duration) forKey:@"duration"];
  190. [progressEntity setValue:@(calculatedPercentage) forKey:@"percentage"];
  191. [progressEntity setValue:@(isCompleted) forKey:@"isCompleted"];
  192. [progressEntity setValue:deviceInfo[@"deviceId"] forKey:@"deviceId"];
  193. [progressEntity setValue:deviceInfo[@"deviceType"] forKey:@"deviceType"];
  194. [progressEntity setValue:deviceInfo[@"deviceName"] forKey:@"deviceName"];
  195. [progressEntity setValue:[NSDate date] forKey:@"updatedAt"];
  196. [progressEntity setValue:@"pending" forKey:@"syncStatus"];
  197. // 保存上下文
  198. NSError *saveError = nil;
  199. BOOL success = [self.managedObjectContext save:&saveError];
  200. dispatch_async(dispatch_get_main_queue(), ^{
  201. if (completion) {
  202. completion(success, saveError);
  203. }
  204. });
  205. }];
  206. }
  207. - (void)saveProgress:(JXPlaybackProgress *)progress
  208. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  209. [self saveProgressWithDramaId:progress.jxDramaId
  210. episodeId:progress.jxEpisodeId
  211. position:progress.position
  212. duration:progress.duration
  213. isCompleted:progress.isCompleted
  214. completion:completion];
  215. }
  216. - (void)saveProgressBatch:(NSArray<JXPlaybackProgress *> *)progressList
  217. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  218. [self.managedObjectContext performBlock:^{
  219. NSError *error = nil;
  220. BOOL allSuccess = YES;
  221. for (JXPlaybackProgress *progress in progressList) {
  222. // 查找现有记录
  223. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  224. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", progress.jxEpisodeId];
  225. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  226. if (error) {
  227. allSuccess = NO;
  228. break;
  229. }
  230. NSManagedObject *progressEntity = nil;
  231. if (results.count > 0) {
  232. progressEntity = results.firstObject;
  233. } else {
  234. progressEntity = [NSEntityDescription insertNewObjectForEntityForName:kPlaybackProgressEntityName
  235. inManagedObjectContext:self.managedObjectContext];
  236. }
  237. // 设置属性
  238. NSDictionary *deviceInfo = [self getDeviceInfo];
  239. NSInteger calculatedPercentage = progress.duration > 0 ? MIN(100, MAX(0, (progress.position * 100 / progress.duration))) : 0;
  240. [progressEntity setValue:progress.jxEpisodeId forKey:@"jxEpisodeId"];
  241. [progressEntity setValue:progress.jxDramaId forKey:@"jxDramaId"];
  242. [progressEntity setValue:@(progress.position) forKey:@"position"];
  243. [progressEntity setValue:@(progress.duration) forKey:@"duration"];
  244. [progressEntity setValue:@(calculatedPercentage) forKey:@"percentage"];
  245. [progressEntity setValue:@(progress.isCompleted) forKey:@"isCompleted"];
  246. [progressEntity setValue:deviceInfo[@"deviceId"] forKey:@"deviceId"];
  247. [progressEntity setValue:deviceInfo[@"deviceType"] forKey:@"deviceType"];
  248. [progressEntity setValue:deviceInfo[@"deviceName"] forKey:@"deviceName"];
  249. [progressEntity setValue:[NSDate date] forKey:@"updatedAt"];
  250. [progressEntity setValue:@"pending" forKey:@"syncStatus"];
  251. }
  252. if (allSuccess) {
  253. allSuccess = [self.managedObjectContext save:&error];
  254. }
  255. dispatch_async(dispatch_get_main_queue(), ^{
  256. if (completion) {
  257. completion(allSuccess, error);
  258. }
  259. });
  260. }];
  261. }
  262. #pragma mark - Fetch Methods
  263. - (void)getProgressWithEpisodeId:(NSString *)episodeId
  264. completion:(void(^)(JXPlaybackProgress * _Nullable progress, NSError * _Nullable error))completion {
  265. [self.managedObjectContext performBlock:^{
  266. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  267. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId];
  268. fetchRequest.fetchLimit = 1;
  269. NSError *error = nil;
  270. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  271. JXPlaybackProgress *progress = nil;
  272. if (results.count > 0) {
  273. progress = [self convertManagedObjectToProgress:results.firstObject];
  274. }
  275. dispatch_async(dispatch_get_main_queue(), ^{
  276. completion(progress, error);
  277. });
  278. }];
  279. }
  280. - (void)getDramaProgressWithDramaId:(NSString *)dramaId
  281. completion:(void(^)(NSArray<JXPlaybackProgress *> * _Nullable progressList, NSError * _Nullable error))completion {
  282. [self.managedObjectContext performBlock:^{
  283. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  284. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxDramaId == %@", dramaId];
  285. fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]];
  286. NSError *error = nil;
  287. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  288. NSMutableArray *progressList = [NSMutableArray array];
  289. for (NSManagedObject *managedObject in results) {
  290. JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject];
  291. if (progress) {
  292. [progressList addObject:progress];
  293. }
  294. }
  295. dispatch_async(dispatch_get_main_queue(), ^{
  296. completion([progressList copy], error);
  297. });
  298. }];
  299. }
  300. - (void)getAllProgressWithCompletion:(void(^)(NSArray<JXPlaybackProgress *> * _Nullable progressList, NSError * _Nullable error))completion {
  301. [self.managedObjectContext performBlock:^{
  302. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  303. fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]];
  304. NSError *error = nil;
  305. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  306. NSMutableArray *progressList = [NSMutableArray array];
  307. for (NSManagedObject *managedObject in results) {
  308. JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject];
  309. if (progress) {
  310. [progressList addObject:progress];
  311. }
  312. }
  313. dispatch_async(dispatch_get_main_queue(), ^{
  314. completion([progressList copy], error);
  315. });
  316. }];
  317. }
  318. - (void)getRecentProgressWithLimit:(NSInteger)limit
  319. completion:(void(^)(NSArray<JXPlaybackProgress *> * _Nullable progressList, NSError * _Nullable error))completion {
  320. [self.managedObjectContext performBlock:^{
  321. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  322. fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]];
  323. fetchRequest.fetchLimit = limit;
  324. NSError *error = nil;
  325. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  326. NSMutableArray *progressList = [NSMutableArray array];
  327. for (NSManagedObject *managedObject in results) {
  328. JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject];
  329. if (progress) {
  330. [progressList addObject:progress];
  331. }
  332. }
  333. dispatch_async(dispatch_get_main_queue(), ^{
  334. completion([progressList copy], error);
  335. });
  336. }];
  337. }
  338. - (void)getCompletedProgressWithCompletion:(void(^)(NSArray<JXPlaybackProgress *> * _Nullable progressList, NSError * _Nullable error))completion {
  339. [self.managedObjectContext performBlock:^{
  340. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  341. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"isCompleted == YES"];
  342. fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:NO]];
  343. NSError *error = nil;
  344. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  345. NSMutableArray *progressList = [NSMutableArray array];
  346. for (NSManagedObject *managedObject in results) {
  347. JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject];
  348. if (progress) {
  349. [progressList addObject:progress];
  350. }
  351. }
  352. dispatch_async(dispatch_get_main_queue(), ^{
  353. completion([progressList copy], error);
  354. });
  355. }];
  356. }
  357. - (void)getUnsyncedProgressWithCompletion:(void(^)(NSArray<JXPlaybackProgress *> * _Nullable progressList, NSError * _Nullable error))completion {
  358. [self.managedObjectContext performBlock:^{
  359. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  360. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"syncStatus == %@ OR syncStatus == %@", @"pending", @"failed"];
  361. fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"updatedAt" ascending:YES]];
  362. NSError *error = nil;
  363. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  364. NSMutableArray *progressList = [NSMutableArray array];
  365. for (NSManagedObject *managedObject in results) {
  366. JXPlaybackProgress *progress = [self convertManagedObjectToProgress:managedObject];
  367. if (progress) {
  368. [progressList addObject:progress];
  369. }
  370. }
  371. dispatch_async(dispatch_get_main_queue(), ^{
  372. completion([progressList copy], error);
  373. });
  374. }];
  375. }
  376. #pragma mark - Update Methods
  377. - (void)markAsSyncedWithEpisodeId:(NSString *)episodeId
  378. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  379. [self updateSyncStatusWithEpisodeId:episodeId status:@"synced" completion:completion];
  380. }
  381. - (void)markSyncFailedWithEpisodeId:(NSString *)episodeId
  382. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  383. [self updateSyncStatusWithEpisodeId:episodeId status:@"failed" completion:completion];
  384. }
  385. - (void)updateSyncStatusWithEpisodeId:(NSString *)episodeId
  386. status:(NSString *)status
  387. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  388. [self.managedObjectContext performBlock:^{
  389. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  390. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId];
  391. fetchRequest.fetchLimit = 1;
  392. NSError *error = nil;
  393. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  394. BOOL success = NO;
  395. if (results.count > 0) {
  396. NSManagedObject *progressEntity = results.firstObject;
  397. [progressEntity setValue:status forKey:@"syncStatus"];
  398. [progressEntity setValue:[NSDate date] forKey:@"syncedAt"];
  399. success = [self.managedObjectContext save:&error];
  400. }
  401. dispatch_async(dispatch_get_main_queue(), ^{
  402. if (completion) {
  403. completion(success, error);
  404. }
  405. });
  406. }];
  407. }
  408. - (void)markAsSyncedBatchWithEpisodeIds:(NSArray<NSString *> *)episodeIds
  409. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  410. [self.managedObjectContext performBlock:^{
  411. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  412. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId IN %@", episodeIds];
  413. NSError *error = nil;
  414. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  415. BOOL success = YES;
  416. if (!error) {
  417. for (NSManagedObject *progressEntity in results) {
  418. [progressEntity setValue:@"synced" forKey:@"syncStatus"];
  419. [progressEntity setValue:[NSDate date] forKey:@"syncedAt"];
  420. }
  421. success = [self.managedObjectContext save:&error];
  422. }
  423. dispatch_async(dispatch_get_main_queue(), ^{
  424. if (completion) {
  425. completion(success, error);
  426. }
  427. });
  428. }];
  429. }
  430. #pragma mark - Delete Methods
  431. - (void)deleteProgressWithEpisodeId:(NSString *)episodeId
  432. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  433. [self.managedObjectContext performBlock:^{
  434. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  435. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId];
  436. NSError *error = nil;
  437. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  438. BOOL success = YES;
  439. if (!error) {
  440. for (NSManagedObject *progressEntity in results) {
  441. [self.managedObjectContext deleteObject:progressEntity];
  442. }
  443. success = [self.managedObjectContext save:&error];
  444. }
  445. dispatch_async(dispatch_get_main_queue(), ^{
  446. if (completion) {
  447. completion(success, error);
  448. }
  449. });
  450. }];
  451. }
  452. - (void)deleteDramaProgressWithDramaId:(NSString *)dramaId
  453. completion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  454. [self.managedObjectContext performBlock:^{
  455. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  456. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxDramaId == %@", dramaId];
  457. NSError *error = nil;
  458. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  459. BOOL success = YES;
  460. if (!error) {
  461. for (NSManagedObject *progressEntity in results) {
  462. [self.managedObjectContext deleteObject:progressEntity];
  463. }
  464. success = [self.managedObjectContext save:&error];
  465. }
  466. dispatch_async(dispatch_get_main_queue(), ^{
  467. if (completion) {
  468. completion(success, error);
  469. }
  470. });
  471. }];
  472. }
  473. - (void)cleanupExpiredProgressWithCompletion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  474. [self.managedObjectContext performBlock:^{
  475. // 90天前的时间
  476. NSDate *expireDate = [NSDate dateWithTimeIntervalSinceNow:-(90 * 24 * 60 * 60)];
  477. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  478. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"updatedAt < %@", expireDate];
  479. NSError *error = nil;
  480. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  481. BOOL success = YES;
  482. if (!error) {
  483. for (NSManagedObject *progressEntity in results) {
  484. [self.managedObjectContext deleteObject:progressEntity];
  485. }
  486. success = [self.managedObjectContext save:&error];
  487. }
  488. dispatch_async(dispatch_get_main_queue(), ^{
  489. if (completion) {
  490. completion(success, error);
  491. }
  492. });
  493. }];
  494. }
  495. - (void)deleteAllProgressWithCompletion:(nullable void(^)(BOOL success, NSError * _Nullable error))completion {
  496. [self.managedObjectContext performBlock:^{
  497. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  498. NSError *error = nil;
  499. NSArray *results = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
  500. BOOL success = YES;
  501. if (!error) {
  502. for (NSManagedObject *progressEntity in results) {
  503. [self.managedObjectContext deleteObject:progressEntity];
  504. }
  505. success = [self.managedObjectContext save:&error];
  506. }
  507. dispatch_async(dispatch_get_main_queue(), ^{
  508. if (completion) {
  509. completion(success, error);
  510. }
  511. });
  512. }];
  513. }
  514. #pragma mark - Statistics Methods
  515. - (void)getProgressStatsWithCompletion:(void(^)(JXProgressStats * _Nullable stats, NSError * _Nullable error))completion {
  516. [self.managedObjectContext performBlock:^{
  517. NSError *error = nil;
  518. JXProgressStats *stats = [[JXProgressStats alloc] init];
  519. // 总数量
  520. NSFetchRequest *totalRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  521. stats.totalCount = [self.managedObjectContext countForFetchRequest:totalRequest error:&error];
  522. if (!error) {
  523. // 已完成数量
  524. NSFetchRequest *completedRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  525. completedRequest.predicate = [NSPredicate predicateWithFormat:@"isCompleted == YES"];
  526. stats.completedCount = [self.managedObjectContext countForFetchRequest:completedRequest error:&error];
  527. }
  528. if (!error) {
  529. // 未同步数量
  530. NSFetchRequest *unsyncedRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  531. unsyncedRequest.predicate = [NSPredicate predicateWithFormat:@"syncStatus == %@ OR syncStatus == %@", @"pending", @"failed"];
  532. stats.unsyncedCount = [self.managedObjectContext countForFetchRequest:unsyncedRequest error:&error];
  533. }
  534. if (!error) {
  535. // 最近7天数量
  536. NSDate *sevenDaysAgo = [NSDate dateWithTimeIntervalSinceNow:-(7 * 24 * 60 * 60)];
  537. NSFetchRequest *recentRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  538. recentRequest.predicate = [NSPredicate predicateWithFormat:@"updatedAt >= %@", sevenDaysAgo];
  539. stats.recentCount = [self.managedObjectContext countForFetchRequest:recentRequest error:&error];
  540. }
  541. dispatch_async(dispatch_get_main_queue(), ^{
  542. completion(error ? nil : stats, error);
  543. });
  544. }];
  545. }
  546. - (void)hasProgressWithEpisodeId:(NSString *)episodeId
  547. completion:(void(^)(BOOL hasProgress, NSError * _Nullable error))completion {
  548. [self.managedObjectContext performBlock:^{
  549. NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:kPlaybackProgressEntityName];
  550. fetchRequest.predicate = [NSPredicate predicateWithFormat:@"jxEpisodeId == %@", episodeId];
  551. NSError *error = nil;
  552. NSUInteger count = [self.managedObjectContext countForFetchRequest:fetchRequest error:&error];
  553. dispatch_async(dispatch_get_main_queue(), ^{
  554. completion(count > 0, error);
  555. });
  556. }];
  557. }
  558. - (void)getProgressPercentageWithEpisodeId:(NSString *)episodeId
  559. completion:(void(^)(NSInteger percentage, NSError * _Nullable error))completion {
  560. [self getProgressWithEpisodeId:episodeId completion:^(JXPlaybackProgress * _Nullable progress, NSError * _Nullable error) {
  561. completion(progress ? progress.percentage : 0, error);
  562. }];
  563. }
  564. - (void)isCompletedWithEpisodeId:(NSString *)episodeId
  565. completion:(void(^)(BOOL isCompleted, NSError * _Nullable error))completion {
  566. [self getProgressWithEpisodeId:episodeId completion:^(JXPlaybackProgress * _Nullable progress, NSError * _Nullable error) {
  567. completion(progress ? progress.isCompleted : NO, error);
  568. }];
  569. }
  570. #pragma mark - Helper Methods
  571. - (JXPlaybackProgress *)convertManagedObjectToProgress:(NSManagedObject *)managedObject {
  572. if (!managedObject) return nil;
  573. JXPlaybackProgress *progress = [[JXPlaybackProgress alloc] init];
  574. progress.jxEpisodeId = [managedObject valueForKey:@"jxEpisodeId"];
  575. progress.jxDramaId = [managedObject valueForKey:@"jxDramaId"];
  576. progress.position = [[managedObject valueForKey:@"position"] integerValue];
  577. progress.duration = [[managedObject valueForKey:@"duration"] integerValue];
  578. progress.percentage = [[managedObject valueForKey:@"percentage"] integerValue];
  579. progress.isCompleted = [[managedObject valueForKey:@"isCompleted"] boolValue];
  580. progress.deviceId = [managedObject valueForKey:@"deviceId"];
  581. progress.deviceType = [managedObject valueForKey:@"deviceType"];
  582. progress.deviceName = [managedObject valueForKey:@"deviceName"];
  583. // 格式化更新时间
  584. NSDate *updatedAt = [managedObject valueForKey:@"updatedAt"];
  585. if (updatedAt) {
  586. NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
  587. formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
  588. formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
  589. progress.updatedAt = [formatter stringFromDate:updatedAt];
  590. }
  591. return progress;
  592. }
  593. @end