VLCHTTPConnection.m 31.7 KB
Newer Older
1 2 3 4
/*****************************************************************************
 * VLCHTTPConnection.m
 * VLC for iOS
 *****************************************************************************
5
 * Copyright (c) 2013-2018 VideoLAN. All rights reserved.
6 7 8
 * $Id$
 *
 * Authors: Felix Paul Kühne <fkuehne # videolan.org>
Felix Paul Kühne's avatar
Felix Paul Kühne committed
9 10
 *          Pierre Sagaspe <pierre.sagaspe # me.com>
 *          Carola Nitz <caro # videolan.org>
11 12 13 14
 *          Jean-Baptiste Kempf <jb # videolan.org>
 *
 * Refer to the COPYING file of the official project for license.
 *****************************************************************************/
15

Felix Paul Kühne's avatar
Felix Paul Kühne committed
16
#import "VLCActivityManager.h"
17 18 19 20 21 22
#import "VLCHTTPConnection.h"
#import "MultipartFormDataParser.h"
#import "HTTPMessage.h"
#import "HTTPDataResponse.h"
#import "HTTPFileResponse.h"
#import "MultipartMessageHeaderField.h"
23
#import "HTTPDynamicFileResponse.h"
24
#import "HTTPErrorResponse.h"
25
#import "NSString+SupportedMedia.h"
26
#import "UIDevice+VLC.h"
Felix Paul Kühne's avatar
Felix Paul Kühne committed
27
#import "VLCHTTPUploaderController.h"
28
#import "VLCMetaData.h"
29

30
#if TARGET_OS_IOS
31
#import "VLC-Swift.h"
32 33
#import "VLCThumbnailsCache.h"
#endif
34 35 36
#if TARGET_OS_TV
#import "VLCPlayerControlWebSocket.h"
#endif
37

38 39
@interface VLCHTTPConnection()
{
40 41 42
    MultipartFormDataParser *_parser;
    NSFileHandle *_storeFile;
    NSString *_filepath;
43 44
    UInt64 _contentLength;
    UInt64 _receivedContent;
45
#if TARGET_OS_TV
46
    NSMutableArray *_receivedFiles;
47
#endif
48 49 50 51 52 53 54 55
}
@end

@implementation VLCHTTPConnection

- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path
{
    // Add support for POST
Felix Paul Kühne's avatar
Felix Paul Kühne committed
56
    if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/upload.json"])
57
        return YES;
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82

    return [super supportsMethod:method atPath:path];
}

- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path
{
    // Inform HTTP server that we expect a body to accompany a POST request
    if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/upload.json"]) {
        // here we need to make sure, boundary is set in header
        NSString* contentType = [request headerField:@"Content-Type"];
        NSUInteger paramsSeparator = [contentType rangeOfString:@";"].location;
        if (NSNotFound == paramsSeparator)
            return NO;

        if (paramsSeparator >= contentType.length - 1)
            return NO;

        NSString* type = [contentType substringToIndex:paramsSeparator];
        if (![type isEqualToString:@"multipart/form-data"]) {
            // we expect multipart/form-data content type
            return NO;
        }

        // enumerate all params in content-type, and find boundary there
        NSArray* params = [[contentType substringFromIndex:paramsSeparator + 1] componentsSeparatedByString:@";"];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
83 84 85
        NSUInteger count = params.count;
        for (NSUInteger i = 0; i < count; i++) {
            NSString *param = params[i];
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
            paramsSeparator = [param rangeOfString:@"="].location;
            if ((NSNotFound == paramsSeparator) || paramsSeparator >= param.length - 1)
                continue;

            NSString* paramName = [param substringWithRange:NSMakeRange(1, paramsSeparator-1)];
            NSString* paramValue = [param substringFromIndex:paramsSeparator+1];

            if ([paramName isEqualToString: @"boundary"])
                // let's separate the boundary from content-type, to make it more handy to handle
                [request setHeaderField:@"boundary" value:paramValue];
        }
        // check if boundary specified
        if (nil == [request headerField:@"boundary"])
            return NO;

        return YES;
    }
    return [super expectsRequestBodyFromMethod:method atPath:path];
}

106
- (NSObject<HTTPResponse> *)_httpPOSTresponseUploadJSON
107
{
108 109
    return [[HTTPDataResponse alloc] initWithData:[@"\"OK\"" dataUsingEncoding:NSUTF8StringEncoding]];
}
Felix Paul Kühne's avatar
Felix Paul Kühne committed
110

