Plutovman
Plutovman

Reputation: 697

Upload video (and properties) to youtube with swift and alamofire

I'm interested in uploading a video to youtube along with title, description, and keywords. The code below uploads a video to youtube without any properties:

func postVideoToYouTube(token: String, callback: Bool -> Void){

let headers = ["Authorization": "Bearer \(token)"]
let urlYoutube = "https://www.googleapis.com/upload/youtube/v3/videos?part=id"

let path = NSBundle.mainBundle().pathForResource("video", ofType: "mp4")
let videodata: NSData = NSData.dataWithContentsOfMappedFile(path!)! as! NSData
upload(
    .POST,
    urlYoutube,
    headers: headers,
    multipartFormData: { multipartFormData in
        multipartFormData.appendBodyPart(data: videodata, name: "video", fileName: "video.mp4", mimeType: "application/octet-stream")
    },
    encodingCompletion: { encodingResult in
        switch encodingResult {
        case .Success(let upload, _, _):
            upload.responseJSON { request, response, error in
                print(response)
                callback(true)
            }
        case .Failure(_):
            callback(false)
        }
    })

}

I've been trying to modify urlYoutube to include the necessary snippet information to no avail:

let snippetTitle = "The Best Video Ever"
let snippetDesc = "first video upload with title"
let snippetTags = "best,video,ever,snoopy,monkey,charlie"
let urlYoutube = "https://www.googleapis.com/upload/youtube/v3/videos?part=id&snippet.title=%@&snippet.description=%@&snippet.keywords=%@", snippetTitle, snippetDesc, snippetTags)"

The other approach I tried (thanks to @adjuremods suggestion) was to use Request Body based on the Youtube-API to EDIT a previously uploaded video. So, first, I defined a video resource:

let parms = [
  "kind": "youtube#video",
  "id" : returnedId,
  "snippet.title" : "summer vacation cali",
  "snippet.description" : "had fun in the sun",
  "snippet.tags" : ["surf","fun","sun"],
  "snippet.categoryId" : "1"
]

and sent it as a PUT request like so:

do {
  let parmsJson = try NSJSONSerialization.dataWithJSONObject(parameters, options: .PrettyPrinted)

  let putURL = NSURL(string: "https://www.googleapis.com/upload/youtube/v3/videos")!

  let request = NSMutableURLRequest(URL: putURL)
  request.HTTPMethod = "PUT"
  request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
  request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
  request.HTTPBody = parmsJson

  let task = NSURLSession.sharedSession().dataTaskWithRequest(request){ data, response, error in
    if error != nil{
      print("Error -> \(error)")
      return
    }

    do {
      let result = try NSJSONSerialization.JSONObjectWithData(data!, options: []) as? [String:AnyObject]

      print("Result -> \(result)")

    } catch {
      print("Error -> \(error)")
    }
  }

  task.resume()
  //return task


} catch {
  print ("...could not accomplish put request")
}   

Unfortunately, the result I get is always the same, regardless of how I modify the video resource:

Result -> Optional(["error": {
code = 400;
errors =     (
            {
        domain = global;
        message = "Unsupported content with type: application/json; charset=UTF-8";
        reason = badContent;
    }
);
message = "Unsupported content with type: application/json; charset=UTF-8";

Could someone advice where I might be going wrong? I don't have a very clear understanding of how to set these parameters defined by the API:

https://developers.google.com/youtube/v3/docs/videos/insert#parameters

Upvotes: 2

Views: 3354

Answers (3)

mrkbxt
mrkbxt

Reputation: 491

Add a line to the multipartFormData block for the parameter values as follows (place the code before the video item -and- add any additional snippet property values per the implied structure):

multipartFormData.appendBodyPart(data:"{'snippet':{'title' : 'TITLE_TEXT', 'description': 'DESCRIPTION_TEXT'}}".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!, name :"snippet", mimeType: "application/json")

the post url should also be changed to part=snippet

https://www.googleapis.com/upload/youtube/v3/videos?part=snippet

i.e.

 .POST,
    "https://www.googleapis.com/upload/youtube/v3/videos?part=snippet",
    headers: headers,
    multipartFormData: { multipartFormData in
        multipartFormData.appendBodyPart(data:"{'snippet':{'title' : 'TITLE_TEXT', 'description': 'DESCRIPTION_TEXT'}}".dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!, name :"snippet", mimeType: "application/json")
        multipartFormData.appendBodyPart(data: videodata, name: "video", fileName: "video.mp4", mimeType: "application/octet-stream")
},

Upvotes: 5

Plutovman
Plutovman

Reputation: 697

Oh boy....This turned out to be quite a puzzle. From previous youtube answers, it had been hinted that the only way to upload a video with snippet metadata was through a combination of POST and PUT requests...However, getting those to work was quite the challenge. One very useful thing I learned along the way was that Alamofire returns an object when making a request that can be used to troubleshoot a cURL session:

let putRequest = request(.PUT, "https://www.googleapis.com/youtube/v3/videos?part=snippet&key=\(ios_key)", parameters: dictionaryParameters, encoding: .JSON , headers: headers)
          debugPrint(putRequest)

This returns output like this:

$ curl -i \
-X PUT \
-H "Authorization: Bearer ##########################" \
-H "Content-Type: application/json" \
-H "Accept-Language: en-US;q=1.0" \
-H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \
-H "User-Agent: testVideoApp/com.thinkforward.testVideoApp (1; OS Version 9.2 (Build 13C75))" \
-d "{\"id\":\"###########\",\"snippet\":{\"title\":\"something in the way\",\"tags\":[\"whisky\",\"tango\",\"fox\"],\"description\":\"is this finally gonna work?\"}}" \

Aha! Now we have a cURL command we can test with independent of Alamofire. Once I got this to work, the rest was a matter of reverse-engineering the command to ensure that I was passing the correct parameters to Alamofire...This is how I found out that Alamofire wants parameters to be passed in a very specific way:

 parameters: <[String : AnyObject]?>

With this intel, I rebuilt my dictionaries as follows:

let dictionarySnippet :Dictionary<String, AnyObject>  = [
  "title" : "something in the way",
  "description" : "is this finally gonna work?",
  "tags" : ["whisky","tango","fox"],
  "categoryId" : "1"
]
let dictionaryParameters :Dictionary<String, AnyObject> = [
            "id" : "\(returnedId)",
            "snippet" : dictionarySnippet,
          ]

Next, I found out that making a PUT request, actually requires a different scope from the one I was using for POST. Since I needed to make both types of requests, I had to update my scope variable as follows:

let scope = "https://www.googleapis.com/auth/youtube+https://www.googleapis.com/auth/youtube.upload"

Similarly, POST and PUT requests need separate urls:

https://www.googleapis.com/upload/youtube/v3/videos?part=snippet for POST
https://www.googleapis.com/youtube/v3/videos?part=snippet&key=\(ios_key) for PUT

With these changes in place, the code posted in my question works like a charm. It's not very elegant, but it gets the job done.

Upvotes: 0

adjuremods
adjuremods

Reputation: 2998

Setting up Titles, Descriptions, Tags, etc. will require you to use Request Body based on the API. Check out this issue raised here on how to set up a request body in Swift.

let json = [ "title":"ABC" , "dict": mapDict ]
let jsonData = NSJSONSerialization.dataWithJSONObject(json, options: .PrettyPrinted, error: nil)
// insert json data to the request
request.HTTPBody = jsonData

Remember that the Request Body should be a resource representation of Video

Upvotes: 0

Related Questions