6

I'm having trouble with some dumb architecture because I'm dumb. I'm trying to loop over YouTube videos posted to Reddit, extract URLs and process them into an .m3u playlist.

Full code at Subreddit to YouTube Source Bookmarklet - Play YouTube music from subreddits in Foobar with foo_youtube

At some point I get the idea that I could check each URL to see if the video is dead or not, and offer an alternative if they're removed.

So I do AJAX requests to YouTube API and if there's an error I'm supposed to react to it and change that item's URL.

But the problem is it only works if the AJAX is NOT async - this takes many seconds, during which the page is jammed.

I'd like to let the AJAX be async but I don't know how I should structure my code.

Here is PSEUDOCODE of how it is now:

var listing = // data from reddit API
$.each(listing, function(key, value) {
    var url = // post URL from reddit posts listing 
    // ( "http://youtu.be/XmQR4_hDtpY&hd=1" )
    var aRegex = // to parse YouTube URLs 
    // ( (?:youtube(?:-nocookie)?.com/....bla...bla )
    var videoID = // YouTube video ID, extracted with regex 
    // ( "XmQR4_hDtpY" )
    var itemArtist = // parsed from reddit posts listing 
    // ( "Awesome Artist" )
    var itemTitle = // parsed from reddit posts listing 
    // ( "Cool Song (Original Mix)" )
    var itemURL = // url, further processed 
    // ( "3dydfy://www.youtube.com/watch?v=XmQR4_hDtpY&hd=1" )

    $.ajax({
            type: "HEAD",
            url: "https://gdata.youtube.com/feeds/api/videos/" + videoID,
            error: function() { 
                // If it's no longer available 
                // (removed, deleted account, made private)
                deadVid++; // chalk up another dead one, for showing progress
                itemURL = // dead videos should get a different URL 
           // ( "3dydfy-search://q=Awesome%20Artist%20Cool%20Song....." )
            }
        });

    // further process itemURL!
    // at this point I want itemURL changed by the .ajax()'s error callback
    // but I'm trying to keep the requests async 
    // to not jam the page while a hundred HTTP requests happen!
    if (condition){
        itemURL += // append various strings
    }
    // Write the item to the .m3u8 playlist
    playlist += itemURL + '\n';
}// end .each()
SouPress
  • 295
  • 1
  • 2
  • 12
  • Try using `$.ajax({...}).done(function(){ // all the code I need to add to my beautiful list goes here });` The problem is coming from the fact you aren't actually handling the success result of the AJAX call, you're just going on with your loop. Therefore, it just doesn't work like you want it to. For further reading, check out http://api.jquery.com/jquery.ajax/ – BobbyDazzler Oct 31 '14 at 16:02
  • A quick fix would be to make it SJAX, meaning set async: false in your AJAX call. So that everything is synchronous and you should be okay. Ref: http://stackoverflow.com/questions/133310/how-can-i-get-jquery-to-perform-a-synchronous-rather-than-asynchronous-ajax-re – lshettyl Oct 31 '14 at 16:10
  • @LShetty: They already mentioned the horrors of `async: false`. Do not recommend that as a fix :) – iCollect.it Ltd Oct 31 '14 at 16:11
  • @TrueBlueAussie, that's why it's a "quick" fix :) – lshettyl Oct 31 '14 at 16:13
  • @LShetty: Please re-read the question. They *already said* `async: false` was unacceptable. – iCollect.it Ltd Oct 31 '14 at 16:14

1 Answers1

5

Basically you want to know

  • What the errors were and
  • When every ajax request is finished

If you push the errors into a list, the results will be ready at the end (order not guaranteed of course).

For the second part, if you keep an array of the ajax promises returned from each $.ajax you can use $.when and wait for them all to complete using always().

As a basic example (other details removed):

var listing = {}; // data from reddit API
var promises = [];
var results = [];
$.each(listing, function(key, value) {
    // [snip]
    promises.push($.ajax({
            type: "HEAD",
            url: "https://gdata.youtube.com/feeds/api/videos/" + videoID,
            error: function() { 
                //[snip]
                results.push({
                   itemArtist: itemArtist,
                   videoID: videoID,
                   url: itemURL});
            }
        });
    );
}
// Wait for all promises to complete (pass or fail) 
$.when.apply($, promises).always(function(){
    // process list of failed URLs
});

Apologies for any typos. This was coded straight into the answer, but you get the idea.

I note you mention 100s of requests, but the browser will only allow a handful through at a time, so no need for additional processing.

If always is not working you can add your own deferred objects that resolve on success or fail:

var listing = {}; // data from reddit API
var promises = [];
var results = [];
$.each(listing, function(key, value) {
    var deferred = $.Deferred();
    promises.push(deferred.promise());
    // [snip]
    $.ajax({
            type: "HEAD",
            url: "https://gdata.youtube.com/feeds/api/videos/" + videoID,
            complete: function(){
                // resolve on success or fail
                deferred.resolve();
            },
            error: function() { 
                //[snip]
                results.push({
                   itemArtist: itemArtist,
                   videoID: videoID,
                   url: itemURL});
            }
        });
    );
}
// Wait for all promises to complete (pass or fail) 
$.when.apply($, promises).always(function(){
    // process list of failed URLs
});

Update Dec 2015

Here is another cool way to chain parallel promises together, without using an array (so long as you do not need the data values passed through to the callbacks):

Simplified code:

   var promise;   // Undefined is also a resolved promise when passed to $.when()
   $.each(listing, function(key, value) {
       // Each new Ajax promise is chained, in parallel, with the previous promise
       promise = $.when(promise, $.ajax({...}));
   });
   // When they are all finished, fire a final callback
   $.when(promise).always(function(){
       // All done!
   });

This has had some criticism, mainly from people that feel it is "unclean", but for the simplifying of parallel promise code the trade-off is minimal.

I figured out this was possible when I saw someone use promise = promise.then(newpromise) to chain events in sequence. After some experimenting I found I could do the same in parallel using promise = $.when(promise, newpromise)

iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • To clarify, I don't care if it's error 403 or 404 or whatever, I just want to hang onto the dead item (I need its videoID, itemArtist and itemTitle). And as for the array of deferreds, can't quite do that because the errors are a problem, [see my question here](/questions/26671328/) – SouPress Oct 31 '14 at 16:26
  • 1
    Push the details you want to retain into the results array as properties of an object. I see you have already attempted some of this in the other question, but `always` should fire at the end of all processing. If not, the solution is to add your own deferred.promises to the array that are resolved on `success` or `fail`. I will add that as an option for you. – iCollect.it Ltd Oct 31 '14 at 16:31
  • @SouPress: Updated to cover using your own deferreds. This will give you full control (although `always` should have worked). – iCollect.it Ltd Oct 31 '14 at 16:36
  • 100 requests would be the cap. I've looked in the Network panel and it takes 8 seconds with async: false, and 1,5 seconds with async: true (but broken). And I have good Internet so I guess not jamming the page for upwards of 8 seconds would be the nice thing to do, hence the efforts. – SouPress Oct 31 '14 at 16:37
  • @SouPress: You are right to avoid `async: false`. It is a bright-shiny gateway, *that leads to a very bad place* :) – iCollect.it Ltd Oct 31 '14 at 16:38