111 112
- (BOOL)fileIsInDocumentFolder:(NSString*)filepath
{
113 114
    if (!filepath) return NO;

115 116 117 118 119
    NSError *error;

    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *directoryPath = [searchPaths firstObject];

120 121 122 123 124 125 126 127
    NSArray *array = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:directoryPath error:&error];

    if (error != nil) {
        APLog(@"checking filerelationship failed %@", error);
        return NO;
    }

    return [array containsObject:filepath.lastPathComponent];
128 129
}

130
#if TARGET_OS_IOS
131 132
- (NSObject<HTTPResponse> *)_httpGETDownloadForPath:(NSString *)path
{
133
    NSString *filePath = [[path stringByReplacingOccurrencesOfString:@"/download/" withString:@""] stringByRemovingPercentEncoding];
134 135 136 137
    if (![self fileIsInDocumentFolder:filePath]) {
       //return nil which gets handled as resource not found
        return nil;
    }
138 139 140 141
    HTTPFileResponse *fileResponse = [[HTTPFileResponse alloc] initWithFilePath:filePath forConnection:self];
    fileResponse.contentType = @"application/octet-stream";
    return fileResponse;
}
142

143 144
- (NSObject<HTTPResponse> *)_httpGETThumbnailForPath:(NSString *)path
{
145
    NSString *filePath = [[path stringByReplacingOccurrencesOfString:@"/Thumbnail/" withString:@""] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLFragmentAllowedCharacterSet];
146

147
    if ([filePath isEqualToString:@"/"]) return [[HTTPErrorResponse alloc] initWithErrorCode:404];
148

149 150 151 152 153 154 155 156 157 158
    UIImage *thumbnail = [UIImage imageWithContentsOfFile:filePath];
    if (!thumbnail) return [[HTTPErrorResponse alloc] initWithErrorCode:404];

    NSData *theData = UIImageJPEGRepresentation(thumbnail, .9);

    if (!theData) return [[HTTPErrorResponse alloc] initWithErrorCode:404];

    HTTPDataResponse *dataResponse = [[HTTPDataResponse alloc] initWithData:theData];
    dataResponse.contentType = @"image/jpg";
    return dataResponse;
159 160 161 162
}

- (NSObject<HTTPResponse> *)_httpGETLibraryForPath:(NSString *)path
{
163 164 165
    NSString *filePath = [self filePathForURI:path];
    NSString *documentRoot = [config documentRoot];
    NSString *relativePath = [filePath substringFromIndex:[documentRoot length]];
166
    BOOL shouldReturnLibVLCXML = [relativePath isEqualToString:@"/libMediaVLC.xml"];
167

168 169 170
    NSArray *allMedia = [self allMedia];
    return shouldReturnLibVLCXML ? [self generateXMLResponseFrom:allMedia path:path] : [self generateHttpResponseFrom:allMedia path:path];
}
171

172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
- (NSArray *)allMedia
{
    MediaLibraryService* medialibrary = [[VLCHTTPUploaderController sharedInstance] medialibrary];

    // Adding all Albums
    NSMutableArray *allMedia = [[medialibrary albumsWithSortingCriteria:VLCMLSortingCriteriaDefault desc:false] mutableCopy] ?: [NSMutableArray new];
    // Adding all Playlists
    [allMedia addObjectsFromArray:[medialibrary playlistsWithSortingCriteria:VLCMLSortingCriteriaDefault desc:false]];
    // Adding all Videos files
    [allMedia addObjectsFromArray:[medialibrary mediaOfType:VLCMLMediaTypeVideo sortingCriteria:VLCMLSortingCriteriaDefault desc:false]];

    //TODO: add all shows
    // Adding all audio files which are not in an Album
    NSArray* audioFiles = [medialibrary mediaOfType:VLCMLMediaTypeAudio sortingCriteria:VLCMLSortingCriteriaDefault desc:false];
    for (VLCMLMedia *track in audioFiles) {
187
        if (track.subtype != VLCMLMediaSubtypeAlbumTrack) {
188
            [allMedia addObject:track];
189
        }
190
    }
191
    return [allMedia copy];
192
}
193

194 195 196 197 198 199 200 201 202 203 204 205
- (NSString *)createHTMLMediaObjectFromMedia:(VLCMLMedia *)media
{
    return [NSString stringWithFormat:
            @"<div style=\"background-image:url('Thumbnail/%@')\"> \
            <a href=\"download/%@\" class=\"inner\"> \
            <div class=\"down icon\"></div> \
            <div class=\"infos\"> \
            <span class=\"first-line\">%@</span> \
            <span class=\"second-line\">%@ - %@</span> \
            </div> \
            </a> \
            </div>",
206
            media.thumbnail.path,
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
            [[media mainFile].mrl.path
             stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLFragmentAllowedCharacterSet],
            media.title,
            [media mediaDuration], [media formatSize]];
}

