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.

4
  • 1
    No, you should make a synchronous request. But just don't do that: learn to use the asynchronous logic of Javascript, and you'll be fine with a lot of other things too. Commented May 31, 2013 at 12:52
  • Execute things on callback... Commented May 31, 2013 at 12:53
  • yes, you should use deferred objects. Commented May 31, 2013 at 13:00
  • For more background: gist.github.com/domenic/3889970 Commented May 31, 2013 at 13:01

2 Answers 2

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
});
Sign up to request clarification or add additional context in comments.

Comments

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.

2 Comments

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.
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.