Programmatically Obtain All YouTube Videos Belonging to a User - javascript

Utilizing the YouTube Data API we can make a query to to obtain the first 50 (maximum amount of results obtainable with a single query) videos belonging to a user with the following request: https://www.googleapis.com/youtube/v3/search?key={access_key}&channelId={users_id}&part=id&order=date&maxResults=50&type=video
If there are more than 50 videos, then the resulting JSON will have a nextPageToken field, to obtain the next 50 videos we can append &pageToken={nextPageToken} to the above request, producing the next page of 50 videos. This is repeatable until the nextPageToken field is no longer present.
Here is a simple JavaScript function I wrote using the fetch API to obtain a single page of videos, specified by the nextPageToken parameter (or lack thereof).
function getUploadedVideosPage(nextPageToken) {
return new Promise((resolve, reject) => {
let apiUrl = 'https://www.googleapis.com/youtube/v3/search?key={access_key}&channelId={users_id}&part=id&order=date&maxResults=50&type=video';
if(nextPageToken)
apiUrl += '&pageToken=' + nextPageToken;
fetch(apiUrl)
.then((response) => {
response.json()
.then((data) => {
resolve(data);
});
});
});
}
Now we need a wrapper function that will iteratively call getUploadedVideosPage for as long as we have a nextPageToken. Here is my 'working' albeit dangerous (more on this later) implementation.
function getAllUploadedVideos() {
return new Promise((resolve, reject) => {
let dataJoined = [];
let chain = getUploadedVideosPage();
for(let i = 0; i < 20000; ++i) {
chain = chain
.then((data) => {
dataJoined.push(data);
if(data.nextPageToken !== undefined)
return getUploadedVideosPage(data.nextPageToken);
else
resolve(dataJoined);
});
}
});
}
The 'dangerous' aspect is the condition of the for loop, theoretically it should be infinite for(;;) since we have no predefined way of knowing exactly how many iterations to make, and the only way to terminate the loop should be with the resolve statement. Yet when I implement it this way it truly is infinite and never terminates.
Hence why I hard coded 20,000 iterations, and it seems to work but I don't trust the reliability of this solution. I was hopping somebody here can shed some light on how to go about implementing this iterative Promise chain that has no predefined terminating condition.

You can do this all with one function that calls itself if applicable.
You are also using an explicit promise construction anti-pattern wrapping fetch() in new Promise since fetch() already returns a promise
function getVideos(nextPageToken, results = []) {
let apiUrl = 'https://www.googleapis.com/youtube/v3/search?key={access_key}&channelId={users_id}&part=id&order=date&maxResults=50&type=video';
if (nextPageToken) {
apiUrl += '&pageToken=' + nextPageToken;
}
// return fetch() promise
return fetch(apiUrl)
.then(response => response.json())
.then(data => {
// merge new data into final results array
results = results.concat(data);
if (data.nextPageToken !== undefined) {
// return another request promise
return getVideos(data.nextPageToken, results);
} else {
// all done so return the final results
return results
}
});
}
// usage
getVideos().then(results=>{/*do something with all results*/})
.catch(err=>console.log('One of the requests failed'));

Get rid of the for loop altogether. Something like this:
function getAllUploadedVideos() {
return new Promise((resolve, reject) => {
let dataJoined = [];
function getNextPage(nextPageToken) {
return new Promise((resolve, reject) => {
getUploadedVideosPage(nextPageToken)
.then((data) => {
dataJoined.push(data);
if(data.nextPageToken !== undefined) {
resolve(data.nextPageToken);
}
else {
reject();
}
})
.catch((err) => {
// Just in case getUploadedVideosPage errors
reject(err);
});
});
}
getNextPage()
.then((nextPageToken) => {
getNextPage(nextPageToken);
})
.catch(() => {
// This will hit when there is no more pages to grab
resolve(dataJoined);
});
});
}

Related

Event listener for foreach loop completion

