Facebook APIFacebook JavaScript SDKJavaScriptPhotos

Facebook API JavaScript SDK get all Albums and Photos

Table of Contents

Introduction

Many apps use this nifty feature to get all of your Facebook photos, like Tinder for instance. It's exceedingly useful, as it makes it easy for a user to add Facebook photos to your application. In reality, it really just adds a convenience factor, as anyone could easily just download Facebook photos to their library anyway.

We will be using the JavaScript SDK for the examples, but can be used with Cordova virtually identically with this plugin. The demo is combined with Framework7 for presentation purposes, but the main aspect is using the Facebook API correctly.

Here's a demo video of me using the code in action. I posted the code I used in demo video with functions on Github: https://github.com/WebsiteBeaver/facebook-javascript-all-photos.

Getting Approved by Facebook

This is an extremely annoying step, but unfortunately necessary if you want to allow users of your application to retrieve their Facebook albums and photos. Here's a useful guide to show you to get user_photos permissions approved. You basically just take a video recording of how the permission is being used. On a website, you'd also send them a link, while on a mobile app, you'd submit your iOS simulator build file and/or Android APK file.

Once this is submitted, you'll have to wait up to seven business days, more or less, depending on the queue.

Using the API and JavaScript SDK

To make API calls to to Facebook's endpoints, you would use:

FB.api(string path, enum method, object params, function callback)

With Cordova, you'd use:

facebookConnectPlugin.api(string path, array permissions, function success, function failure)

The JavaScript SDK and Cordova plugin are mostly pretty similar, but there's slight differences as you can see. For the Javascript SDK, we're solely concerned with path and callback. With Cordova, you'll likely be using the entire parameter list. The unique aspect of the plugin is that it requires you fill out the permission for each call, and has a failure callback. In the regular Javascript SDK, you'll be error handling by the existence of an error object, as shown in the error handling section.

With the JavaScript SDK, you'll need to initialize it immediately after the opening <body> tag.

We will be using nested requests in certain parts, as well as heavily use cursor-based pagination. Cursor-based pagination is necessary, due to the fact that there's a set limit for how much data you can retrieve from each api call. The default is 25, but you can specify the amount to whatever you want, as long as you're not hitting the rate limit.

Login, Logout and Check Status

Login

FB.login(response => {
 if(response.authResponse) console.log('Logged in');
 else console.log('Cancelled authorization');
}, {scope: 'user_photos'});

Logout

FB.logout(response => {
  console.log('Logged out');
});

Check Login Status

FB.getLoginStatus(response => {
  if(response.status === 'connected') console.log('Logged in');
  else console.log('Logged out');
});

Get Albums

Let's get five albums at a time and lazy load the rest later. This is pretty simple, as you can utilize cursor-based pagination by attaching limit(n) to a field or edge. We'll also use the fields name, count and cover_photo, which are in enclosed in curly braces Sometimes you might want to get sub- fields or edges too. Like when I want to get the cover photo for each album, it returns some date I don't really need, as I'm strictly concerned with the icon for each album, which is the field named picture. This is an example of a nested request, which is done by using curly braces to enclose it.

Here's a full list of fields you can choose from.

FB.api('/me?fields=albums.limit(5){name,count,cover_photo{picture}}', response => {
  console.log(response);
});

Output:

{
  albums: {
    data: [
      {
        name: "Timeline Photos",
        count: 4,
        cover_photo: {
          picture: "https://scontent.xx.fbcdn.net/v/t1.0-0/s130x130/59593_4554_54_n.jpg?_nc_cat=0&oh=2845",
          id: "4664590403"
        },
        id: "5544509390434"
      },
      ... 4 more
    ],
    paging: {
      cursors: {
        before: "dfdf",
        after: "fddf"
      },
      next: "https://graph.facebook.com/v3.0/3443438/albums?access_token=fdfjkdfjdkfdfjk&pretty=0&fields=name%2Ccount%2Ccover_photo%7Bpicture%7D&limit=5&after=fddf"
    }
  },
  id: "4455495895"
}

It would be a good idea to store this in a variable, let's call it let fbAlbumsPhotosObj = {}. The following is optional, but I personally don't see the need to store the id of the cover photo. All we need is picture, so you may want to convert the cover_photo object into a string literal, in a loop for each album. I also don't really feel like the initial albums layer is necessary, as it's being stored in the variable fbAlbumsPhotosObj anyway.

FB.api('/me?fields=albums.limit(5){name,count,cover_photo{picture}}', response => {
  response.albums.data.forEach(album => {
    album.cover_photo = album.cover_photo.picture; //All we need is picture
  });

  fbAlbumsPhotosObj = response.albums; //Remove initial albums layer
});

Get More Albums

You might be wondering how get more albums, since the API call you just made will only give you five. By the way, this is the amount of objects in the data array. This is actually exceedingly simple, as all you need to do is make an AJAX call to the next URL inside of the paging object. The next property will no longer be there once there are no more albums retrieve.

