0

I would like to write a javascript function that returns informations from youtube videos; to be more specific I would like to get the ID and the length of videos got by a search, in a json object. So I took a look at the youtube API and I came out with this solution:

   function getYoutubeDurationMap( query ){
        var youtubeSearchReq = "https://gdata.youtube.com/feeds/api/videos?q="+ query +
                "&max-results=20&duration=long&category=film&alt=json&v=2";
        var youtubeMap = [];
        $.getJSON(youtubeSearchReq, function(youtubeResult){
            var youtubeVideoDetailReq = "https://gdata.youtube.com/feeds/api/videos/";
            for(var i =0;i<youtubeResult.feed.entry.length;i++){
                var youtubeVideoId = youtubeResult.feed.entry[i].id.$t.substring(27);
                $.getJSON(youtubeVideoDetailReq + youtubeVideoId + "?alt=json&v=2",function(videoDetails){
                    youtubeMap.push({id: videoDetails.entry.id.$t.substring(27),runtime: videoDetails.entry.media$group.media$content[0].duration});

                });
            }
        });
        return youtubeMap;    

    }

The logic is ok, but as many of you have already understood because of ajax when I call this function I get an empty array. Is there anyway to get the complete object? Should I use a Deferred object? Thanks for your answers.

user1012480
  • 752
  • 2
  • 11
  • 24

2 Answers2

4

Yes, you should use deferred objects.

The simplest approach here is to create an array into which you can store the jqXHR result of your inner $.getJSON() calls.

var def = [];
for (var i = 0; ...) {
    def[i] = $.getJSON(...).done(function(videoDetails) {
        ... // extract and store in youtubeMap
    });
}

and then at the end of the whole function, use $.when to create a new promise that will be resolved only when all of the inner calls have finished:

return $.when.apply($, def).then(function() {
    return youtubeMap;
});

and then use .done to handle the result from your function:

getYoutubeDurationMap(query).done(function(map) {
    // map contains your results
});

See http://jsfiddle.net/alnitak/8XQ4H/ for a demonstration using this YouTube API of how deferred objects allow you to completely separate the AJAX calls from the subsequent data processing for your "duration search".

The code is a little long, but reproduced here too. However whilst the code is longer than you might expect note that the generic functions herein are now reusable for any calls you might want to make to the YouTube API.

// generic search - some of the fields could be parameterised
function youtubeSearch(query) {
    var url = 'https://gdata.youtube.com/feeds/api/videos';
    return $.getJSON(url, {
        q: query,
        'max-results': 20,
        duration: 'long', category: 'film',  // parameters?
        alt: 'json', v: 2
    });
}

// get details for one YouTube vid
function youtubeDetails(id) {
    var url = 'https://gdata.youtube.com/feeds/api/videos/' + id;
    return $.getJSON(url, {
        alt: 'json', v: 2
    });
}

// get the details for *all* the vids returned by a search
function youtubeResultDetails(result) {
    var details = [];

    var def = result.feed.entry.map(function(entry, i) {
        var id = entry.id.$t.substring(27);
        return youtubeDetails(id).done(function(data) {
            details[i] = data;
        });
    });

    return $.when.apply($, def).then(function() {
        return details;
    });
}

// use deferred composition to do a search and then get all details
function youtubeSearchDetails(query) {
   return youtubeSearch(query).then(youtubeResultDetails);
}

// this code (and _only_ this code) specific to your requirement to
// return an array of {id, duration}
function youtubeDetailsToDurationMap(details) {
    return details.map(function(detail) {
        return {
            id: detail.entry.id.$t.substring(27),
            duration: detail.entry.media$group.media$content[0].duration
        }
    });
}

// and calling it all together
youtubeSearchDetails("after earth").then(youtubeDetailsToDurationMap).done(function(map) {
    // use map[i].id and .duration
});
Alnitak
  • 334,560
  • 70
  • 407
  • 495
0

As you have discovered, you can't return youtubeMap directly as it's not yet populated at the point of return. But you can return a Promise of a fully populated youtubeMap, which can be acted on with eg .done(), .fail() or .then().

function getYoutubeDurationMap(query) {
    var youtubeSearchReq = "https://gdata.youtube.com/feeds/api/videos?q=" + query + "&max-results=20&duration=long&category=film&alt=json&v=2";
    var youtubeVideoDetailReq = "https://gdata.youtube.com/feeds/api/videos/";
    var youtubeMap = [];
    var dfrd = $.Deferred();
    var p = $.getJSON(youtubeSearchReq).done(function(youtubeResult) {
        $.each(youtubeResult.feed.entry, function(i, entry) {
            var youtubeVideoId = entry.id.$t.substring(27);
            //Build a .then() chain to perform sequential queries
            p = p.then(function() {
                return $.getJSON(youtubeVideoDetailReq + youtubeVideoId + "?alt=json&v=2").done(function(videoDetails) {
                    youtubeMap.push({
                        id: videoDetails.entry.id.$t.substring(27),
                        runtime: videoDetails.entry.media$group.media$content[0].duration
                    });
                });
            });
        });
        //Add a terminal .then() to resolve dfrd when all video queries are complete.
        p.then(function() {
            dfrd.resolve(query, youtubeMap);
        });
    });
    return dfrd.promise();
}

And the call to getYoutubeDurationMap() would be of the following form :

getYoutubeDurationMap("....").done(function(query, map) {
    alert("Query: " + query + "\nYouTube videos found: " + map.length);
});

Notes:

  • In practice, you would probably loop through map and display the .id and .runtime data.
  • Sequential queries is preferable to parallel queries as sequential is kinder to both client and server, and more likely to succeed.
  • Another valid approach would be to return an array of separate promises (one per video) and to respond to completion with $.when.apply(..), however the required data would be more awkward to extract.
Beetroot-Beetroot
  • 18,022
  • 3
  • 37
  • 44
  • nice, but in my experience running the queries in series will make the batch slower than it could be, and the browser _should_ limit the number of concurrent AJAX queries to 4 or so if they're run in parallel. – Alnitak May 31 '13 at 21:36
  • Yes, it will run slower but is more guaranteed to complete. I've not done a comprehensive review of browser behaviour in this regard but wouldn't steak my life on all of them imposing a safeguard. Remember there's a bunch of popular "also rans" out there these days especially in Linux-land (incl Raspberry PI) - Midori, Dillo, Chromium, NetSurf, IceWeasel ... . How do they behave? I haven't got a clue. – Beetroot-Beetroot May 31 '13 at 22:08