so what I have right now is
window.addEventListener('load', fetchInfo)
function fetchInfo() {
const tableRows = //an array of results
tableRows.forEach((row) => {
const rowId = //get the id of each row
fetch(...) //fetch some stuff using the id
.then(() => {
//do some stuff
return rowId;
})
.then((id) => {
//do some stuff
}
})
})
}
basically using rowId to fetch information and populate each table row, so this happens a few times, the table maxes out at 10 rows so max 10 fetches
I want to have an event listener to see when all the fetching is done, aka when the table is completely done loading. How should I go about that?
Edit: these fetches are api requests so they take a few seconds to respond. I've tried using Promise.all(tableRows.map(row) => and it returned results before the api could respond. So in the end, it still doesn't really detect when does the table actually finish loading information.
Use Promise.all:
async function fetchInfo() {
const tableRows = //an array of results
const results = await Promise.all(tableRows.map(row => fetch(...))
console.log(results)
}
From the suggestion in the comments length was indeed the way, however that would only be half of the solution. Because the key point here is that I want to know whether or not the table has been fully populated with requested information.
Tried Promise.all and map() and didn't work out because as stated in my edit, these will return when the fetch was called, when the fetch was still pending, and doesn't really care if the fetch was a 200 OK, which was what I needed.
So the solution was to use response.status
function fetchInfo() {
const tableRows = ...
let successFetch = 0;
tableRows.forEach((row) => {
fetch(...)
.then((response) => {
if (response.status == 200) {
successFetch = successFetch + 1
}
if (successFetch == tableRows.length) {
//this point here was exactly what I needed
}
return response;
})
.then((response) => {
...
})
})
}
Use Promise.all to wait for all promises to complete
Promise.all(
tableRows.map((row) => {
return fetch(...).then(() => {
return rowId;
})
})
).then((results) => {
console.log(results);
})

Javascript: Making sure one async function doesn't run until the other one is complete; working with promises

I'm working with fetching information from a github repository. I want to get the list of pull requests within that repo, get the list of commits associated with each pull request, then for each commit I want to get information such as the author of the commit, the number of files associated with each commit and the number of additions and deletions made to each file. I'm using axios and the github API to accomplish this. I know how to work with the API, but the promises and async functions are keeping me from accomplishing my task. I have the following code:
const axios = require('axios');
var mapOfInformationObjects = new Map();
var listOfCommits = [];
var listOfSHAs = [];
var gitApiPrefix = link I'll use to start fetching data;
var listOfPullRequestDataObjects = [];
var listOfPullRequestNumbers = [];
var mapOfPullNumberToCommits = new Map();
function getAllPullRequests(gitPullRequestApiLink) {
return new Promise((resolve, reject) => {
axios.get(gitPullRequestApiLink).then((response) =>{
listOfPullRequestDataObjects = response['data'];
var k;
for (k = 0; k < listOfPullRequestDataObjects.length; k++){
listOfPullRequestNumbers.push(listOfPullRequestDataObjects[k]['number']);
}
resolve(listOfPullRequestNumbers);
}).catch((error) => {
reject(error);
})
})
}
function getCommitsForEachPullRequestNumber(listOfPRNumbers) {
var j;
for (j = 0; j < listOfPRNumbers.length; j++) {
currPromise = new Promise((resolve, reject) => {
currentGitApiLink = gitApiPrefix + listOfPRNumbers[j] + "/commits";
axios.get(currentGitApiLink).then((response) => {
mapOfPullNumberToCommits.set(listOfPRNumbers[j], response['data']);
resolve("Done with Pull Request Number: " + listOfPRNumbers[j]);
}).catch((error) => {
reject(error);
})
})
}
}
function getListOfCommits(gitCommitApiLink){
return new Promise((resolve, reject) => {
axios.get(gitCommitApiLink).then((response) => {
resolve(response);
}).catch((error) => {
reject(error);
})
})
}
So far, I made some functions that I would like to call sequentially.
First I'd like to call getAllPullRequestNumbers(someLink)
Then I'd like to call getCommitsForEachPullRequestNumber(listofprnumbers)
Then getListOfCommits(anotherLink)
So it would look something like
getAllPullRequestNumbers(someLink)
getCommitsForEachPullRequestNumber(listofprnumbers)
getListOfCommits(anotherlink)
But two problems arise:
1) I'm not sure if this is how you would call the functions so that the first function in the sequence completes before the other.
2) Because I'm not familiar with Javascript, I'm not sure, especially with the getCommitsForEachPullRequestNumber function since you run a loop and call axios.get() on each iteration of the loop, if this is how you work with promises within the functions.
Would this be how you would go about accomplishing these two tasks? Any help is much appreciated. Thanks!
When you a number of asynchronous operations (represented by promises) that you can run all together and you want to know when they are all done, you use Promise.all(). You collect an array of promises and pass it to Promise.all() and it will tell you when they have all completed or when one of them triggers an error. If all completed, Promise.all() will return a promise that resolves to an array of results (one for each asynchronous operation).
When you're iterating an array to do your set of asynchronous operations, it then works best to use .map() because that helps you create a parallel array of promises that you can feed to Promise.all(). Here's how you do that in getCommitsForEachPullRequestNumber():
function getCommitsForEachPullRequestNumber(listOfPRNumbers) {
let mapOfPullNumberToCommits = new Map();
return Promise.all(listOfPRNumbers.map(item => {
let currentGitApiLink = gitApiPrefix + item + "/commits";
return axios.get(currentGitApiLink).then(response => {
// put data into the map
mapOfPullNumberToCommits.set(item, response.data);
});
})).then(() => {
// make resolved value be the map we created, now that everything is done
return mapOfPullNumberToCommits;
});
}
// usage:
getCommitsForEachPullRequestNumber(list).then(results => {
console.log(results);
}).catch(err => {
console.log(err);
});
Then, in getListOfCommits(), since axios already returns a promise, there is no reason to wrap it in a manually created promise. That is, in fact, consider a promise anti-pattern. Instead, just return the promise that axios already returns. In fact, there's probably not even a reason to have this as a function since one can just use axios.get() directly to achieve the same result:
function getListOfCommits(gitCommitApiLink){
return axios.get(gitCommitApiLink);
}
Then, in getAllPullRequests() it appears you are just doing one axios.get() call and then processing the results. That can be done like this:
function getAllPullRequests(gitPullRequestApiLink) {
return axios.get(gitPullRequestApiLink).then(response => {
let listOfPullRequestDataObjects = response.data;
return listOfPullRequestDataObjects.map(item => {
return item.number;
});
});
}
Now, if you're trying to execute these three operations sequentially in this order:
getAllPullRequests(someLink)
getCommitsForEachPullRequestNumber(listofprnumbers)
getListOfCommits(anotherlink)
You can chain the promises from those three operations together to sequence them:
getAllPullRequests(someLink)
.then(getCommitsForEachPullRequestNumber)
.then(mapOfPullNumberToCommits => {
// not entirely sure what you want to do here, perhaps
// call getListOfCommits on each item in the map?
}).catch(err => {
console.log(err);
});
Or, if you put this code in an async function, then you can use async/awit:
async function getAllCommits(someLink) {
let pullRequests = await getAllPullRequests(someLink);
let mapOfPullNumberToCommits = await getCommitsForEachPullRequestNumber(pullRequests);
// then use getlistOfCommits() somehow to process mapOfPullNumberToCommits
return finalResults;
}
getAllCommits.then(finalResults => {
console.log(finalResults);
}).catch(err => {
console.log(err);
});
not as clean as jfriend00 solution,
but I played with your code and it finally worked
https://repl.it/#gui3/githubApiPromises
you get the list of commits in the variable listOfCommits
I don't understand the purpose of your last function, so I dropped it