- (NSString *)createHTMLFolderObjectWithImagePath:(NSString *)imagePath
                                             name:(NSString *)name
                                            count:(NSUInteger)count
{
    return [NSString stringWithFormat:
            @"<div style=\"background-image:url('Thumbnail/%@')\"> \
            <a href=\"#\" class=\"inner folder\"> \
            <div class=\"open icon\"></div> \
            <div class=\"infos\"> \
            <span class=\"first-line\">%@</span> \
            <span class=\"second-line\">%lu items</span> \
            </div> \
            </a> \
            <div class=\"content\">",
            imagePath,
            name,
            count];
}

232 233 234
- (HTTPDynamicFileResponse *)generateHttpResponseFrom:(NSArray *)media path:(NSString *)path
{
    NSMutableArray *mediaInHtml = [[NSMutableArray alloc] initWithCapacity:media.count];
235 236
    for (NSObject <VLCMLObject> *mediaObject in media) {
        if ([mediaObject isKindOfClass:[VLCMLMedia class]]) {
237
            [mediaInHtml addObject:[self createHTMLMediaObjectFromMedia:(VLCMLMedia *)mediaObject]];
238 239
        } else if ([mediaObject isKindOfClass:[VLCMLPlaylist class]]) {
            VLCMLPlaylist *playlist = (VLCMLPlaylist *)mediaObject;
240
            NSArray *playlistItems = [playlist media];
241 242 243
            [mediaInHtml addObject: [self createHTMLFolderObjectWithImagePath:playlist.artworkMrl
                                                                name:playlist.name
                                                               count:playlistItems.count]];
244
            for (VLCMLMedia *media in playlistItems) {
245
                [mediaInHtml addObject:[self createHTMLMediaObjectFromMedia:media]];
246 247
            }
            [mediaInHtml addObject:@"</div></div>"];
248 249
        } else if ([mediaObject isKindOfClass:[VLCMLAlbum class]]) {
            VLCMLAlbum *album = (VLCMLAlbum *)mediaObject;
250
            NSArray *albumTracks = [album tracks];
251
            [mediaInHtml addObject:[self createHTMLFolderObjectWithImagePath:[album artworkMRL].path
252 253
                                                                        name:album.title
                                                                       count:albumTracks.count]];
254
            for (VLCMLMedia *track in albumTracks) {
255
                [mediaInHtml addObject:[self createHTMLMediaObjectFromMedia:track]];
256
            }
257
            [mediaInHtml addObject:@"</div></div>"];
258
        }
259 260 261 262 263 264 265 266 267 268 269 270 271 272
    } // end of forloop
    NSString *deviceModel = [[UIDevice currentDevice] model];

    NSDictionary *replacementDict = @{@"FILES" : [mediaInHtml componentsJoinedByString:@" "],
                        @"WEBINTF_TITLE" : NSLocalizedString(@"WEBINTF_TITLE", nil),
                        @"WEBINTF_DROPFILES" : NSLocalizedString(@"WEBINTF_DROPFILES", nil),
                        @"WEBINTF_DROPFILES_LONG" : [NSString stringWithFormat:NSLocalizedString(@"WEBINTF_DROPFILES_LONG", nil), deviceModel],
                        @"WEBINTF_DOWNLOADFILES" : NSLocalizedString(@"WEBINTF_DOWNLOADFILES", nil),
                        @"WEBINTF_DOWNLOADFILES_LONG" : [NSString stringWithFormat: NSLocalizedString(@"WEBINTF_DOWNLOADFILES_LONG", nil), deviceModel]};
    HTTPDynamicFileResponse *fileResponse = [[HTTPDynamicFileResponse alloc] initWithFilePath:[self filePathForURI:path]
                                                       forConnection:self
                                                           separator:@"%%"
                                               replacementDictionary:replacementDict];
    fileResponse.contentType = @"text/html";
273

274 275
    return fileResponse;
}
276

