VLCThumbnailsCache.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. /*****************************************************************************
  2. * VLCThumbnailsCache.m
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2013-2015 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Authors: Gleb Pinigin <gpinigin # gmail.com>
  9. * Felix Paul Kühne <fkuehne # videolan.org>
  10. * Carola Nitz <caro # videolan.org>
  11. *
  12. * Refer to the COPYING file of the official project for license.
  13. *****************************************************************************/
  14. #import "VLC for iOS-Prefix.pch"
  15. #import "VLCThumbnailsCache.h"
  16. #import <CommonCrypto/CommonDigest.h>
  17. #import "UIImage+Blur.h"
  18. @interface VLCThumbnailsCache() {
  19. NSInteger MaxCacheSize;
  20. NSCache *_thumbnailCache;
  21. NSCache *_thumbnailCacheMetadata;
  22. NSInteger _currentDeviceIdiom;
  23. }
  24. @end
  25. @implementation VLCThumbnailsCache
  26. #define MAX_CACHE_SIZE_IPHONE 21 // three times the number of items shown on iPhone 5
  27. #define MAX_CACHE_SIZE_IPAD 27 // three times the number of items shown on iPad
  28. #define MAX_CACHE_SIZE_WATCH 15 // three times the number of items shown on 42mm Watch
  29. - (instancetype)init
  30. {
  31. self = [super init];
  32. if (self) {
  33. _currentDeviceIdiom = [[UIDevice currentDevice] userInterfaceIdiom];
  34. MaxCacheSize = 0;
  35. switch (_currentDeviceIdiom) {
  36. case UIUserInterfaceIdiomPad:
  37. MaxCacheSize = MAX_CACHE_SIZE_IPAD;
  38. break;
  39. case UIUserInterfaceIdiomPhone:
  40. MaxCacheSize = MAX_CACHE_SIZE_IPHONE;
  41. break;
  42. default:
  43. MaxCacheSize = MAX_CACHE_SIZE_WATCH;
  44. break;
  45. }
  46. _thumbnailCache = [[NSCache alloc] init];
  47. _thumbnailCacheMetadata = [[NSCache alloc] init];
  48. [_thumbnailCache setCountLimit: MaxCacheSize];
  49. [_thumbnailCacheMetadata setCountLimit: MaxCacheSize];
  50. }
  51. return self;
  52. }
  53. + (instancetype)sharedThumbnailCache
  54. {
  55. static dispatch_once_t onceToken;
  56. static VLCThumbnailsCache *sharedThumbnailCache;
  57. dispatch_once(&onceToken, ^{
  58. sharedThumbnailCache = [[VLCThumbnailsCache alloc] init];
  59. });
  60. return sharedThumbnailCache;
  61. }
  62. - (NSString *)_md5FromString:(NSString *)string
  63. {
  64. const char *ptr = [string UTF8String];
  65. unsigned char md5Buffer[CC_MD5_DIGEST_LENGTH];
  66. CC_MD5(ptr, (unsigned int)strlen(ptr), md5Buffer);
  67. NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
  68. for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
  69. [output appendFormat:@"%02x",md5Buffer[i]];
  70. return [NSString stringWithString:output];
  71. }
  72. + (UIImage *)thumbnailForMediaItemWithTitle:(NSString *)title Artist:(NSString*)artist andAlbumName:(NSString*)albumname
  73. {
  74. return [UIImage imageWithContentsOfFile:[[VLCThumbnailsCache sharedThumbnailCache] artworkPathForMediaItemWithTitle:title Artist:artist andAlbumName:albumname]];
  75. }
  76. - (NSString *)artworkPathForMediaItemWithTitle:(NSString *)title Artist:(NSString*)artist andAlbumName:(NSString*)albumname
  77. {
  78. NSString *artworkURL;
  79. NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  80. NSString *cacheDir = searchPaths[0];
  81. cacheDir = [cacheDir stringByAppendingFormat:@"/%@", [[NSBundle mainBundle] bundleIdentifier]];
  82. if (artist.length == 0 || albumname.length == 0) {
  83. /* Use generated hash to find art */
  84. artworkURL = [cacheDir stringByAppendingFormat:@"/art/arturl/%@/art.jpg", [self _md5FromString:title]];
  85. } else {
  86. /* Otherwise, it was cached by artist and album */
  87. artworkURL = [cacheDir stringByAppendingFormat:@"/art/artistalbum/%@/%@/art.jpg", artist, albumname];
  88. }
  89. return artworkURL;
  90. }
  91. - (NSString *)_getArtworkPathFromMedia:(MLFile *)file
  92. {
  93. NSString *artist, *album, *title;
  94. if (file.isAlbumTrack) {
  95. artist = file.albumTrack.artist;
  96. album = file.albumTrack.album.name;
  97. }
  98. title = file.title;
  99. return [self artworkPathForMediaItemWithTitle:title Artist:artist andAlbumName:album];
  100. }
  101. + (UIImage *)thumbnailForManagedObject:(NSManagedObject *)object
  102. {
  103. UIImage *thumbnail;
  104. VLCThumbnailsCache *cache = [VLCThumbnailsCache sharedThumbnailCache];
  105. if ([object isKindOfClass:[MLShow class]]) {
  106. thumbnail = [cache thumbnailForShow:(MLShow *)object];
  107. } else if ([object isKindOfClass:[MLShowEpisode class]]) {
  108. MLFile *anyFileFromEpisode = [(MLShowEpisode *)object files].anyObject;
  109. thumbnail = [cache thumbnailForMediaFile:anyFileFromEpisode];
  110. } else if ([object isKindOfClass:[MLLabel class]]) {
  111. thumbnail = [cache thumbnailForLabel:(MLLabel *)object];
  112. } else if ([object isKindOfClass:[MLAlbum class]]) {
  113. MLFile *anyFileFromAnyTrack = [[(MLAlbum *)object tracks].anyObject files].anyObject;
  114. thumbnail = [cache thumbnailForMediaFile:anyFileFromAnyTrack];
  115. } else if ([object isKindOfClass:[MLAlbumTrack class]]) {
  116. MLFile *anyFileFromTrack = [(MLAlbumTrack *)object files].anyObject;
  117. thumbnail = [cache thumbnailForMediaFile:anyFileFromTrack];
  118. } else {
  119. thumbnail = [cache thumbnailForMediaFile:(MLFile *)object];
  120. }
  121. return thumbnail;
  122. }
  123. - (UIImage *)thumbnailForMediaFile:(MLFile *)mediaFile
  124. {
  125. if (mediaFile == nil || mediaFile.objectID == nil)
  126. return nil;
  127. NSManagedObjectID *objID = mediaFile.objectID;
  128. UIImage *displayedImage = [_thumbnailCache objectForKey:objID];
  129. if (displayedImage)
  130. return displayedImage;
  131. if (mediaFile.isAlbumTrack || mediaFile.isShowEpisode)
  132. displayedImage = [UIImage imageWithContentsOfFile:[self _getArtworkPathFromMedia:mediaFile]];
  133. if (!displayedImage)
  134. displayedImage = mediaFile.computedThumbnail;
  135. if (displayedImage)
  136. [_thumbnailCache setObject:displayedImage forKey:objID];
  137. return displayedImage;
  138. }
  139. - (UIImage *)thumbnailForShow:(MLShow *)mediaShow
  140. {
  141. NSManagedObjectID *objID = mediaShow.objectID;
  142. UIImage *displayedImage;
  143. BOOL forceRefresh = NO;
  144. NSUInteger count = [mediaShow.episodes count];
  145. NSNumber *previousCount = [_thumbnailCacheMetadata objectForKey:objID];
  146. if (previousCount.unsignedIntegerValue != count)
  147. forceRefresh = YES;
  148. if (!forceRefresh) {
  149. displayedImage = [_thumbnailCache objectForKey:objID];
  150. if (displayedImage)
  151. return displayedImage;
  152. }
  153. NSUInteger fileNumber = count > 3 ? 3 : count;
  154. NSArray *episodes = [mediaShow.episodes allObjects];
  155. NSMutableArray *files = [[NSMutableArray alloc] init];
  156. for (NSUInteger x = 0; x < count; x++) {
  157. /* this is a multi-threaded app, so the episode object might be there already,
  158. * but without an assigned file, so we need to check for its existance (#13128) */
  159. if ([episodes[x] files].anyObject != nil)
  160. [files addObject:[episodes[x] files].anyObject];
  161. }
  162. displayedImage = [self clusterThumbFromFiles:files andNumber:fileNumber blur:NO];
  163. if (displayedImage) {
  164. [_thumbnailCache setObject:displayedImage forKey:objID];
  165. [_thumbnailCacheMetadata setObject:@(count) forKey:objID];
  166. }
  167. return displayedImage;
  168. }
  169. - (UIImage *)thumbnailForLabel:(MLLabel *)mediaLabel
  170. {
  171. NSManagedObjectID *objID = mediaLabel.objectID;
  172. UIImage *displayedImage;
  173. BOOL forceRefresh = NO;
  174. NSUInteger count = [mediaLabel.files count];
  175. NSNumber *previousCount = [_thumbnailCacheMetadata objectForKey:objID];
  176. if (previousCount.unsignedIntegerValue != count)
  177. forceRefresh = YES;
  178. if (!forceRefresh) {
  179. displayedImage = [_thumbnailCache objectForKey:objID];
  180. if (displayedImage)
  181. return displayedImage;
  182. }
  183. NSUInteger fileNumber = count > 3 ? 3 : count;
  184. NSArray *files = [mediaLabel.files allObjects];
  185. BOOL blur = NO;
  186. if (SYSTEM_RUNS_IOS7_OR_LATER)
  187. blur = YES;
  188. displayedImage = [self clusterThumbFromFiles:files andNumber:fileNumber blur:blur];
  189. if (displayedImage) {
  190. [_thumbnailCache setObject:displayedImage forKey:objID];
  191. [_thumbnailCacheMetadata setObject:@(count) forKey:objID];
  192. }
  193. return displayedImage;
  194. }
  195. - (UIImage *)clusterThumbFromFiles:(NSArray *)files andNumber:(NSUInteger)fileNumber blur:(BOOL)blurImage
  196. {
  197. UIImage *clusterThumb;
  198. CGSize imageSize;
  199. if (_currentDeviceIdiom == UIUserInterfaceIdiomPad) {
  200. if ([UIScreen mainScreen].scale==2.0)
  201. imageSize = CGSizeMake(682., 384.);
  202. else
  203. imageSize = CGSizeMake(341., 192.);
  204. } else if (_currentDeviceIdiom == UIUserInterfaceIdiomPhone) {
  205. if (SYSTEM_RUNS_IOS7_OR_LATER) {
  206. if ([UIScreen mainScreen].scale==2.0)
  207. imageSize = CGSizeMake(480., 270.);
  208. else
  209. imageSize = CGSizeMake(720., 405.);
  210. } else {
  211. if ([UIScreen mainScreen].scale==2.0)
  212. imageSize = CGSizeMake(258., 145.);
  213. else
  214. imageSize = CGSizeMake(129., 73.);
  215. }
  216. } else {
  217. imageSize = CGSizeMake(272., 120.);
  218. }
  219. UIGraphicsBeginImageContext(imageSize);
  220. NSUInteger iter = files.count < fileNumber ? files.count : fileNumber;
  221. for (NSUInteger i = 0; i < iter; i++) {
  222. MLFile *file = [files objectAtIndex:i];
  223. clusterThumb = [self thumbnailForMediaFile:file];
  224. CGContextRef context = UIGraphicsGetCurrentContext();
  225. CGFloat imagePartWidth = (imageSize.width / iter);
  226. //the rect in which the image should be drawn
  227. CGRect clippingRect = CGRectMake(imagePartWidth * i, 0, imagePartWidth, imageSize.height);
  228. CGContextSaveGState(context);
  229. CGContextClipToRect(context, clippingRect);
  230. //take the center of the clippingRect and calculate the offset from the original center
  231. CGFloat centerOffset = (imagePartWidth * i + imagePartWidth / 2) - imageSize.width / 2;
  232. //shift the rect to draw the middle of the image in the clippingrect
  233. CGRect drawingRect = CGRectMake(centerOffset, 0, imageSize.width, imageSize.height);
  234. [clusterThumb drawInRect:drawingRect];
  235. //get rid of the old clippingRect
  236. CGContextRestoreGState(context);
  237. }
  238. clusterThumb = UIGraphicsGetImageFromCurrentImageContext();
  239. UIGraphicsEndImageContext();
  240. if (!blurImage)
  241. return clusterThumb;
  242. return [UIImage applyBlurOnImage:clusterThumb withRadius:0.1];
  243. }
  244. @end