Sequential execution of Promise.all

Hi I need to execute promises one after the other how do I achieve this using promise.all any help would be awesome. Below is the sample of my code I am currently using but it executes parallel so the search will not work properly
public testData: any = (req, res) => {
// This method is called first via API and then promise is triggerd
var body = req.body;
// set up data eg 2 is repeated twice so insert 2, 5 only once into DB
// Assuming we cant control the data and also maybe 3 maybe inside the DB
let arrayOfData = [1,2,3,2,4,5,5];
const promises = arrayOfData.map(this.searchAndInsert.bind(this));
Promise.all(promises)
.then((results) => {
// we only get here if ALL promises fulfill
console.log('Success', results);
res.status(200).json({ "status": 1, "message": "Success data" });
})
.catch((err) => {
// Will catch failure of first failed promise
console.log('Failed:', err);
res.status(200).json({ "status": 0, "message": "Failed data" });
});
}
public searchAndInsert: any = (data) => {
// There are database operations happening here like searching for other
// entries in the JSON and inserting to DB
console.log('Searching and updating', data);
return new Promise((resolve, reject) => {
// This is not an other function its just written her to make code readable
if(dataExistsInDB(data) == true){
resolve(data);
} else {
// This is not an other function its just written her to make code readable
insertIntoDB(data).then() => resolve(data);
}
});
}
I looked up in google and saw the reduce will help I would appreciate any help on how to convert this to reduce or any method you suggest (Concurrency in .map did not work)
the Promises unfortunatelly does not allow any control of their flow. It means -> once you create new Promise, it will be doing its asynchronous parts as they like.
The Promise.all does not change it, its only purpose is that it checks all promises that you put into it and it is resolved once all of them are finished (or one of them fail).
To be able to create and control asynchronous flow, the easiest way is to wrap the creation of Promise into function and create some kind of factory method. Then instead of creating all promises upfront, you just create only one promise when you need it, wait until it is resolved and after it continue in same behaviour.
async function doAllSequentually(fnPromiseArr) {
for (let i=0; i < fnPromiseArr.length; i++) {
const val = await fnPromiseArr[i]();
console.log(val);
}
}
function createFnPromise(val) {
return () => new Promise(resolve => resolve(val));
}
const arr = [];
for (let j=0; j < 10; j++) {
arr.push(createFnPromise(Math.random()));
}
doAllSequentually(arr).then(() => console.log('finished'));
PS: It is also possible without async/await using standard promise-chains, but it requires to be implemented with recursion.
If anyone else cares about ESLint complaining about the use of "for" and the "no await in loop" here is a typescript ESLint friendly version of the above answer:
async function runPromisesSequentially<T>(promises: Array<Promise<T>>):Promise<Array<T>> {
if (promises.length === 0) return [];
const [firstElement, ...rest] = promises;
return [await firstElement, ...(await runPromisesSequentially(rest))];
}
You can then just replace Promise.all by runPromisesSequentially.
#lmX2015's answer is close but it's taking in promises that have already started executing.
A slight modification fixes it
export async function runPromisesSequentially<T>(functions: (() => Promise<T>)[]): Promise<T[]> {
if (functions.length === 0) {
return [];
}
const [first, ...rest] = functions;
return [await first(), ...(await runPromisesSequentially(rest))];
}