277 278 279 280
- (HTTPDynamicFileResponse *)generateXMLResponseFrom:(NSArray *)media path:(NSString *)path
{
    NSMutableArray *mediaInXml = [[NSMutableArray alloc] initWithCapacity:media.count];
    NSString *hostName = [NSString stringWithFormat:@"%@:%@", [[VLCHTTPUploaderController sharedInstance] hostname], [[VLCHTTPUploaderController sharedInstance] hostnamePort]];
281 282 283 284
    for (NSObject <VLCMLObject> *mediaObject in media) {
        if ([mediaObject isKindOfClass:[VLCMLMedia class]]) {
            VLCMLMedia *file = (VLCMLMedia *)mediaObject;
            NSString *pathSub = [self _checkIfSubtitleWasFound:[file mainFile].mrl.path];
285 286
            if (pathSub)
                pathSub = [NSString stringWithFormat:@"http://%@/download/%@", hostName, pathSub];
287 288 289
            [mediaInXml addObject:[NSString stringWithFormat:@"<Media title=\"%@\" thumb=\"http://%@/Thumbnail/%@\" duration=\"%@\" size=\"%@\" pathfile=\"http://%@/download/%@\" pathSubtitle=\"%@\"/>",
                                   file.title,
                                   hostName,
290
                                   file.thumbnail.path,
291 292 293 294 295 296 297 298
                                   [file mediaDuration], [file formatSize],
                                   hostName,
                                   [[file mainFile].mrl.path stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLFragmentAllowedCharacterSet], pathSub]];
        } else if ([mediaObject isKindOfClass:[VLCMLPlaylist class]]) {
            VLCMLPlaylist *playlist = (VLCMLPlaylist *)mediaObject;
            NSArray *playlistItems = [playlist media];
            for (VLCMLMedia *file in playlistItems) {
                NSString *pathSub = [self _checkIfSubtitleWasFound:[file mainFile].mrl.path];
299 300
                if (pathSub)
                    pathSub = [NSString stringWithFormat:@"http://%@/download/%@", hostName, pathSub];
301 302
                [mediaInXml addObject:[NSString stringWithFormat:@"<Media title=\"%@\" thumb=\"http://%@/Thumbnail/%@\" duration=\"%@\" size=\"%@\" pathfile=\"http://%@/download/%@\" pathSubtitle=\"%@\"/>", file.title,
                                       hostName,
303
                                       file.thumbnail.path,
304 305 306 307
                                       [file mediaDuration],
                                       [file formatSize],
                                       hostName,
                                       [[file mainFile].mrl.path stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLFragmentAllowedCharacterSet], pathSub]];
308
            }
309 310 311 312 313 314 315
        } else if ([mediaObject isKindOfClass:[VLCMLAlbum class]]) {
            VLCMLAlbum *album = (VLCMLAlbum *)mediaObject;
            NSArray *albumTracks = [album tracks];
            for (VLCMLMedia *track in albumTracks) {

                [mediaInXml addObject:[NSString stringWithFormat:@"<Media title=\"%@\" thumb=\"http://%@/Thumbnail/%@\" duration=\"%@\" size=\"%@\" pathfile=\"http://%@/download/%@\" pathSubtitle=\"\"/>", track.title,
                                       hostName,
316
                                       track.thumbnail.path,
317 318 319 320
                                       [track mediaDuration],
                                       [track formatSize],
                                       hostName,
                                       [[track mainFile].mrl.path stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLFragmentAllowedCharacterSet]]];
321 322 323 324 325 326 327
            }
        }
    } // end of forloop

    NSDictionary *replacementDict = @{@"FILES" : [mediaInXml componentsJoinedByString:@" "],
                        @"NB_FILE" : [NSString stringWithFormat:@"%li", (unsigned long)mediaInXml.count],
                        @"LIB_TITLE" : [[UIDevice currentDevice] name]};
328

329 330 331 332 333
    HTTPDynamicFileResponse *fileResponse = [[HTTPDynamicFileResponse alloc] initWithFilePath:[self filePathForURI:path]
                                                       forConnection:self
                                                           separator:@"%%"
                                               replacementDictionary:replacementDict];
    fileResponse.contentType = @"application/xml";
334 335
    return fileResponse;
}
336
#else
337 338 339 340 341 342 343 344 345
- (NSObject<HTTPResponse> *)_httpGETLibraryForPath:(NSString *)path
{
    UIDevice *currentDevice = [UIDevice currentDevice];
    NSString *deviceModel = [currentDevice model];
    NSString *filePath = [self filePathForURI:path];
    NSString *documentRoot = [config documentRoot];
    NSString *relativePath = [filePath substringFromIndex:[documentRoot length]];
    NSDictionary *replacementDict = @{@"WEBINTF_TITLE" : NSLocalizedString(@"WEBINTF_TITLE_ATV", nil),
                                      @"WEBINTF_DROPFILES" : NSLocalizedString(@"WEBINTF_DROPFILES", nil),
346 347
                                      @"WEBINTF_DROPFILES_LONG" : [NSString stringWithFormat:NSLocalizedString(@"WEBINTF_DROPFILES_LONG_ATV", nil), deviceModel],
                                      @"WEBINTF_OPEN_URL" : NSLocalizedString(@"ENTER_URL", nil)};
348 349 350 351 352 353 354 355 356 357 358 359

    HTTPDynamicFileResponse *fileResponse;
    if ([relativePath isEqualToString:@"/index.html"]) {
        fileResponse = [[HTTPDynamicFileResponse alloc] initWithFilePath:[self filePathForURI:path]
                                                           forConnection:self
                                                               separator:@"%%"
                                                   replacementDictionary:replacementDict];
        fileResponse.contentType = @"text/html";
    }

    return fileResponse;
}
360
#endif
361