I'll be using the native JavaScript Fetch API for the AJAX calls.

if(fbAlbumsPhotosObj.paging.hasOwnProperty('next')) { //If there are any more albums
  fetch(fbAlbumsPhotosObj.paging.next)
    .then(response => response.json())
    .then(response => {
      response.data.forEach(album => {
        album.cover_photo = album.cover_photo.picture; //All we need is picture
      });

      fbAlbumsPhotosObj.data.push(...response.data); //Append albums
      fbAlbumsPhotosObj.paging = response.paging; //Set paging to new values
    });
} else {
  console.log('no more albums');
}

Output:

{
  data: [
    {
      name: "Summer",
      count: 32,
      cover_photo: {
        picture: "https://scontent.xx.fbcdn.net/v/t1.0-0/s130x130/59593_4554_54_n.jpg?_nc_cat=0&oh=2845",
        id: "965564590633"
      },
      id: "854989390454"
    },
    ... 3 more
  ],
  paging: {
    cursors: {
      before: "dfdf",
      after: "fddf"
    },
    next: "https://graph.facebook.com/v3.0/3443438/albums?access_token=fdfjkdfjdkfdfjk&pretty=0&fields=name%2Ccount%2Ccover_photo%7Bpicture%7D&limit=5&after=fddf"
  }
}

As you can see, the output is essentially the same as getting your five albums initially, but without the albums outer layer and its id sibling property. So you you'll want to append this to your object and replace the paging. In this case, the user only had 9 albums, so only 4 more albums were needed to be lazy loaded. It'll be appended to the existing fbAlbumsPhotosObj.data array, while the paging will be set to a new value, without a next property, as there's nothing more to be loaded.

Get Photos from an Album

Getting photos is fairly straightforward, as it's extremely to similar to getting all albums. We'll be using pagination once again with limit, while using the fields picture and images, which contain the icon and full image URL, respectively. Nesting will not be for this.

Here's a full list of fields you can choose from.

FB.api(albumId + '/?fields=photos.limit(10){picture,images}', response => {
  console.log(response);
});

Output:

{
  photos: {
    data: [
      {
        picture: "https://scontent.xx.fbcdn.net/v/t1.0-0/s130x130/59593_4554_54_n.jpg?_nc_cat=0&oh=2845",
        images: [
          {
            height: 959,
            source: "https://scontent.xx.fbcdn.net/v/t1.0-9/15409549049054_55959954544_1219126565757322390_n.jpg?_nc_cat=0&oh=jk56jkf8fudf&oe=6BUF9583",
            width: 958
          },
          ...More image sizes
        ],
        id: "35503434905309568887"
      },
      ...9 More photos
    ],
    paging: {
      cursors: {
        before: "IO988hFKkndji5",
        after: "Opdfd87BJL945f"
      },
      next: "https://graph.facebook.com/v3.0/504595905445953/photos?access_token=fdfjkdfjdkfdfjk&pretty=0&fields=picture%2Cimages&limit=2&after=Opdfd87BJL945f"
    }
  },
  id: "46449434053"
}

So images is an array of all the image sizes Facebook has created for each picture. For this article, we'll be using the largest representation, which is the first element images array, rather than store the entire array for each photo's stored sizes. Let's create a property name called picture_full to store the URL and scrap the rest. Facebook returns ids as strings for some reason, so you should either convert it to an int or loosely check with ==.

const index = fbAlbumsPhotosObj.data.findIndex(album => album.id == albumId); //Get index of album

FB.api(albumId + '/?fields=photos.limit(' + limitPics + '){picture,images}', response => {
  response.photos.data.forEach(photo => {
    photo.picture_full = photo.images[0].source; //[0] is the largest image
    delete photo.images; //Only need one full image
  });

  fbAlbumsPhotosObj.data[index].photos = response.photos;
});

Now we have structure for how to access this later, as photos is added to the corresponding album in the data array. So to access photos from an album later, you'd do fbAlbumsPhotosObj.data[index].photos.

Get More Photos

We'll be doing an AJAX call on the next property of the photo, as you might assume from when we learned how to get more albums. The photos for each album will get pushed, until there are no more, while the paging will get set to the next value. As stated previously, you'll know when you reached the end, as there won't be a next property value anymore. Again, Facebook returns ids as strings for some reason, so you should either convert it to an int or loosely check with ==.

const index = fbAlbumsPhotosObj.data.findIndex(album => album.id == albumId); //Get index of album

if(fbAlbumsPhotosObj.data[index].photos.paging.hasOwnProperty('next')) { //If there are any more albums
  fetch(fbAlbumsPhotosObj.data[index].photos.paging.next)
    .then(response => response.json())
    .then(response => {
      response.data.forEach(photo => {
        photo.picture_full = photo.images[0].source; //[0] is the largest image
        delete photo.images; //Don't need the rest, only one
      });

      fbAlbumsPhotosObj.data[index].photos.data.push(...response.data); //Append photos in album
      fbAlbumsPhotosObj.data[index].photos.paging = response.paging; //Set paging to new values
    });
} else {
  console.log('no more albums');
}