Promise not returning expected value

Ive been learning about promises and I have a question. I have a function named getNumber which return an array of number (for the sake of understanding). The I used that function to iterate over that array and make a http request for each value (with a setTimeout to make a delay between calls)
Then I want to used that information gathered in a then function, but it's giving me a 'undefined error'. obviously something is wrong here, but I cant see it. Do you guy know how can I fix this and what is wrong?
var getNumbers = () => {
return new Promise(function(resolve, reject) {
console.log("In function getNumbers");
var data = [1,2,3,4,5,6,7,8,9];
resolve(data);
});
};
getNumbers()
.then(numbersArray => {
//Supposed to return array of posts title
return numbersArray.map(number => {
console.log("Reading number" + number);
setTimeout(() => {
//make a http request
return getHtml("https://jsonplaceholder.typicode.com/posts/"+number)
.then(function(post) {
return post.title;
})
}, 10000);//make a request each ten seconds
});
})
.then(postTitlesArray => {
//Shows array of undefined
console.log(postTitlesArray)
});
function getHtml(webUrl) {
return fetch(webUrl)
.then(function(res) {
return res.json();
});
}
There are several conceptual things in the way of your approach doing what you want.
First, .map() is synchronous. That means it runs to completion and doesn't wait for any async operations to finish.
Second, setTimeout() is non-blocking. It just schedules a timer for some time in the future and then your .map() callback returns immediately, returning nothing.
So, your approach doesn't work at all.
From your comments, it appears that what you're trying to accomplish is to make a bunch of network calls in a loop, but put a delay between them so you don't get rate limited. There are a bunch of ways to do that.
There are two basic concepts you need to make that work:
Make your async operations be sequential so the next one doesn't get initiated until the prior one is done.
Put a delay that works with promises before starting the next one.
I'll first show an ES7 approach using async/await as it probably looks conceptually the simplest.
Using async/await to sequence asynchronous array access
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
getNumbers().then(async function(numbersArray) {
//Supposed to return array of posts title
let results = [];
let delayT = 0; // first delay is zero
for (let number of numbersArray) {
console.log("Reading number" + number);
let r = await delay(delayT).then(() => {
delayT = 10 * 1000; // 10 seconds for subsequent delays
return getHtml("https://jsonplaceholder.typicode.com/posts/"+number).then(function(post) {
return post.title;
});
});
results.push(r);
}
return results;
});
Using .reduce() to sequence asynchronous array acess
If you wanted to do it without async/await, then you could use the .reduce() design pattern for sequencing async iteration of an array:
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
getNumbers().then(numbersArray => {
//Supposed to return array of posts title
let results = [];
let delayT = 0; // first delay is zero
return numersArray.reduce((p, number) => {
return p.then(() => {
return delay(delayT).then(() => {
delayT = 10 * 1000; // 10 seconds for subsequent delays
return getHtml("https://jsonplaceholder.typicode.com/posts/"+number).then(function(post) {
results.push(post.title);
});
});
});
}, Promise.resolve()).then(() => {
// make array of results be the resolved value of the returned promise
return results;
});
});
Note that both of these algorithms are coded to not delay the first operation since presumably you don't need to, so it only delays between successive operations.
As coded, these are modeled after Promise.all() and they will reject if any of your getHtml() calls reject. If you want to return all results, even if some reject, then you can change:
return getHtml(...).then(...)
to
return getHtml(...).then(...).catch(err => null);
which will put null in the returned array for any result that failed, or if you want to log the error, you would use:
return getHtml(...).then(...).catch(err => {
console.log(err);
return null;
});
Generic Helper Function
And, since this is a somewhat generic problem, here's a generic helper function that lets you iterate an array calling an async operation on each item in the array and accumulating all the results into an array:
// Iterate through an array in sequence with optional delay between each async operation
// Returns a promise, resolved value is array of results
async iterateArrayAsync(array, fn, opts = {}) {
const options = Object.assign({
continueOnError: true,
delayBetweenAsyncOperations: 0,
errPlaceHolder: null
}, opts);
const results = [];
let delayT = 0; // no delay on first iteration
for (let item of array) {
results.push(await delay(delayT).then(() => {
return fn(item);
}).catch(err => {
console.log(err);
if (options.continueOnError) {
// keep going on errors, let options.errPlaceHolder be result for an error
return options.errPlaceHolder;
} else {
// abort processing on first error, will reject the promise
throw err;
}
}));
delayT = options.delayBetweenAsyncOperations; // set delay between requests
}
return results;
}
This accepts options that let you continueOnError, lets you set the delay between each async operation and lets you control the placeholder in the array of results for any failed operation (only used if continueOnError is set). All the options are optional.
I assume what you want to do is: 1) Get a list of numbers using getNumbers. 2) Iterate through each number from step one and form a url, with which an http request is made every ten seconds. 3) If a request is successfully sent, wait for its response. 4) Get post.title from response. 5) Wait until the iteration in step 2 ends, and return an array of all post.titles received from each call.
With the above assumptions in mind, I edit your code a bit and the following solution will work. See in jsfiddle.
I think the main problem with your code is that the map method doesn't return anything.
const getNumbers = () => {
return new Promise(function(resolve, reject) {
console.log("In function getNumbers");
var data = [1,2,3,4,5,6,7,8,9];
resolve(data);
});
};
const delay = (number, t) => {
return new Promise((resolve) => {
setTimeout(() => {
//make a http request
resolve(
getHtml("https://jsonplaceholder.typicode.com/posts/"+number)
.then(function(post) {
console.log('title', post.title)
return post.title;
})
)
}, t)
})
}
const getHtml = (webUrl) => {
return fetch(webUrl)
.then(function(res) {
return res.json();
});
}
getNumbers()
.then(numbersArray => {
//Supposed to return array of posts title
return Promise.all(numbersArray.map((number, i) => {
console.log("Reading number" + number);
return delay(number, 10000*(i+1));//make a request each ten seconds
}))
.then(postTitlesArray => {
console.log(postTitlesArray)
});
})
You can use Promise.all, assuming numbers are not in the thousands or else you can use batched Promise.all.
Then use throttlePeriod from here to make sure only 1 request is made every 10 seconds.
And then resolve failed requests with a special value so you don't loose all successes if one fails:
var getNumbers = () => {
return new Promise(function (resolve, reject) {
console.log("In function getNumbers");
var data = [1, 2, 3, 4, 5, 6, 7, 8, 9];
resolve(data);
});
};
function getHtml(webUrl) {
return fetch(webUrl)
.then(function (res) {
return res.json();
});
}
const Fail = function(reason){this.reason=reason;};
const isFail = x=>(x&&x.constructor)===Fail;
const notFail = x=>!isFail(x);
//maximum 1 per 10 seconds
//you can get throttle period from here:
//https://github.com/amsterdamharu/lib/blob/master/src/index.js
const max1Per10Seconds = lib.throttlePeriod(1,10000)(getHtml);
getNumbers()
.then(
numbersArray =>
Promise.all(//process all numbers
numbersArray
.map(//map number to url
number =>
`https://jsonplaceholder.typicode.com/posts/${number}`
)
//map url to promise
//max1Per10Seconds calls getHtml maximum 1 time per 10 seconds
// (will schedule the calls)
.map(max1Per10Seconds)
.map(//map promise to promise that does not reject
p=>//instead of rejecting promise, resolve with Fail value
//these Fail values can be filtered out of the result later.
//(see last then)
p.catch(err=>new Fail([err,number]))
)
)
).then(
//got the results, not all results may be successes
postTitlesArray => {
//is a comment really needed here?
const successes = postTitlesArray.filter(notFail);
const failed = postTitlesArray.filter(isFail);
}
);