362 363 364

- (NSObject<HTTPResponse> *)_httpGETCSSForPath:(NSString *)path
{
365
#if TARGET_OS_IOS
366
    NSDictionary *replacementDict = @{@"WEBINTF_TITLE" : NSLocalizedString(@"WEBINTF_TITLE", nil)};
367
#else
368
    NSDictionary *replacementDict = @{@"WEBINTF_TITLE" : NSLocalizedString(@"WEBINTF_TITLE_ATV", nil)};
369
#endif
370 371 372 373 374 375 376 377
    HTTPDynamicFileResponse *fileResponse = [[HTTPDynamicFileResponse alloc] initWithFilePath:[self filePathForURI:path]
                                                                                forConnection:self
                                                                                    separator:@"%%"
                                                                        replacementDictionary:replacementDict];
    fileResponse.contentType = @"text/css";
    return fileResponse;
}

378 379 380 381 382 383 384 385 386 387 388 389 390 391
#if TARGET_OS_TV
- (NSObject <HTTPResponse> *)_HTTPGETPlaying
{
    /* JSON response:
     {
        "currentTime": 42,
        "media": {
            "id": "some id",
            "title": "some title",
            "duration": 120000
        }
     }
     */

392
    VLCPlaybackService *vpc = [VLCPlaybackService sharedInstance];
393
    if (!vpc.isPlaying) {
394 395
        return [[HTTPErrorResponse alloc] initWithErrorCode:404];
    }
396
    VLCMedia *media = [vpc currentlyPlayingMedia];
397 398 399 400
    if (!media) {
        return [[HTTPErrorResponse alloc] initWithErrorCode:404];
    }

401
    NSString *mediaTitle = vpc.metadata.title;
402 403 404 405
    if (!mediaTitle)
        mediaTitle = @"";
    NSDictionary *mediaDict = @{ @"id" : media.url.absoluteString,
                                 @"title" : mediaTitle,
406 407
                                 @"duration" : @([vpc mediaDuration])};
    NSDictionary *returnDict = @{ @"currentTime" : @([vpc playedTime].intValue),
408 409 410 411 412 413 414 415 416 417 418 419
                                  @"media" : mediaDict };

    NSError *error;
    NSData *returnData = [NSJSONSerialization dataWithJSONObject:returnDict options:0 error:&error];
    if (error != nil) {
        APLog(@"JSON serialization failed %@", error);
        return [[HTTPErrorResponse alloc] initWithErrorCode:500];
    }

    return [[HTTPDataResponse alloc] initWithData:returnData];
}

