Implementing Flickr.Net

Posted 26 April 2010, 15:27 | by | | Perma-link

Flickr.NET is a ".NET library for accessing the Flickr API". It's hosted on CodePlex, and (thankfully for me) updated to version 3.0 Beta just before I downloaded it.

I host all my images here on the site, provide RSS feeds of both the latest photos and each album, however beyond using a service like twitterfeed I'd not found a nice way to integrate it into other social networks - Facebook can only accept one blog feed directly and while Google Buzz picked up on the blog feed through the "Connected Sites" options (I guess through the WebMaster tools association) it only seems to accept the first feed it finds, so I thought if I post the images to Flickr as well, then there would be more integration for them.

So, after spending a little time nosing around the Flickr API docs, I went off and got the latest version of Flickr.NET, dropped it in my projects /bin folder, and added a reference to it.

The next step was to request an API key from Flickr - you'll want a different key for each application you produce as the query limits are per key - and then set up the application - you need to give it a description, pick your application type (Desktop, Web or Mobile) and then define how you're handling authentication, for a web application this means supplying a callback url - while testing http://localhost/ addresses (with port numbers if required) worked fine.

Flickr.NET comes with it's own configuration section that you could use to set up Flickr.NET:

<flickrNet apiKey="APIKEY" secret="SHAREDSECRET" cacheDisabled="true">

Which then allows you to call the library as:

var flickr = new FlickrNet.Flickr();