Possible to add promises to an array and resolve them based on a single action?

I'm attempting to stop multiple requests from hitting the disk at once by caching requests and storing promises into an array. When the initial request finishes it should resolve all of the promises. Here's what I have, but unfortunately it doesn't look like new Promise() can be used this way, and deffered is no longer part of the spec. Note: some ES6 syntax such as const and the rocket operator are present in this example
This is a NodeJS application and I would prefer to not bring in any external libraries, however I will if necessary.
var observers = {}
function resolveObservers(link, value) {
for(var i = observers[link].length - 1; i >= 0; i--) {
if(observers[link][i] != null) {
observers[link][i].resolve(value)
observers[link].splice(i, 1)
}
}
}
function get(link) {
const b64link = base64.encode(link)
const promise = new Promise()
var handle = false
if(observers[b64link] == null) {
observers[b64link] = []
handle = true
} else if(observers[b64link].length == 0) {
handle = true
}
observers[b64link].push(promise)
if(handle) {
doAsyncOne.then(() => {
doAsyncTwo.then(() => {
doAsyncThree.then(data => {
resolveObservers(b64link, data)
})
})
})
}
}
The idea is that the Async code will only execute one time, and once it finishes all promises created by parallel requests will be resolved.
EDIT: I'm aware of how Promises in JS are normally used, I guess I'm looking for how Promises are used in other languages, usually called deferring.
EDIT2: You should be able to chain this event, for example:
get('...').then(data => {
// ...
})
You still can use the new Promise constructor in that way, even if you don't have deferreds any more:
var observers = {}
function get(link) {
const b64link = base64.encode(link)
return new Promise(resolve => {
if (observers[b64link] == undefined) {
observers[b64link] = [];
}
observers[b64link].push(resolve);
if (observers[b64link].length == 1) {
doAsyncOne
.then(() => doAsyncTwo)
.then(() => doAsyncThree)
.then(data => {
for (resolve of resolveObservers[b64link])
resolve(data)
}, err => {
err = Promise.rejct(err)
for (resolve of resolveObservers[b64link])
resolve(err)
})
}
});
}
But as you can see, error handling is not especially pretty (you'd even forgotten it completely), this is basically the deferred antipattern. There's a much simpler solution - just cache the promise objects themselves; they're values like every other and can be memoised! You don't even need to construct a new promise on every call:
var promises = {}
function get(link) {
const b64link = base64.encode(link)
if (promises[b64link] == undefined) {
promises[b64link] = doAsyncOne
.then(() => doAsyncTwo)
.then(() => doAsyncThree);
}
return promises[b64link];
}
That's it!

Categories

Resources