420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
- (NSObject <HTTPResponse> *)_HTTPGETwebResources
{
    /* JS response
     {
        "WEBINTF_URL_SENT" : "URL sent successfully.",
        "WEBINTF_URL_EMPTY" :"'URL cannot be empty.",
        "WEBINTF_URL_INVALID" : "Not a valid URL."
     }
     */

    NSString *returnString = [NSString stringWithFormat:
                              @"var LOCALES = {\n" \
                                         "PLAYER_CONTROL: {\n" \
                                         "URL: {\n" \
                                         "EMPTY: \"%@\",\n" \
                                         "NOT_VALID: \"%@\",\n" \
                                         "SENT_SUCCESSFULLY: \"%@\"\n" \
                                         "}\n" \
                                         "}\n" \
                              "}",
                              NSLocalizedString(@"WEBINTF_URL_EMPTY", nil),
                              NSLocalizedString(@"WEBINTF_URL_INVALID", nil),
                              NSLocalizedString(@"WEBINTF_URL_SENT", nil)];

    NSData *returnData = [returnString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
    return [[HTTPDataResponse alloc] initWithData:returnData];
}

448 449 450 451 452 453 454 455 456 457 458 459 460 461
- (NSObject <HTTPResponse> *)_HTTPGETPlaylist
{
    /* JSON response:
     [
        {
            "media": {
                "id": "some id 1",
                "title": "some title 1",
                "duration": 120000
            }
        },
     ...]
     */

462
    VLCPlaybackService *vpc = [VLCPlaybackService sharedInstance];
463
    if (!vpc.isPlaying || !vpc.mediaList) {
464 465 466 467 468 469 470 471 472 473
        return [[HTTPErrorResponse alloc] initWithErrorCode:404];
    }

    VLCMediaList *mediaList = vpc.mediaList;
    [mediaList lock];
    NSUInteger mediaCount = mediaList.count;
    NSMutableArray *retArray = [NSMutableArray array];
    for (NSUInteger x = 0; x < mediaCount; x++) {
        VLCMedia *media = [mediaList mediaAtIndex:x];
        NSString *mediaTitle;
474
        if (media.parsedStatus == VLCMediaParsedStatusDone) {
475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
            mediaTitle = [media metadataForKey:VLCMetaInformationTitle];
        } else {
            mediaTitle = media.url.lastPathComponent;
        }

        NSDictionary *mediaDict = @{ @"id" : media.url.absoluteString,
                                     @"title" : mediaTitle,
                                     @"duration" : @(media.length.intValue) };
        [retArray addObject:@{ @"media" : mediaDict }];
    }
    [mediaList unlock];

    NSError *error;
    NSData *returnData = [NSJSONSerialization dataWithJSONObject:retArray options:0 error:&error];
    if (error != nil) {
        APLog(@"JSON serialization failed %@", error);
        return [[HTTPErrorResponse alloc] initWithErrorCode:500];
    }

    return [[HTTPDataResponse alloc] initWithData:returnData];
}
#endif
497 498 499 500 501 502 503 504 505 506

- (NSObject<HTTPResponse> *)httpResponseForMethod:(NSString *)method URI:(NSString *)path
{
    if ([method isEqualToString:@"POST"] && [path isEqualToString:@"/upload.json"])
        return [self _httpPOSTresponseUploadJSON];

#if TARGET_OS_IOS
    if ([path hasPrefix:@"/download/"]) {
        return [self _httpGETDownloadForPath:path];
    }
507
    if ([path hasPrefix:@"/Thumbnail/"]) {
508 509
        return [self _httpGETThumbnailForPath:path];
    }
510 511 512 513 514 515 516
#else
    if ([path hasPrefix:@"/playing"]) {
        return [self _HTTPGETPlaying];
    }
    if ([path hasPrefix:@"/playlist"]) {
        return [self _HTTPGETPlaylist];
    }
517 518 519
    if ([path hasPrefix:@"/web_resources.js"]) {
        return [self _HTTPGETwebResources];
    }
520 521 522 523 524 525 526 527 528 529
#endif

    NSString *filePath = [self filePathForURI:path];
    NSString *documentRoot = [config documentRoot];
    NSString *relativePath = [filePath substringFromIndex:[documentRoot length]];

    if ([relativePath isEqualToString:@"/index.html"] || [relativePath isEqualToString:@"/libMediaVLC.xml"]) {
        return [self _httpGETLibraryForPath:path];
    } else if ([relativePath isEqualToString:@"/style.css"]) {
        return [self _httpGETCSSForPath:path];
530
    }
531

532 533 534
    return [super httpResponseForMethod:method URI:path];
}

535 536 537 538 539 540 541
#if TARGET_OS_TV
- (WebSocket *)webSocketForURI:(NSString *)path
{
    return [[VLCPlayerControlWebSocket alloc] initWithRequest:request socket:asyncSocket];
}
#endif

542 543 544 545 546 547 548
- (void)prepareForBodyWithSize:(UInt64)contentLength
{
    // set up mime parser
    NSString* boundary = [request headerField:@"boundary"];
    _parser = [[MultipartFormDataParser alloc] initWithBoundary:boundary formEncoding:NSUTF8StringEncoding];
    _parser.delegate = self;

549 550
    APLog(@"expecting file of size %lli kB", contentLength / 1024);
    _contentLength = contentLength;
551 552 553 554 555 556 557
}

- (void)processBodyData:(NSData *)postDataChunk
{
    /* append data to the parser. It will invoke callbacks to let us handle
     * parsed data. */
    [_parser appendData:postDataChunk];
558

559
    _receivedContent += postDataChunk.length;
560

561 562 563 564
    long long percentage = ((_receivedContent * 100) / _contentLength);
    APLog(@"received %lli kB (%lli %%)", _receivedContent / 1024, percentage);
#if TARGET_OS_TV
        if (percentage >= 10) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
565
            [self performSelectorOnMainThread:@selector(startPlaybackOfPath:) withObject:_filepath waitUntilDone:NO];
566 567
        }
#endif
568 569
}

