FSParsePlaylistRequest.m 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /*
  2. * This file is part of the FreeStreamer project,
  3. * (C)Copyright 2011-2018 Matias Muhonen <mmu@iki.fi> 穆马帝
  4. * See the file ''LICENSE'' for using the code.
  5. *
  6. * https://github.com/muhku/FreeStreamer
  7. */
  8. #import "FSParsePlaylistRequest.h"
  9. #import "FSPlaylistItem.h"
  10. @interface FSParsePlaylistRequest ()
  11. - (void)parsePlaylistFromData:(NSData *)data;
  12. - (void)parsePlaylistM3U:(NSString *)playlist;
  13. - (void)parsePlaylistPLS:(NSString *)playlist;
  14. - (NSURL *)parseLocalFileUrl:(NSString *)fileUrl;
  15. @property (readonly) FSPlaylistFormat format;
  16. @end
  17. @implementation FSParsePlaylistRequest
  18. - (id)init
  19. {
  20. self = [super init];
  21. if (self) {
  22. }
  23. return self;
  24. }
  25. - (void)start
  26. {
  27. if (_task) {
  28. return;
  29. }
  30. NSURLRequest *request = [NSURLRequest requestWithURL:self.url
  31. cachePolicy:NSURLRequestUseProtocolCachePolicy
  32. timeoutInterval:10.0];
  33. NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
  34. delegate:self
  35. delegateQueue:[NSOperationQueue mainQueue]];
  36. @synchronized (self) {
  37. _receivedData = [NSMutableData data];
  38. _task = [session dataTaskWithRequest:request];
  39. _playlistItems = [[NSMutableArray alloc] init];
  40. _format = kFSPlaylistFormatNone;
  41. }
  42. [_task resume];
  43. }
  44. - (void)cancel
  45. {
  46. if (!_task) {
  47. return;
  48. }
  49. @synchronized (self) {
  50. [_task cancel];
  51. _task = nil;
  52. }
  53. }
  54. /*
  55. * =======================================
  56. * Properties
  57. * =======================================
  58. */
  59. - (NSMutableArray *)playlistItems
  60. {
  61. return [_playlistItems copy];
  62. }
  63. - (FSPlaylistFormat)format
  64. {
  65. return _format;
  66. }
  67. /*
  68. * =======================================
  69. * Private
  70. * =======================================
  71. */
  72. - (void)parsePlaylistFromData:(NSData *)data
  73. {
  74. NSString *playlistData = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
  75. if (_format == kFSPlaylistFormatM3U) {
  76. [self parsePlaylistM3U:playlistData];
  77. if ([_playlistItems count] == 0) {
  78. // If we failed to grab any playlist items, still try
  79. // to parse it in another format; perhaps the server
  80. // mistakingly identified the playlist format
  81. [self parsePlaylistPLS:playlistData];
  82. }
  83. } else if (_format == kFSPlaylistFormatPLS) {
  84. [self parsePlaylistPLS:playlistData];
  85. if ([_playlistItems count] == 0) {
  86. // If we failed to grab any playlist items, still try
  87. // to parse it in another format; perhaps the server
  88. // mistakingly identified the playlist format
  89. [self parsePlaylistM3U:playlistData];
  90. }
  91. }
  92. if ([_playlistItems count] == 0) {
  93. /*
  94. * Fail if we failed to parse any items from the playlist.
  95. */
  96. self.onFailure();
  97. }
  98. }
  99. - (void)parsePlaylistM3U:(NSString *)playlist
  100. {
  101. [_playlistItems removeAllObjects];
  102. for (NSString *line in [playlist componentsSeparatedByString:@"\n"]) {
  103. if ([line hasPrefix:@"#"]) {
  104. /* metadata, skip */
  105. continue;
  106. }
  107. if ([line hasPrefix:@"http://"] ||
  108. [line hasPrefix:@"https://"]) {
  109. FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
  110. item.url = [NSURL URLWithString:[line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
  111. [_playlistItems addObject:item];
  112. } else if ([line hasPrefix:@"file://"]) {
  113. FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
  114. item.url = [self parseLocalFileUrl:line];
  115. [_playlistItems addObject:item];
  116. }
  117. }
  118. }
  119. - (void)parsePlaylistPLS:(NSString *)playlist
  120. {
  121. [_playlistItems removeAllObjects];
  122. NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
  123. size_t i = 0;
  124. for (NSString *rawLine in [playlist componentsSeparatedByString:@"\n"]) {
  125. NSString *line = [rawLine stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  126. if (i == 0) {
  127. if ([[line lowercaseString] hasPrefix:@"[playlist]"]) {
  128. i++;
  129. continue;
  130. } else {
  131. // Invalid playlist; the first line should indicate that this is a playlist
  132. return;
  133. }
  134. }
  135. // Ignore empty lines
  136. if ([line length] == 0) {
  137. i++;
  138. continue;
  139. }
  140. // Not an empty line; so expect that this is a key/value pair
  141. NSRange r = [line rangeOfString:@"="];
  142. // Invalid format, key/value pair not found
  143. if (r.length == 0) {
  144. return;
  145. }
  146. NSString *key = [[line substringToIndex:r.location] lowercaseString];
  147. NSString *value = [line substringFromIndex:r.location + 1];
  148. props[key] = value;
  149. i++;
  150. }
  151. NSInteger numItems = [[props valueForKey:@"numberofentries"] integerValue];
  152. if (numItems == 0) {
  153. // Invalid playlist; number of playlist items not defined
  154. return;
  155. }
  156. for (i=0; i < numItems; i++) {
  157. FSPlaylistItem *item = [[FSPlaylistItem alloc] init];
  158. NSString *title = [props valueForKey:[NSString stringWithFormat:@"title%lu", (i+1)]];
  159. item.title = title;
  160. NSString *file = [props valueForKey:[NSString stringWithFormat:@"file%lu", (i+1)]];
  161. if ([file hasPrefix:@"http://"] ||
  162. [file hasPrefix:@"https://"]) {
  163. item.url = [NSURL URLWithString:file];
  164. [_playlistItems addObject:item];
  165. } else if ([file hasPrefix:@"file://"]) {
  166. item.url = [self parseLocalFileUrl:file];
  167. [_playlistItems addObject:item];
  168. }
  169. }
  170. }
  171. - (NSURL *)parseLocalFileUrl:(NSString *)fileUrl
  172. {
  173. // Resolve the local bundle URL
  174. NSString *path = [fileUrl substringFromIndex:7];
  175. NSRange range = [path rangeOfString:@"." options:NSBackwardsSearch];
  176. NSString *fileName = [path substringWithRange:NSMakeRange(0, range.location)];
  177. NSString *suffix = [path substringWithRange:NSMakeRange(range.location + 1, [path length] - [fileName length] - 1)];
  178. return [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:fileName ofType:suffix]];
  179. }
  180. /*
  181. * =======================================
  182. * NSURLSessionDelegate
  183. * =======================================
  184. */
  185. - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
  186. didReceiveResponse:(NSURLResponse *)response
  187. completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
  188. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  189. _httpStatus = [httpResponse statusCode];
  190. NSString *contentType = response.MIMEType;
  191. NSString *absoluteUrl = [response.URL absoluteString];
  192. _format = kFSPlaylistFormatNone;
  193. if ([contentType isEqualToString:@"audio/x-mpegurl"] ||
  194. [contentType isEqualToString:@"application/x-mpegurl"]) {
  195. _format = kFSPlaylistFormatM3U;
  196. } else if ([contentType isEqualToString:@"audio/x-scpls"] ||
  197. [contentType isEqualToString:@"application/pls+xml"]) {
  198. _format = kFSPlaylistFormatPLS;
  199. } else if ([contentType isEqualToString:@"text/plain"]) {
  200. /* The server did not provide meaningful content type;
  201. last resort: check the file suffix, if there is one */
  202. if ([absoluteUrl hasSuffix:@".m3u"]) {
  203. _format = kFSPlaylistFormatM3U;
  204. } else if ([absoluteUrl hasSuffix:@".pls"]) {
  205. _format = kFSPlaylistFormatPLS;
  206. }
  207. }
  208. if (_format == kFSPlaylistFormatNone) {
  209. #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
  210. NSLog(@"FSParsePlaylistRequest: Unable to determine the type of the playlist for URL: %@", _url);
  211. #endif
  212. self.onFailure();
  213. } else {
  214. completionHandler(NSURLSessionResponseAllow);
  215. }
  216. [_receivedData setLength:0];
  217. }
  218. - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
  219. {
  220. // Resume the Download Task manually because apparently iOS does not do it automatically?!
  221. [downloadTask resume];
  222. }
  223. - (void)URLSession:(NSURLSession *)session
  224. dataTask:(NSURLSessionDataTask *)dataTask
  225. didReceiveData:(NSData *)data {
  226. [_receivedData appendData:data];
  227. }
  228. - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
  229. didCompleteWithError:(nullable NSError *)error {
  230. if(error) {
  231. @synchronized (self) {
  232. _task = nil;
  233. _receivedData = nil;
  234. }
  235. #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
  236. NSLog(@"FSParsePlaylistRequest: Connection failed for URL: %@, error %@", _url, [error localizedDescription]);
  237. #endif
  238. self.onFailure();
  239. } else {
  240. @synchronized (self) {
  241. _task = nil;
  242. }
  243. if (_httpStatus != 200) {
  244. #if defined(DEBUG) || (TARGET_IPHONE_SIMULATOR)
  245. NSLog(@"FSParsePlaylistRequest: Unable to receive playlist from URL: %@", _url);
  246. #endif
  247. self.onFailure();
  248. return;
  249. }
  250. [self parsePlaylistFromData:_receivedData];
  251. self.onCompletion();
  252. }
  253. }
  254. @end