Output:

{
  data: [
    {
      picture: "https://scontent.xx.fbcdn.net/v/t1.0-0/s130x130/59593_4554_54_n.jpg?_nc_cat=0&oh=2845",
      images: [
        {
          height: 959,
          source: "https://scontent.xx.fbcdn.net/v/t1.0-9/15409549049054_55959954544_1219126565757322390_n.jpg?_nc_cat=0&oh=jk56jkf8fudf&oe=6BUF9583",
          width: 958
        },
        ...More image sizes
      ],
      id: "2323050878755568887"
    },
    ...5 More photos
  ],
  paging: {
    cursors: {
      before: "IO988hFKkndji5",
      after: "Opdfd87BJL945f"
    }
  }
}

The Full Object

For reference, this is the structure of the object that we created called fbAlbumsPhotosObj. To reiterate, here what we did and changed:

  • The outer album layer was removed.
  • The cover_photo object was flattened to become single URL string.
  • A photos object was appended to each album data object.
  • The images array containing each images size Facebook has was removed in place of picture_full, which is the largest representation.

As stated before, none of these things are necessary to do. You're free to customize your cached object variable any way you like. This just happened to be the approach that seemed intuitive to me for easy access to each album, photo and keeping track of if I need to lazy load anymore.

{
  data: [
    {
      name: "Timeline Photos",
      count: 28,
      cover_photo: "https://scontent.xx.fbcdn.net/v/t1.0-0/s130x130/59593_4554_54_n.jpg?_nc_cat=0&oh=2845",
      id: 55899008509390434,
      photos: {
        data: [
          {
            picture: "https://scontent.xx.fbcdn.net/v/t1.0-0/s130x130/59593_4554_54_n.jpg?_nc_cat=0&oh=2845",
            picture_full: "https://scontent.xx.fbcdn.net/v/t1.0-9/15409549049054_55959954544_1219126565778878722390_n.jpg?_nc_cat=0&oh=jk56jkf8fudf&oe=6BUF9583",
            id: "35509305309568887"
          },
          ...More photos
        ],
        paging: {
          cursors: {
            before: "IO988hFKkndji5",
            after: "Opdfd87BJL945f"
          }
        }
      }
    },
    ...More Albums
  ],
  paging: {
    cursors: {
      before: "dfdf",
      after: "fddf"
    },
    next: "https://graph.facebook.com/v3.0/3443438/albums?access_token=fdfjkdfjdkfdfjk&pretty=0&fields=name%2Ccount%2Ccover_photo%7Bpicture%7D&limit=5&after=fddf"
  }
}

Get Everything at Once

To get all albums and photos all at once, you might think that you should turn your functions into promises. This would work, but Facebook has a more concise and efficient way of doing this with nesting, as we did when we got albums for the first time. Here's an example of what that would look like.

FB.api('/me?fields=albums.limit(5){name,count,cover_photo{picture},photos.limit(10){picture,images}}', response => {
  console.log(response);
});

Just get Profile Picture

Sometimes you might just need the user's profile picture. This is a very simple request.

FB.api('/me?fields=picture.height(9999)', response => {
  console.log(response);
});

Output:

{
  id: "98537733395171745",
  picture: {
    data: {
      height: 946,
      is_silhouette: true,
      url: "https://platform-lookaside.fbsbx.com/platform/profilepic/?asid=9483483855903&height=9999&ext=85779053&hash=Oi948JkpLIQkn945KVCs",
      width: 946
    }
  }
}

You might be wondering why I randomly chose 9999 for the height. This is because I wanted to get the largest representation. Based off of Facebook's docs, common sense would dictate that using type(large) would give you its largest representation. This is actually not the case, and the only way to achieve this is by using an arbitrarily big number.

Also, is_sillhoute simply tells you if it's the default generic profile picture you get when you sign up or not.

Error Handling

There's three possible scenarios for a Facebook: an error, no response or success.

FB.api('', response => {
  if(response.error) {
    console.log(response.error);
  } else if(!response) {
    console.log('noResponse');
  } else {
    //Success
  }
});

With Cordova, you'll be using the failure callback function instead.

facebookConnectPlugin.api('', [], response => {
  if(!response) {
    console.log('noResponse');
  } else {
    //Success
  }
}, error => {
  console.log(error);
});

If it's an error, you'll get an error object as the response.

{
  error: {
    message: "Message describing the error",
    type: "OAuthException",
    code: 190,
    error_subcode: 460,
    error_user_title: "A title",
    error_user_msg: "A message",
    fbtrace_id: "EJplcsCHuLu"
  }
}

Here's a description of the properties and error codes. There's many different approaches to dealing with the error object. A simple way could be by just alerting error.message to the user.

You'll also probably want to error handle the AJAX calls when you get more albums or photos to give the user a message.