Tobias's avatar
Tobias committed
570
#if TARGET_OS_TV
Felix Paul Kühne's avatar
Felix Paul Kühne committed
571 572 573
- (void)startPlaybackOfPath:(NSString *)path
{
    APLog(@"Starting playback of %@", path);
574 575 576 577 578 579 580 581
    if (_receivedFiles == nil)
        _receivedFiles = [[NSMutableArray alloc] init];

    if ([_receivedFiles containsObject:path])
        return;

    [_receivedFiles addObject:path];

582
    VLCPlaybackService *vpc = [VLCPlaybackService sharedInstance];
583 584 585 586 587 588 589
    VLCMediaList *mediaList = vpc.mediaList;

    if (!mediaList) {
        mediaList = [[VLCMediaList alloc] init];
    }

    [mediaList addMedia:[VLCMedia mediaWithURL:[NSURL fileURLWithPath:path]]];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
590

591 592
    if (!vpc.mediaList) {
        [vpc playMediaList:mediaList firstIndex:0 subtitlesFilePath:nil];
593 594 595 596 597 598 599 600 601
    }

    VLCFullscreenMovieTVViewController *movieVC = [VLCFullscreenMovieTVViewController fullscreenMovieTVViewController];

    if (![movieVC isBeingPresented]) {
        [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:movieVC
                                                                                     animated:YES
                                                                                   completion:nil];
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
602
}
Tobias's avatar
Tobias committed
603
#endif
Felix Paul Kühne's avatar
Felix Paul Kühne committed
604

605 606 607 608 609 610 611 612 613 614
//-----------------------------------------------------------------
#pragma mark multipart form data parser delegate


- (void)processStartOfPartWithHeader:(MultipartMessageHeader*) header
{
    /* in this sample, we are not interested in parts, other then file parts.
     * check content disposition to find out filename */

    MultipartMessageHeaderField* disposition = (header.fields)[@"Content-Disposition"];
615
    NSString* filename = (disposition.params)[@"filename"];
616 617 618 619 620 621 622

    if ((nil == filename) || [filename isEqualToString: @""]) {
        // it's either not a file part, or
        // an empty form sent. we won't handle it.
        return;
    }

623
    // create the path where to store the media temporarily
624
    NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
625 626
    NSString *uploadDirPath = [searchPaths.firstObject
                               stringByAppendingPathComponent:kVLCHTTPUploadDirectory];
627
    NSFileManager *fileManager = [NSFileManager defaultManager];
628 629

    BOOL isDir = YES;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
630
    if (![fileManager fileExistsAtPath:uploadDirPath isDirectory:&isDir])
631
        [fileManager createDirectoryAtPath:uploadDirPath withIntermediateDirectories:YES attributes:nil error:nil];
632

633
    _filepath = [uploadDirPath stringByAppendingPathComponent: filename];
634

635
    NSNumber *freeSpace = [[UIDevice currentDevice] VLCFreeDiskSpace];
636 637 638 639 640 641 642 643
    if (_contentLength >= freeSpace.longLongValue) {
        /* avoid deadlock since we are on a background thread */
        [self performSelectorOnMainThread:@selector(notifyUserAboutEndOfFreeStorage:) withObject:filename waitUntilDone:NO];
        [self handleResourceNotFound];
        [self stop];
        return;
    }

644
    APLog(@"Saving file to %@", _filepath);
645 646
    if (![fileManager createDirectoryAtPath:[_filepath stringByDeletingLastPathComponent]
                withIntermediateDirectories:true attributes:nil error:nil])
647
        APLog(@"Could not create directory at path: %@", _filepath);
648

649
    if (![fileManager createFileAtPath:_filepath contents:nil attributes:nil])
650
        APLog(@"Could not create file at path: %@", _filepath);
651

652
    _storeFile = [NSFileHandle fileHandleForWritingAtPath:_filepath];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
653 654 655 656

    VLCActivityManager *activityManager = [VLCActivityManager defaultManager];
    [activityManager networkActivityStarted];
    [activityManager disableIdleTimer];
657 658
}

659 660
- (void)notifyUserAboutEndOfFreeStorage:(NSString *)filename
{
661
#if TARGET_OS_IOS
662 663 664 665 666
    [VLCAlertViewController alertViewManagerWithTitle:NSLocalizedString(@"DISK_FULL", nil)
                                         errorMessage:[NSString stringWithFormat:
                                                       NSLocalizedString(@"DISK_FULL_FORMAT", nil),
                                                       filename,
                                                       [[UIDevice currentDevice] model]]
667
                                       viewController:[UIApplication sharedApplication].keyWindow.rootViewController];
668 669 670 671 672 673 674 675 676 677 678 679
#else
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"DISK_FULL", nil)
                                                                             message:[NSString stringWithFormat:
                                                                                      NSLocalizedString(@"DISK_FULL_FORMAT", nil),
                                                                                      filename,
                                                                                      [[UIDevice currentDevice] model]]
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_CANCEL", nil)
                                                        style:UIAlertActionStyleCancel
                                                      handler:nil]];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
