I can't figure out the right way to do that:
I'm calling external API for products and I get a response I want to add those products to my database and if the response contains a next_page url loop it again until there is no next_page left
Here is what I came up with:
var products = api.ProductsActive('GET', {includes: 'Images', limit: 1});
requestProducts = function(){
products.results.forEach(function(product){
var sameproduct = apiProducts.findOne({listing_id: product.listing_id});
if (sameproduct) {
console.log('found sameproduct');
return;
}
//Add userId to current product so we can assosicate products belong to "X" user
var productExtend = _.extend(product, {userId: Meteor.userId()});
apiProducts.insert(productExtend);
});
//If there is next page get the next page number and get products
var nextPage = products.pagination.next_page;
if (nextPage !== null) {
products = api.ProductsActive('GET', {includes: 'Images', page: nextPage, limit: 1});
console.log(products);
}
};
//loop at least once, and then if more pages found
do {
requestProducts();
}
while (products.pagination.next_page !== null);
Which doesn't seems to work right I'm not sure if this is the right method for such function I really help your input! Please help me figure out the right way for this!
Keep in mind javascript is asynchronous, so you need to handle your loop with callbacks instead of do. The reason for this is requestProducts likely uses a callback and the javascript interpreter doesn't "wait" for requestProducts() to finish running before it runs the next line of code.
You haven't provided any details of how the api works so I have to generalise.
If requestProducts() takes a parameter as a callback (which it should), use that to re-run itself:
var callback = function(err, result) {
if(products.pagination.next_page !== null) requestProducts(callback);
};
requestProducts(callback);
You would have to check the your api's documentation to check how the callback is defined but it would be something like the above where err is returned if there is a problem.
How this works is, when requestProducts is called the callback fires and if its not the last page to repeat itself until ..next_page == null
Related
I have created a script that performs a request with firebase. Since I want to avoid multiple queries on a relation at the same time, I use the transaction function as seen in the code. First, I get the current status of the relation and check if it is present or not. This seems to work. The first time is const current = null and the function to add value to data2 will be executed.
And if I run the script the second time, then he goes into the next function successfully (doSecondFunction()).
And unfortunately, no currentData returns to me here. I expect the value of the ref data2 that was added the first time. But as a result, I get null.
Is there a way to work in the transaction with the current state of the database?
const ref = firebase.database().ref('data2');
ref.once('value', function(snapshot) {
const current = snapshot.val();
if(current == null){
console.log('FIRST');
addValToRefFunction();
}else{
console.log('SECOND');
doSecondFunction(current, ref);
}
});
function doSecondFunction(current, ref){
ref.transaction(function(currentData){
console.log(current); // result is the right value
console.log(currentData); // result is null ?
if (currentData === current){
console.log('Check');
return;
}
}
There is no way control what the client sees as the current status of a node when starting a transaction. In my experience the first time the callback gets invoked, the value is almost always null, and the callback needs to handle that.
Also see my answer here for a description of the process: Firebase runTransaction not working - MutableData is null
I'm using a node.js server, the Spotify API, and the spotify-web-api-js node module to create a web application where the user can enter an artist's name, see a list of songs from related artists, and then optionally save that playlist to their own Spotify account. However, i'm still having trouble with the last step.
My user authorization flow is happening first:
if (params.access_token) {
s.setAccessToken(params.access_token);
s.getMe().then(function(data) {
console.log(data);
console.log(data.id);
user_id = data.id;
Below it is where the the actual details of the songs are gathered by the API. Although it is lower down, it occurs first and the user authorization only happens if the user clicks a second button on that page.
async.times(counter, function(n, next){
s.getArtistTopTracks(relatedArtists[n].id, "US", function (err, data2) {
relatedArtists[n].song = data2.tracks[0].name;
relatedArtists[n].uri = data2.tracks[0].uri;
console.log(relatedArtists[n].uri);
// make sure to put the access token here add song to playlist
// create array
song_uris.push(relatedArtists[n].uri);
console.log(song_uris);
// song_uris = relatedArtists[n].uri;
//
// console.log(song_uris);
next(null);
$("#playlist").load(function() {
s.addTracksToPlaylist(user_id, playlist_id, song_uris);
});
});
}, function(err) {
// console.table(relatedArtists);
for (k = 0; k < 20; k++)
{
$('#related-artist').append('<p><strong>' + relatedArtists[k].name + '</strong> -- \"' + relatedArtists[k].song + '\"</p>');
}
(JSBin of full code here, though it might not work because I use browserify on my own server)
Right now, on line 114 I have song_uris.push(relatedArtists[n].uri); pushing the contents into an array, using async.times. Since this is below where I create the playlist on line 66, it shows as an empty array:
s.createPlaylist(user_id, {name: 'Related Artist Playlist'}).then(function(data3) {
console.log(data3);
playlist_id = data3.uri;
playlist_id = playlist_id.substring(33);
console.log(playlist_id);
console.log(song_uris);
});
There, console.log(song_uris) shows an empty array, so addTracksToPlaylist() breaks like so:
On the other hand, if I try to addTracksToPlaylist() below, I don't have authorization to access the user's account.
The user authorization flow has been added later on after the basic functionality of showing a list of songs had already been working, but i'm not sure how to refactor it effectively in order to save that list to my user's playlist. At the moment, i'm only creating an empty playlist in the Spotify account.
What kind of event listener can I add so that it will wait until every single instance of async.times is performed, so that addTracksToPlaylist() can work? The DOM is already initially loaded before I get this data. I looked at this question, but it didn't quite help me solve this problem. Thanks!
EDIT: I now have the song_uri array created the way I need it, but I still can't get it into the playlist. I have been playing around with the location of my access token so that I can access the playlist created, but still no luck.
The console.log(song_uris); statement on line 130 shows the completed array that I need, but when I insert it into s.addTracksToPlaylist(user_id, playlist_id, song_uris); I get these errors in the developer console:
POST https://api.spotify.com/v1/users/tenderoni-/playlists/7MtQTzUsxD4IEJ8NmmE36q/tracks?uris= 400 (Bad Request)
bundle.js:10616
Uncaught (in promise) XMLHttpRequest {}
Basically, it's not receiving the parameter for some reason. And I log the playlist_id beforehand, so I can tell it's working (also, I see blank playlists with the specified title being created in my Spotify account).
Full updated code here: https://github.com/yamilethmedina/cs50xmiami/blob/master/assignments/portfolio/public/scripts.js
The callback argument to async.times can take a second 'results' argument, so if you call 'next' with the song_uri you get around each turn of the loop, you can get song_uris at the end, pass a callback through searchArtists, and use it there. Skeleton version:
$(document).ready(function($) {
$('#s').on('submit', function() {
searchArtists($('#originalartist').val(), function(err, song_uris) {
if (params.access_token) {
// omitted
s.setAccessToken(params.access_token);
s.getMe().then(function(data) {
console.log(data);
console.log(song_uris);
});
}
});
return false;
});
})
function searchArtists(originalArtist, callback) {
s.getArtistRelatedArtists(originalArtistId, function(err, data) {
async.times(counter, function(n, next) {
// omitted
next(related_artists[n].uri)
}, function(err, song_uris) {
callback(err, song_uris);
});
});
}
The YouTube API takes a very short amount of time for a request to its server to come back (In my case, I'm sending a search query). However, it's still too slow to make my program synchronous.
This is the Search Bar handler:
Template.search_bar.events({
'keypress #query' : function (evt,template) {
// template data, if any, is available in 'this'
if (evt.which === 13){
var url = template.find('#query').value;
$("#query").val('');
//YoutubeAPI calls go here
Template.list.search_get(url);
Links.insert({sess:Template.list.my_playlist_id,youtube_link:Session.get("search_results").items[0].snippet.title});
}
}
});
And this is what carries out the GET call:
Template.list.search_get= function(str){
var request = gapi.client.youtube.search.list({part:'snippet',q:str});
request.execute(function(response) {
str = JSON.stringify(response.result);
str = JSON.parse(str);
Session.set("search_results",str);
});
}
The top search result is displayed in a list, however the result is always displayed one result behind. Meaning the first search will yield undefined because the Session variable wouldn't have updated by that time, the second search will yield the results of the first search, and so on.
Any input on how I could go about resolving this would be great.
You don't need to use Session for this one. Try moving the Links.insert routine to your request callback, and things should start to look better ;)
I have a Firebase with a users reference, which has a field user_id.
([Firebase]/[app]/users/user_id)
I insert a user & assign a user_id. However, when I read the value of user_id from userRef (a reference to users), it does not read it the first time. Subsequent reads work perfectly fine.
Using the debugger, I can see that when the user is created, a user_id is assigned. It seems like my first call refreshes the Firebase reference, so that subsequent calls are now seeing the udpated Firebase (I don't think that is what it is - that is just how it appears).
This is my code to read the value of user_id:
var userID = 0;
userRef.on('value', function(snapshot) {
userID = snapshot.val().user_id;
});
alert(userID);
The first time, the alert shows 0. Every time after that, it shows the correct value.
To make the problem even stranger, I also have a games reference with a game_id, created in the same way as user. But my read on game_id (in the exact same way & at the same time) works every time.
Any ideas?
The issue here is that .on() doesn't (in general) trigger your callback function immediately. In particular, the first time you do a .on() at a location, Firebase has to go ask the server for the current value, wait for the response, and THEN call your callback. And it does this all asynchronously.
The way your code is currently written, "alert(userID);" is being run before your callback code ("userID = snapshot.val().user_id;") so it always reports 0 the first time. The simple fix is to move the alert() inside your callback:
var userID = 0;
userRef.on('value', function(snapshot) {
userID = snapshot.val().user_id;
alert(userID);
});
Here's a common methodology to wait on two callbacks, using using jQuery's Promise model and once:
var userID = 0;
// sends a callback to fx where the results can be stored
function defer( fx ) {
return function() {
var deferred = $.Deferred();
fx( function(snapshot) { deferred.resolve(snapshot.val(); } );
return deferred.promise();
}
}
$.when( // wait for both actions to complete
defer( function(callback) { userRef.once('value', callback) }),
defer( function(callback) { widgetRef.once('value', callback) })
).then( function(values) {
// both deferreds are done now
// and values contains an array with the snapshot.val() from each call
console.log(values);
});
Here I attach the screenshots of my console log results. Both of them are Objects. But they are not the same. What could be the problem? One is showing
(5) [{..},{..},{..},{..},{..}]
Another one just shows [].
let tmp_array = [];
this.database.ref('/users/').once('value', (snapshot) => {
snapshot.forEach( (childSnapshot) => {
var key = (childSnapshot.val() && childSnapshot.key) || 'Anonymous';
var name = (childSnapshot.val() && childSnapshot.val().name) || 'Anonymous';
var email = (childSnapshot.val() && childSnapshot.val().email) || 'Anonymous';
tmp_array.push({ key: key, email: email, name: name });
});
this.setState({ data: tmp_array });
this.getImageList(tmp_array);
console.log(tmp_array);
});
let tmp_array2 = [];
lodash.forEach(tmp_array, (value, key) => {
this.storage.ref().child(value.key + '.jpg').getDownloadURL().then(function (url) {
tmp_array2.push({ id: value.key, image: url });
});
});
this.setState({ image: tmp_array2 });
console.log(tmp_array2);
Welcome to programming with web APIs. You have a typical async problem here. I'll explain what's happening in your code, then give you first steps to a solution, point out some further considerations, and finally provide links with more information for you to read up on this incredibly common (but initially equally confusing) topic.
The problem: the download URLs are loaded asynchronously
Your code calls getDownloadURL, which happens asynchronously. This means that your other code continues while the URL is being loaded. Then when the URL is loaded, your callback is called with the URL. This means that any code that needs the URL, must be inside the callback.
This is easiest to see if you simply the code and add some log statements:
console.log("Before starting getDownloadURL");
this.storage.ref().child(value.key + '.jpg').getDownloadURL().then(function (url) {
console.log("Got download URL");
});
console.log("After starting getDownloadURL");
When you run this code it prints:
Before starting getDownloadURL
After starting getDownloadURL
Got download URL
That is probably not the order you expected, but it completely explains why the array is empty when you print it: the URLs haven't been loaded yet, so they haven't been added to the array yet.
The solution: put all code that needs the download URL inside the callback
To solve this problem all code that needs the download URL from the database must be inside the callback (or be called from there). So a first step would be:
let tmp_array2 = [];
lodash.forEach(tmp_array, (value, key) => {
this.storage.ref().child(value.key + '.jpg').getDownloadURL().then(function (url) {
tmp_array2.push({ id: value.key, image: url });
this.setState({ image: tmp_array2 });
console.log(tmp_array2);
});
});
When you run this, you will see that it logs the array as often as there are child nodes/images. Each time it gets a download URL (remember: this happens asynchronously) it adds it to the array, and then tells React that the state has changed (to it can update the UI).
Some additional considerations and problems
There are more pitfalls in my last code snippet still:
Calling setState() repeatedly may result in a UI that is only partially updated, or in flicker. In that case, consider checking if you've gotten all download URLs before calling setState with something like: if (tmp_array2.length === tmp_array.length) this.setState({ image: tmp_array2 });.
The download URL calls happen asynchronously, and may (I think, it depends on the backend implementation of that API) complete in a different order than in which you call them. In other words: the download URL for the second image may be returned before the download URL for the first image. If this is a concern for your application, be sure to either replicate the other information for the image in tmp_array2 (I think you already do this), or consider storing the download URLs in a way that allows you to associate them back with the items in tmp_array.
More info
As I said at the start: this is a very common problem for developers who are new to dealing with asynchronous APIs to run into. So there are probably hundreds more relevant questions/links if you search for them. Here are some of my favorites:
Doug's post on Firebase's asynchronous APIs
Handling Asynchronous Calls (Firebase) in functions
Best way to retrieve Firebase data and return it, or an alternative way
Why Does Firebase Lose Reference outside the once() Function?
JavaScript - Firebase value to global variable
Firebase use query result outside function
A [] is an empty array. The other [{..},{..},{..},{..},{..}] is an array containing 5 objects. Since you didn't specify which temp array is representing which logged array, I cannot help you determine why one array is empty.