However, I found that this didn't play nicely in a Medium Trust environment (the main disadvantage of shared hosting), as the constructor threw an exception complaining that the it couldn't write to the cache location (even though it's configured to be disabled) - I'll admit now that I didn't look too hard into diagnosing this issue - I'm only submitting images, rather than downloading them, so I didn't feel much need for caching.

So I pulled the API Key and Shared Secret out into the main appSettings settings, and factored out the constructor into a simple factory class:

public class FlickrControl
{
  internal static Flickr GetFlickr() {
    // Disable Cache before calling anything else
    Flickr.CacheDisabled = true;

    // Create Flickr instance with simple constructor.
    var flickr = new Flickr(WebConfigurationManager.AppSettings["FlickrApi"],
                            WebConfigurationManager.AppSettings["FlickrSecret"]);

    return flickr;
  }
}

I then created two new controllers - one to handle the initial login request, and one to handle the response back from Flickr:

// User wants to authenticate with Flickr
public ActionResult Flickr() {
  // Store calling page in session to return the user to later
  Session["Referrer"] = null != Request.UrlReferrer ?
                       Request.UrlReferrer.PathAndQuery : string.Empty;

  // Get an instance of Flickr
  var flickr = FlickrControl.GetFlickr();

  // Redirect the user to Flickr authentication service
  // asking for Delete priviledges (so we can remove images).
  return new RedirectResult(flickr.AuthCalcWebUrl(AuthLevel.Delete));
}

// Flickr is returning a logged in user
public ActionResult FlickrReturn(string frob) {
  var flickr = FlickrControl.GetFlickr();

  // Generate an Auth Token from the "frob" returned from Flickr
  var auth = flickr.AuthGetToken(frob);

  // Store the authentication token for later use
  // ExternalSites.FlickrAuth is a static string value for finding this object.
  Session[ExternalSites.FlickrAuth] = auth;

  // See if we can find the users previous request to return them to.
  var referrer = Session["Referrer"] as string;

  if (!string.IsNullOrEmpty(referrer)) {
    // We found their previous page, bounce them back
    return new RedirectResult(referrer);
  }

  return RedirectToAction("Index");
}

Then when a user uploads, edits or deletes an image from the site, we check to see if they are authenticated with Flickr, and perform the same action on Flickr:

// Inside the Upload action
// Check to see if the user is Authenticated, and that they want to upload the image
if (null != Session[ExternalSites.FlickrAuth] && editPhoto.UploadToExternal) {
  var auth = Session[ExternalSites.FlickrAuth] as Auth;

  if (auth != null) {
    var flickr = FlickrControl.GetFlickr();
    // Add the user's auth token to our Flickr instance
    flickr.AuthToken = auth.Token;

    // See below
    FlickrControl.UploadImage(photo, flickr, ImageData.FileName);

    // Upload image adds the Flickr id to the photo, so we need to save
    // that to the image as well.
    m_PhotoRepository.Save();
  }
}

This is the method for uploading an image:

internal static void UploadImage(DoodlePhoto photo, Flickr flickr, string fileName) {
  if (null == photo.PhotoDetail) {
    // If editing a photo, the image won't be in memory, so needs to be loaded.
    photo.PhotoDetailReference.Load();
  }

  if (null == photo.Album) {
    // If editing a photo, the Album info won't be in memory, so needs to be loaded.
    photo.AlbumReference.Load();
  }

  // UploadPicture returns the id of the image on Flickr
  photo.FlickrId = flickr.UploadPicture(
              // Raw stream of image bytes
              new MemoryStream(photo.PhotoDetail.BytesOriginal.ToArray()),
              // might as well pass in string.Empty here,
              // not really needed as we're passing in a stream
              fileName,
              // The title that's displayed in Flickr
              photo.Caption,
              // The description that appears under the image in Flickr
              photo.Description,
              // The tag list
              photo.CollapsedTags,
              // Set the permissions on the Album - if it's public,
              // whether Family or Friends can see it.
              photo.Album.IsPublic, true, true,
              // What type of image it is (they are effectively all photos).
              ContentType.Photo,
              // I'm not uploading anything inappropriate
              SafetyLevel.Safe,
              // What search level should be used
              photo.Album.IsPublic
                ? HiddenFromSearch.Visible
                : HiddenFromSearch.Hidden);

  // Once the image is uploaded, we need to add it to the a photo set as well.
  addPhotoToPhotoset(photo, flickr);
}

To update an existing image on Flickr I use the following method:

internal static void UpdateImage(DoodlePhoto photo, Flickr flickr) {
  // Update the metadata for an existing image
  flickr.PhotosSetMeta(photo.FlickrId, photo.Caption, photo.Description);
  // Replace all tags for an existing image
  flickr.PhotosSetTags(photo.FlickrId, photo.CollapsedTags);

  // Check to see if we've created this photoset already
  if (string.IsNullOrEmpty(photo.Album.FlickrPhotoSetId)) {
    // Photo set doesn't exist, therefore we can safetly add the image
    addPhotoToPhotoset(photo, flickr);
  } else {
    // Photoset already exists, so we need to check if the image is
    // not already in the photoset, and add it if needed
    if (!photoSetContainsPhoto(photo, flickr)) {
      addPhotoToPhotoset(photo, flickr);
    }
  }
}

To add a photo to a photoset:

private static void addPhotoToPhotoset(DoodlePhoto photo, Flickr flickr) {
  // Check to see if we've already created a photoset
  if (String.IsNullOrEmpty(photo.Album.FlickrPhotoSetId)) {
    // Create a new Photoset, with the Photo's FlickrId as the Primary Photo Id
    var photoset = flickr.PhotosetsCreate(photo.Album.Caption,
                                          photo.Album.Description,
                                          photo.FlickrId);

    // Store the Photoset Id with the Album.
    photo.Album.FlickrPhotoSetId = photoset.PhotosetId;
  } else {
    // Simply add the Photo to the Photoset.
    flickr.PhotosetsAddPhoto(photo.Album.FlickrPhotoSetId, photo.FlickrId);
  }
}

Finally, the code to check if a Photo already exists in a PhotoSet:

private static bool photoSetContainsPhoto(DoodlePhoto photo, Flickr flickr) {
  // Get the details of the photos in a Photoset, will become an issue
  // only when I add more than 500 images to a Photoset!
  var photos = flickr.PhotosetsGetPhotos(photo.Album.FlickrPhotoSetId);
  // Return true if the photo's Id appears in the list.
  return null != photos.SingleOrDefault(p => p.PhotoId == photo.FlickrId);
}

Deleting a photo from Flickr is a trivial exercise, simply requiring a call to PhotosDelete with the Flickr Id of the photo.

All in all, I found this to be very clean, and fairly intutive - especially if you take the time to read the API as you go.

Filed under: .Net, ASP.NET MVC, Site Updates