#endif
680 681
}

682 683 684
- (void)processContent:(NSData*)data WithHeader:(MultipartMessageHeader*) header
{
    // here we just write the output from parser to the file.
685 686 687 688 689 690 691 692 693 694 695 696 697
    if (_storeFile) {
        @try {
            [_storeFile writeData:data];
        }
        @catch (NSException *exception) {
            APLog(@"File to write further data because storage is full.");
            [_storeFile closeFile];
            _storeFile = nil;
            /* don't block */
            [self performSelector:@selector(stop) withObject:nil afterDelay:0.1];
        }
    }

698 699 700 701 702
}

- (void)processEndOfPartWithHeader:(MultipartMessageHeader*)header
{
    // as the file part is over, we close the file.
703
    APLog(@"closing file");
704 705 706 707
    [_storeFile closeFile];
    _storeFile = nil;
}

708
- (BOOL)shouldDie
709
{
710
    if (_filepath) {
711
        if (_filepath.length > 0) {
Felix Paul Kühne's avatar
Felix Paul Kühne committed
712
            [[VLCHTTPUploaderController sharedInstance] moveFileFrom:_filepath];
713 714 715 716 717

#if TARGET_OS_TV
            [_receivedFiles removeObject:_filepath];
#endif
        }
718
    }
719
    return [super shouldDie];
720 721
}

722 723
#pragma mark subtitle

Felix Paul Kühne's avatar
Felix Paul Kühne committed
724
- (NSMutableArray *)_listOfSubtitles
725
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
726
    NSMutableArray *listOfSubtitles = [[NSMutableArray alloc] init];
727
    NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
728
    NSArray *allFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectory error:nil];
729
    NSString *filePath;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
730 731 732
    NSUInteger count = allFiles.count;
    for (NSUInteger i = 0; i < count; i++) {
        filePath = [[NSString stringWithFormat:@"%@/%@", documentsDirectory, allFiles[i]] stringByReplacingOccurrencesOfString:@"file://"withString:@""];
733
        if ([filePath isSupportedSubtitleFormat])
Felix Paul Kühne's avatar
Felix Paul Kühne committed
734
            [listOfSubtitles addObject:filePath];
735
    }
Felix Paul Kühne's avatar
Felix Paul Kühne committed
736
    return listOfSubtitles;
737 738
}

739
- (NSString *)_checkIfSubtitleWasFound:(NSString *)filePath
740
{
Felix Paul Kühne's avatar
Felix Paul Kühne committed
741
    NSString *subtitlePath;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
742 743 744
    NSString *fileSub;
    NSString *currentPath;

745
    NSString *fileName = [[filePath lastPathComponent] stringByDeletingPathExtension];
Felix Paul Kühne's avatar
Felix Paul Kühne committed
746 747 748
    if (fileName == nil)
        return nil;

Felix Paul Kühne's avatar
Felix Paul Kühne committed
749 750
    NSMutableArray *listOfSubtitles = [self _listOfSubtitles];
    NSUInteger count = listOfSubtitles.count;
Felix Paul Kühne's avatar
Felix Paul Kühne committed
751

Felix Paul Kühne's avatar
Felix Paul Kühne committed
752 753 754
    for (NSUInteger i = 0; i < count; i++) {
        currentPath = listOfSubtitles[i];
        fileSub = [NSString stringWithFormat:@"%@", currentPath];
755
        if ([fileSub rangeOfString:fileName].location != NSNotFound)
Felix Paul Kühne's avatar
Felix Paul Kühne committed
756
            subtitlePath = currentPath;
757 758 759 760
    }
    return subtitlePath;
}

761
@end