// vim: ts=2 sw=2 expandtab
//
// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2008
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL

// Developer ID = 1aiV9CsTs3o
// Developer Secret = flock_is_open_source

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;
const CU = Components.utils;

CU.import("resource:///modules/FlockScheduler.jsm");
CU.import("resource:///modules/FlockSvcUtils.jsm");
CU.import("resource:///modules/FlockXPCOMUtils.jsm");
FlockXPCOMUtils.debug = false;
CU.import("resource:///modules/FlockGoogleAuth.jsm");
CU.import("resource://gre/modules/ISO8601DateUtils.jsm");

const YT_DEVID = "1aiV9CsTs3o";
const YT_DEVSECRET = "flock_is_open_source";

const API_CLASS_NAME = "Flock Youtube API";
const API_CLASS_ID = Components.ID("{524f2f40-47a5-11dd-ae16-0800200c9a66}");
const API_CONTRACT_ID = "@flock.com/webservice/api/youtube;1";

const MODULE_NAME = "Youtube";
const CLASS_ID = Components.ID("{DCB6A01E-7D4A-4C30-AB3D-9CC98C02F617}");
const CONTRACT_ID = "@flock.com/?photo-api-youtube;1";
const CLASS_NAME = "YouTube";
const CLASS_SHORT_NAME = "youtube";

const YOUTUBE_FAVICON = "chrome://flock/content/services/youtube/favicon.png";
const SERVICE_ENABLED_PREF = "flock.service.youtube.enabled";
const FRIENDS_AVATAR_TIMEOUT = 5000;  // in milliseconds
const FRIENDS_ACTIVITY_TIMEOUT = 500;  // in milliseconds

const GPHOTO_NAMESPACE = "http://search.yahoo.com/mrss/";
const GDATA_NAMESPACE = "http://schemas.google.com/g/2005";
const YT_NAMESPACE = "http://gdata.youtube.com/schemas/2007";
const OPENSEARCH_NAMESPACE = "http://a9.com/-/spec/opensearchrss/1.0/"
const FAV_CHANNELS_ROOT = "urn:media:favorites";

const FLOCK_RDNDS_CONTRACTID = "@flock.com/rich-dnd-service;1";

const YOUTUBE_IDENTITY_URN_PREFIX = "urn:flock:identity:youtube:";
const PEOPLE_PROPERTIES = "chrome://flock/locale/people/people.properties";
const YOUTUBE_STRING_BUNDLE = "chrome://flock/locale/services/youtube.properties";
const LOGIN_URL = "https://www.google.com/youtube/accounts/ClientLogin";
const YOUTUBE_API_GET_USER_INFO_URL = "http://gdata.youtube.com/feeds/api/users/";

// From nsIXMLHttpRequest.idl
// 4: COMPLETED     Finished with all operations.
const XMLHTTPREQUEST_READYSTATE_COMPLETED = 4;

const HTTP_CODE_OK = 200;

// The delay between two refreshes when the sidebar is closed (in seconds)
const YOUTUBE_REFRESH_INTERVAL = 3600; // 60 minutes
const YAHOO_PHOTO_MAX_RESULT = 25;

var gYoutubeAPI = null;

function _getIdentityUrn(aAccount, aUid) {
  var result = YOUTUBE_IDENTITY_URN_PREFIX
             + aAccount.getParam("name") + ":"
             + aUid;
  return result;
}

function loadLibraryFromSpec(aSpec) {
  var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
                         .getService(Components.interfaces.mozIJSSubScriptLoader);
  loader.loadSubScript(aSpec);
}

// String defaults... may be updated later through Web Detective
var gStrings = {
  "domains": "youtube.com",
  "userprofile": "http://www.youtube.com/profile?user=%accountid%",
  "editprofile": "http://youtube.com/my_profile",
  "inbox": "http://youtube.com/my_messages"
};

var flockMediaItemFormatter = {
  canBuildTooltip: true,
  canBuildHTML: true,
  canBuildLargeHTML: true,
  canBuildBBCode: true,
  canBuildMiniPage: true,

  buildTooltip: function fmif_buildTooltip(aMediaItem) {

    default xml namespace =
      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

    var extras = <vbox anonid="ttUserTxt" />;
    if (aMediaItem.length_seconds) {
      extras.appendChild(<label>Length: {aMediaItem.length_seconds} seconds</label>);
    }
    if (aMediaItem.rating_avg) {
      extras.appendChild(<label>Rating: {aMediaItem.rating_avg}
                         ({aMediaItem.rating_count} times)</label>);
    }
    if (aMediaItem.view_count) {
      extras.appendChild(<label>Views: {aMediaItem.view_count}</label>);
    }

    extras.appendChild(<label class="user">{aMediaItem.username}</label>);

    var xml =
      <vbox>
        <hbox>
          <image src={aMediaItem.midSizePhoto} style="margin-bottom: 2px;" />
          <spacer flex="1" />
        </hbox>
        <vbox anonid="ttInfoContainer">
          <label anonid="ttTitleTxt">{aMediaItem.title}</label>
          {extras}
        </vbox>
      </vbox>;

    return xml;
  },

  buildHTML: function fmif_buildHTML(aMediaItem) {
    var flashUrl = aMediaItem.webPageUrl.replace(/\/\?v\=/,"/v/");
    var html = '<object width="425" height="350">'
         + '<param name="movie" value="'
         + flashUrl
         + '"/>'
         + '<embed src="'
         + flashUrl
         + '" type="application/x-shockwave-flash" width="425" height="350"/>'
         + aMediaItem.webPageUrl
         + '</object>';
    return html;
  },

  buildLargeHTML: function fmif_buildLargeHTML(aMediaItem) {
    return this.buildHTML(aMediaItem);
  },

  buildBBCode: function fmif_buildBBCode(aMediaItem) {
    var flashUrl = aMediaItem.webPageUrl.replace(/\/\?v\=/,"/watch?v=");
    var bbCode = "[youtube]" + flashUrl + "[/youtube]";
    return bbCode;
  },

  buildMiniPage: function fmif_buildMiniPage(aMediaItem) {
    var flashUrl = aMediaItem.webPageUrl.replace(/\/\?v\=/,"/v/");
    var html = '<html><head><title>'
             + aMediaItem.title
             + ' (' + aMediaItem.username + ')</title></head>'
             + '<body><center><object width="425" height="350">'
             + '<param name="movie" value="'
             + flashUrl
             + '"/>'
             + '<embed src="'
             + flashUrl
             + '&autoplay=1" type="application/x-shockwave-flash"'
             + ' width="425" height="350"/></object></center></body></html>';
    return html;
  }
};


/*************************************************************************
 * Component: YoutubeService
 *************************************************************************/

function YoutubeService() {
  loadLibraryFromSpec("chrome://flock/content/photo/photoAPI.js");
  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getLogger(this);

  if (!gYoutubeAPI) {
    gYoutubeAPI = new YoutubeAPI();
  }
  this.api = gYoutubeAPI;
  this.api.svc = this;
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "isHidden");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "getStringBundle");
  FlockSvcUtils.flockIWebService.addDefaultMethod(this, "url");

  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "loginURL");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAuthenticatedAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccounts");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "removeAccount");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "docRepresentsSuccessfulLogin");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getAccountIDFromDocument");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getCredentialsFromForm");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "ownsDocument");
  FlockSvcUtils.flockILoginWebService
               .addDefaultMethod(this, "getSessionValue");
  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "getChannel");
  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "enumerateChannels");
  FlockSvcUtils.flockIMediaWebService
               .addDefaultMethod(this, "getIconForQuery");

  FlockSvcUtils.flockIRichContentDropHandler
               .addDefaultMethod(this, "_handleTextareaDrop");

  FlockSvcUtils.flockIMediaEmbedWebService
               .addDefaultMethod(this, "getSharingContent");

  this._obs = CC["@mozilla.org/observer-service;1"]
              .getService(CI.nsIObserverService);
  this._obs.addObserver(this, "xpcom-shutdown", false);

  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);
  this._currentQueryExhausted = false;
  this._ppUtils = CC["@flock.com/people-utils;1"]
                  .getService(CI.flockIPeopleUtils);
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

  this._channels = {
    "special:added": {
      title: bundle.GetStringFromName("flock.youtube.title.added"),
      supportsSearch: false,
      feed: "http://youtube.com/rss/global/recently_added.rss"
    },
    "special:featured": {
      title: bundle.GetStringFromName("flock.youtube.title.featured"),
      supportsSearch: false
    },
    "special:top_favorites": {
      title: bundle.GetStringFromName("flock.youtube.title.topfav"),
      supportsSearch: false,
      feed: "http://youtube.com/rss/global/top_favorites.rss"
    },
    "special:rated": {
      title: bundle.GetStringFromName("flock.youtube.title.rated"),
      supportsSearch: false,
      feed: "http://youtube.com/rss/global/top_rated.rss"
    }
  };

  var prefs = CC["@mozilla.org/preferences-service;1"]
              .getService(CI.nsIPrefBranch);
  if (prefs.getPrefType(SERVICE_ENABLED_PREF) &&
     !prefs.getBoolPref(SERVICE_ENABLED_PREF))
  {
    this._logger.debug(SERVICE_ENABLED_PREF + " is FALSE! Not initializing.");
    this.deleteCategories();
    return;
  }

  this._accountClassCtor = YoutubeAccount;

  FlockSvcUtils.getCoop(this);

  this._c_svc = new this._coop.Service(
    "urn:youtube:service",
    {
      name: "youtube",
      desc: "The YouTube Service"
    }
  );
  this._c_svc.serviceId = CONTRACT_ID;

  // Load Web Detective file
  this._WebDetective = this._acUtils.useWebDetective("youtube.xml");
  for (var s in gStrings) {
    gStrings[s] = this._WebDetective.getString("youtube", s, gStrings[s]);
  }
  this._c_svc.domains = gStrings["domains"];

  this.urn = this._c_svc.id();

  
  this._updateFriendActivityComplete = false;
  this._friendActivityArray = [];

  // Initialize member that specifies path for localized string bundle
  this._stringBundlePath = YOUTUBE_STRING_BUNDLE;

  var query = {serviceId: CONTRACT_ID, isAuthenticated: true};
  var authenticatedAccounts = this._coop.Account.find(query);
  if (authenticatedAccounts.length != 0) {
    this.api.setAuthAccount(authenticatedAccounts[0]);
  }
}

YoutubeService.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME,
  CLASS_ID,
  CONTRACT_ID,
  YoutubeService,
  CI.nsIClassInfo.SINGLETON,
  [
    CI.flockILoginWebService,
    CI.flockIMediaEmbedWebService,
    CI.flockIMediaWebService,
    CI.flockIPollingService,
    CI.flockIRichContentDropHandler,
    CI.flockISocialWebService,
    CI.flockIWebService,
    CI.flockIYoutubeService,
    CI.nsIObserver
  ]
);

YoutubeService.prototype._xpcom_categories = [
  { category: "wsm-startup" },
  { category: "flockWebService", entry: CLASS_SHORT_NAME },
  { category: "flockMediaProvider", entry: CLASS_SHORT_NAME },
  { category: "flockRichContentHandler", entry: CLASS_SHORT_NAME }  
];

/**************************************************************************
 * YoutubeService: flockILoginWebService Implementation
 **************************************************************************/

// readonly attribute AString domains;
YoutubeService.prototype.__defineGetter__("domains",
function YoutubeService_getdomains() {
  this._logger.debug("Getting property: domains");
  return this._c_svc.domains;
});

// readonly attribute boolean needPassword;
YoutubeService.prototype.needPassword = true;

/**
 * flockIWebServiceAccount addAccount(in AString aAccountID,
 *                                    in boolean aIsTransient,
 *                                    in flockIListener aFlockListener);
 * @see flockILoginWebService#addAccount
 */
YoutubeService.prototype.addAccount =
function YoutubeService_addAccount(aAccountID, aIsTransient, aFlockListener) {
  this._logger.debug(".addAccount('" + aAccountID + "', "
                                     + aIsTransient + ", ...)");

  var url = gStrings["userprofile"].replace("%accountid%", aAccountID);
  var accountURN = this._acUtils.createAccount(this,
                                               aAccountID,
                                               aAccountID,
                                               url,
                                               aIsTransient);
  var account = this.getAccount(accountURN);

  if (!account.coopObj.friendsList) {
    var friendsListUrn = accountURN + ":friends";
    var friendsList = new this._coop.FriendsList(
      friendsListUrn,
      {
        account: account.coopObj
      }
    );
    account.coopObj.friendsList = friendsList;
  }

  // Instantiate account component
  account._addMediaToFavorites(aAccountID);
  if (aFlockListener) {
    aFlockListener.onSuccess(account, "addAccount");
  }
  return account;
};

YoutubeService.prototype.ownsLoginForm =
function YoutubeService_ownsLoginForm(aForm) {
  this._logger.debug(".ownsLoginForm()");
  aForm.QueryInterface(CI.nsIDOMHTMLFormElement);
  if (this._WebDetective.detectForm(this.shortName,
                                    "login",
                                    aForm,
                                    FlockSvcUtils.newResults()))
  {
    // Did we login using google account login? 
    this.api._googleLogin = this._WebDetective.detectForm(this.shortName,
                                                          "googlelogin",
                                                          aForm,
                                                          null);
    return true;
  }
  if (this._WebDetective.detectForm(this.shortName,
                                    "signup",
                                    aForm,
                                    FlockSvcUtils.newResults()))
  {
    return true;
  }
  if (this._WebDetective.detectForm(this.shortName,
                                    "changepassword",
                                    aForm,
                                    FlockSvcUtils.newResults()))
  {
    return true;
  }
  return false;
};

// DEFAULT: boolean docRepresentsSuccessfulLogin(in nsIDOMHTMLDocument aDocument);
// DEFAULT: AString getAccountIDFromDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: nsISimpleEnumerator getAccounts();
// DEFAULT: flockIWebServiceAccount getAccount(in AString aUrn);
// DEFAULT: flockIWebServiceAccount getAuthenticatedAccount();
// DEFAULT: flockILoginInfo getCredentialsFromForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: AString getSessionValue();

// DEFAULT: boolean ownsDocument(in nsIDOMHTMLDocument aDocument);
// DEFAULT: boolean ownsLoginForm(in nsIDOMHTMLFormElement aForm);
// DEFAULT: void removeAccount(in AString aUrn);

YoutubeService.prototype.logout =
function YoutubeService_logout() {
  this._logger.debug(".logout()");
  this.api.deauthenticate();
  this._acUtils.removeCookies(this._WebDetective.getSessionCookies("youtube"));
}

/**
 * void updateAccountStatusFromDocument(in nsIDOMHTMLDocument aDocument,
 *                                      in AString aAcctURN,
 *                                      in flockIListener aFlockListener);
 * @see flockILoginWebService#updateAccountStatusFromDocument
 */
YoutubeService.prototype.updateAccountStatusFromDocument =
function YoutubeService_updateAccountStatusFromDocument(aDocument,
                                                        aAcctURN,
                                                        aFlockListener)
{
  this._logger.info(".updateAccountStatusFromDocument('" + aDocument.URL + "', '"
                                                         + aAcctURN + "', ...)");
  if (aAcctURN) {
    // We're logged in to this account
    var account = this.getAccount(aAcctURN);
    if (!account.isAuthenticated()) {
      account.login(aFlockListener);
    }
  } else if (this._WebDetective
                 .detect("youtube", "loggedout", aDocument, null))
  {
    // We're logged out (of all accounts)
    this._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
    this.logout();
  }
};


/**************************************************************************
 * YoutubeService: flockIMediaEmbedWebService Implementation
 **************************************************************************/

/**
 * boolean checkIsStreamUrl(in AString aUrl);
 * @see flockIMediaEmbedWebService#checkIsStreamUrl
 */
YoutubeService.prototype.checkIsStreamUrl =
function YoutubeService_checkIsStreamUrl(aUrl) {
  if (this._WebDetective.detectNoDOM("youtube", "isStreamUrl", "", aUrl, null)) {
    this._logger.debug("Checking if url is youtube stream: YES: " + aUrl);
    return true;
  }
  this._logger.debug("Checking if url is youtube stream: NO: " + aUrl);
  return false;
};

/**
 * void getMediaQueryFromURL(in AString aUrl, in flockIListener aFlockListener);
 * @see flockIMediaEmbedWebService#getMediaQueryFromURL
 */
YoutubeService.prototype.getMediaQueryFromURL =
function youTubeService_getMediaQueryFromURL(aUrl, aFlockListener) {
  var videoId = this.getVideoIDFromUrl(aUrl);
  if (videoId) {
    var inst = this;
    var myListener = {
      onResult: function getMediaQueryFromURL_onResult(aXML) {
        var userID = aXML.getElementsByTagName('author')[0].firstChild.nodeValue;
        var results = CC["@mozilla.org/hash-property-bag;1"]
                      .createInstance(CI.nsIWritablePropertyBag2);
        results.setPropertyAsAString("query", "user:" + userID + "|username:" + userID);
        // Note: The "title" property is currently not used by the media bar.
        results.setPropertyAsAString("title", userID);
        aFlockListener.onSuccess(results, "query");
      },
      onError: function getMediaQueryFromURL_onError(aFlockError) {
        inst._logger.error(".getMediaQueryFromURL() myListener.onError()");
        aFlockListener.onError(aFlockError, null);
      }
    };
    var params = {};
    params.video_id = videoId;
    this.call(myListener, "youtube.videos.get_details", params);
  } else {
    this._logger.error(".getMediaQueryFromURL() Unable to get user.");
    aFlockListener.onError(null, "Unable to get user.");
  }
};

// DEFAULT: boolean getSharingContent(in nsIDOMHTMLElement aSrc,
//                                    in nsIWritablePropertyBag2 aProp);


/**************************************************************************
 * YoutubeService: flockIMediaWebService Implementation
 **************************************************************************/

// readonly attribute boolean supportsUsers;
YoutubeService.prototype.supportsUsers = true;

// DEFAULT: flockIMediaChannel getChannel(in AString aChannelId);

/**
 * void decorateForMedia(in nsIDOMHTMLDocument aDocument);
 * @see flockIMediaWebService#decorateForMedia
 */
YoutubeService.prototype.decorateForMedia =
function YoutubeService_decorateForMedia(aDocument) {
  aDocument.QueryInterface(Components.interfaces.nsIDOMHTMLDocument);
  this._logger.debug(".decorateForMedia('" + aDocument.URL + "')");
  var results = FlockSvcUtils.newResults();
  if (this._WebDetective.detect("youtube", "media", aDocument, results)) {
    var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
    var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

    var mediaArr = [];
    var userid = results.getPropertyAsAString("userid");

    // media item for user videos
    var vidname = bundle.formatStringFromName("flock.youtube.videosUC",
                                              [userid], 1);
    var media = {
      name: userid,
      query: "user:" + userid + "|username:" + userid,
      label: vidname,
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);

    // media item for user favorites
    var favname = bundle.formatStringFromName("flock.youtube.favoritesUC",
                                              [userid], 1);
    media = {
      name: userid,
      query: "favorites:" + userid,
      label: favname,
      favicon: this.icon,
      service: this.shortName
    };
    mediaArr.push(media);

    if (!aDocument._flock_decorations) {
      aDocument._flock_decorations = {};
    }

    if (aDocument._flock_decorations.mediaArr) {
      aDocument._flock_decorations.mediaArr =
        aDocument._flock_decorations.mediaArr.concat(mediaArr);
    } else {
      aDocument._flock_decorations.mediaArr = mediaArr;
    }

    this._obs.notifyObservers(aDocument, "media", "media:update");
  }
};

// DEFAULT: nsISimpleEnumerator enumerateChannels();

/**
 * void findByUsername(in flockIListener aFlockListener,
 *                     in AString aUsername);
 * @see flockIMediaWebService#findByUsername
 */
YoutubeService.prototype.findByUsername =
function YoutubeService_findByUsername(aFlockListener, aUsername) {
  this._logger.debug("findByUsername ");
  var inst = this;
  var isUserListener = {
    onResult: function isUser_onResult(aXML) {
      var newUserObj = CC["@flock.com/person;1"]
                       .createInstance(CI.flockIPerson);
      newUserObj.serviceId = this.contractID;
      newUserObj.accountId = aUsername;
      newUserObj.name = aUsername;
      newUserObj.totalMedia = -1; // See mediabar.js
      aFlockListener.onSuccess(newUserObj, "");
    },
    onError: function isUser_onError(aFlockError) {
      inst._logger.error(".getMediaQueryFromURL() Unable to get user.");
      aFlockListener.onError(aFlockError, aUsername);
    }
  };
  this.api.doCall(isUserListener,
                  YOUTUBE_API_GET_USER_INFO_URL + aUsername,
                  null);
};

/**
 * void getAlbums(in flockIListener aFlockListener,
 *                in AString aUsername);
 * @see flockIMediaWebService#getAlbums
 */
YoutubeService.prototype.getAlbums =
function YoutubeService_getAlbums(aFlockListener, aUsername) { 
  aFlockListener.onError(null, null);
};

/**
 * flockIMediaItemFormatter getMediaItemFormatter();
 * @see flockIMediaWebService#getMediaItemFormatter
 */
YoutubeService.prototype.getMediaItemFormatter =
function YoutubeService_getMediaItemFormatter() {
  return flockMediaItemFormatter;
};

/**
 * void migrateAccount(in AString aId, in AString aUsername);
 * @see flockIMediaWebService#migrateAccount
 */
YoutubeService.prototype.migrateAccount =
function YoutubeService_migrateAccount(aId, aUsername) {
  this.addAccount(aId, false, null);
};

/**
 * void search(in flockIListener aFlockListener,
 *             in AString aQuery,
 *             in long aCount,
 *             in long aPage,
 *             in AString aRequestId);
 * @see flockIMediaWebService#search
 */
YoutubeService.prototype.search =
function YoutubeService_search(aFlockListener,
                               aQueryString,
                               aCount,
                               aPage,
                               aRequestId)
{
  var aQuery = new queryHelper(aQueryString);
  if (this._currentQueryExhausted) {
    // No need to search as all pages for this query have been returned
    if (aPage == 1) {
      this._currentQueryExhausted = false;
    } else {
      return;
    }
  }
  if (aQuery.favorites && !aQuery.user) {
    aQuery.user = aQuery.favorites;
  }
  if (!aQuery.user && aQuery.special != "favorites") {
    this._queryChannel(aFlockListener, aQueryString, aPage, aRequestId);
    return;
  }

  var aUserid = aQuery.user;
  var params = {};
  params["start-index"] =
    (aPage == 1) ? aPage : (aPage - 1) * YAHOO_PHOTO_MAX_RESULT + 1;
  params["max-results"] = YAHOO_PHOTO_MAX_RESULT;

  var inst = this;
  var myListener = {
    onResult: function search_onResult(aXML) {
      inst._logger.debug(".search() myListener.onResult()");
      var rval = inst._handlePhotosResult(aXML, aUserid);
      if (rval.length < YAHOO_PHOTO_MAX_RESULT) {
        // All results for the current query have been returned 
        inst._currentQueryExhausted = true;
      }

      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length > 0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(CI.nsISimpleEnumerator)) {
            return this;
          }
          throw CR.NS_ERROR_NO_INTERFACE;
        }
      };
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function search_onError(aFlockError) {
      inst._logger.error(".search() myListener.onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };
  var link = "http://gdata.youtube.com/feeds/api/users/"
           + aUserid + (aQuery.favorites ? "/favorites" : "/uploads");
  this.api.doCall(myListener, link, params);
};

/**
 * boolean supportsSearch(in AString aQuery);
 * @see flockIMediaWebService#supportsSearch
 */
YoutubeService.prototype.supportsSearch =
function YoutubeService_supportsSearch(aQueryString) {
  var aQuery = new queryHelper(aQueryString);

  if (aQuery.special) {
    var channel = this._channels["special:" + aQuery.special];
    if (channel) {
      return channel.supportsSearch;
    }
  }

  // None of the other apis/feeds support search
  return false;
};

YoutubeService.prototype._getFeed =
function YoutubeService__getFeed(aFlockListener, aFeedURL, aRequestId) {
  var ios = CC["@mozilla.org/network/io-service;1"].getService(CI.nsIIOService);
  var uri = ios.newURI(aFeedURL, null, null);

  var inst = this;
  var feedListener = {
    onGetFeedComplete: function oncomplete(feed) {
      inst._handleFeedResult(aFlockListener, feed, aRequestId);
    },
    onError: function onError(aFlockError) {
      inst._logger.error("._getFeed() feedListener.onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var fm = Components.classes["@flock.com/feed-manager;1"]
    .getService(Components.interfaces.flockIFeedManager);
  fm.getFeedBypassCache(uri, feedListener, false);
};

YoutubeService.prototype._handleFeedResult =
function YoutubeService__handleFeedResult(aFlockListener, aFeed, aRequestId) {
  var photos = [];
  var nextIdx = 0;

  var items = aFeed.getItems();
  while (items && items.hasMoreElements()) {
    var item = items.getNext();

    var newMediaItem = CC["@flock.com/photo;1"]
                       .createInstance(CI.flockIMediaItem);
    newMediaItem.init(this.shortName, flockMediaItemFormatter);
    newMediaItem.has_miniView = "true";

    newMediaItem.title = item.getTitle();
    newMediaItem.webPageUrl = item.getLink().spec;

    newMediaItem.uploadDate = item.getPubDate() / 1000;
    // Undo the scaling applied in _handlePhotosResult() and ensure that the ID
    // is truly unique so tooltips work properly.
    newMediaItem.id = "" + (newMediaItem.uploadDate / 1000) + nextIdx++;

    var author = item.getAuthor();
    newMediaItem.username = author;
    newMediaItem.userid = author;

    var desc = item.getContent();
    var re = /<img [^>]*src="([^"]*)"/i;
    var thumbnail = desc.match(re)[1];
    newMediaItem.thumbnail = thumbnail;
    newMediaItem.midSizePhoto = thumbnail;
    newMediaItem.largeSizePhoto = thumbnail;

    newMediaItem.is_public = "true";
    newMediaItem.is_video = true;

    photos.push(newMediaItem);
  }

  var resultsEnum = {
    hasMoreElements: function resultsEnum_hasMoreElements() {
      return (photos.length > 0);
    },
    getNext: function resultsEnum_getNext() {
      return photos.shift();
    },
    QueryInterface: function resultsEnum_QueryInterface(aIID) {
      if (aIID.equals(CI.nsISimpleEnumerator)) {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    }
  };
  aFlockListener.onSuccess(resultsEnum, aRequestId);
};

YoutubeService.prototype._handlePhotosResult =
function YoutubeService__handlePhotoResult(aXML, aUserid) {
  var rval = [];
  var nextIdx = 0;
  var photoList = aXML.getElementsByTagName("entry");
  // Extract all photo information from photo XML result and add them to
  // flockMediaItem
  for (var i = 0; i < photoList.length; i++) {
    var photo = photoList[i];
    var newMediaItem = CC["@flock.com/photo;1"]
                       .createInstance(CI.flockIMediaItem);
    newMediaItem.webPageUrl = photo.getElementsByTagName("link")[0]
                                   .getAttribute("href");
    // If this media item is not valid, continue to next item
    if (this._WebDetective.detectNoDOM("youtube",
                                       "mediabarDud",
                                       "",
                                       newMediaItem.webPageUrl,
                                       null))
    {
      continue;
    }
    newMediaItem.init(this.shortName, flockMediaItemFormatter);
    newMediaItem.has_miniView = "true";
    var author = photo.getElementsByTagName("author")[0]
                      .firstChild.firstChild.nodeValue;
    newMediaItem.username = author;
    newMediaItem.userid = author;
    newMediaItem.title = photo.getElementsByTagName("title")[0]
                              .firstChild.nodeValue;
    newMediaItem.description = photo.getElementsByTagName("content")[0]
                                    .firstChild.nodeValue;
    var published = photo.getElementsByTagName("published")[0]
                         .firstChild.nodeValue;
    var date = ISO8601DateUtils.parse(published);
    newMediaItem.uploadDate = date.getTime();
    newMediaItem.id = "" + newMediaItem.uploadDate + nextIdx++;
    var video_id = photo.getElementsByTagName("id")[0]
                        .firstChild.nodeValue;
    video_id = this.getVideoIDFromUrl(video_id);
    newMediaItem.webPageUrl =
    this._WebDetective.getString("youtube", "youtubeMedia_URL", "")
        .replace("%VIDEO_ID%", video_id);
    var mediagroup = photo.getElementsByTagNameNS(GPHOTO_NAMESPACE, "group")[0];
    var duration =
      mediagroup.getElementsByTagNameNS(YT_NAMESPACE, "duration")[0]
                .getAttribute("seconds");
    newMediaItem.length_seconds = duration;
    var thumb =
      mediagroup.getElementsByTagNameNS(GPHOTO_NAMESPACE, "thumbnail")[0]
                .getAttribute("url");
    newMediaItem.thumbnail = thumb;
    newMediaItem.midSizePhoto = thumb;
    newMediaItem.largeSizePhoto = thumb;
    var comments = photo.getElementsByTagNameNS(GDATA_NAMESPACE, "comments")[0];
    newMediaItem.comment_count = comments
                               ? comments.firstChild.getAttribute("countHint")
                               : 0;
    var rating = photo.getElementsByTagNameNS(GDATA_NAMESPACE, "rating")[0];
    newMediaItem.rating_count = rating ? rating.getAttribute("average") : 0;
    var view = photo.getElementsByTagNameNS(YT_NAMESPACE, "statistics")[0];
    newMediaItem.view_count = view ? view.getAttribute("viewCount") : 0;
    newMediaItem.is_public = "true";
    newMediaItem.is_video = true;
    rval.push(newMediaItem);
  }
  return rval;
};

YoutubeService.prototype._queryChannel =
function YoutubeService__queryChannel(aFlockListener,
                                      aQueryString,
                                      aPage,
                                      aRequestId)
{
  var aQuery = new queryHelper(aQueryString);
  // return; // XXX TODO FIXME: This is killing perf

  var inst = this;
  var myListener = {
    onResult: function _queryChannel_onResult(aXML) {
      var rval = inst._handlePhotosResult(aXML);
      var totalPhotosViewed = rval.length * aPage;
      if (rval.length < YAHOO_PHOTO_MAX_RESULT) {
        // All results for the current query have been returned
        inst._currentQueryExhausted = true;
      }

      var resultsEnum = {
        hasMoreElements: function resultsEnum_hasMoreElements() {
          return (rval.length > 0);
        },
        getNext: function resultsEnum_getNext() {
          return rval.shift();
        },
        QueryInterface: function resultsEnum_QueryInterface(aIID) {
          if (aIID.equals(CI.nsISimpleEnumerator)) {
            return this;
          }
          throw CR.NS_ERROR_NO_INTERFACE;
        }
      };
      aFlockListener.onSuccess(resultsEnum, aRequestId);
    },
    onError: function _queryChannel_onError(aFlockError) {
      inst._logger.error("._queryChannel() myListener.onError()");
      aFlockListener.onError(aFlockError, aRequestId);
    }
  };

  var params = {};
  if (aQuery.search) {
    params.vq = aQuery.search;
    params.start_index =
      (aPage == 1) ? aPage : (aPage - 1) * YAHOO_PHOTO_MAX_RESULT + 1;
    params.per_page = YAHOO_PHOTO_MAX_RESULT;
    var link = "http://gdata.youtube.com/feeds/api/videos/";
    this.api.doCall(myListener, link, params);
  } else {
    // This API call doesn't support pagination and only shows most recent 25 items
    if (aPage > 1) {
      return;
    }
    var channel = this._channels[aQuery.stringVal];
    if (!channel) {
      return;
    }
    if (channel.feed) {
      this._getFeed(aFlockListener, channel.feed, aRequestId);
    } else {
      var link ="http://gdata.youtube.com/feeds/api/standardfeeds/"
               + "recently_featured";
      this.api.doCall(myListener, link, params);
    }
  }
};


/**************************************************************************
 * YoutubeService: flockIPollingService Implementation
 **************************************************************************/

/**
 * void refresh(in AString aURN, in flockIPollingListener aPollingListener);
 * @see flockIPollingService#refresh
 */
YoutubeService.prototype.refresh =
function YoutubeService_refresh(aURN, aPollingListener) {
  this._logger.info(".refresh('" + aURN + "', ...)");
  var acct = this.getAccount(aURN).QueryInterface(CI.flockISocialAccount);

  if (acct) {
    this._logger.debug("refreshing an Account");
    if (acct.isAuthenticated()) {
      this._refreshAccount(acct, aPollingListener);
    } else {
      // If the user is not logged in, return a success without
      // refreshing anything
      aPollingListener.onResult();
    }
  } else {
    throw CR.NS_ERROR_ABORT;
  }
};

YoutubeService.prototype._refreshAccount =
function YoutubeService__refreshAccount(aAccount, aPollingListener) {
  var inst = this;
  var pplRefreshListener = CC["@flock.com/people-refresh-listener;1"]
                           .createInstance(CI.flockIPeopleRefreshListener);
  pplRefreshListener.init(3, aAccount.urn, aPollingListener, 0);
  var friendsListener = {
    onSuccess: function friendListener_onSuccess(aFriendsList, aTopic) {
      inst._logger.debug("Friends retrieved successfully");
      // REMOVE locally people removed on the server
      var localFriends = aAccount.enumerateFriends();
      while (localFriends.hasMoreElements()) {
        var identity = localFriends.getNext();
        if (!aFriendsList[identity.accountId]) {
          inst._logger.debug("Friend " + identity.accountId
                             + " has been deleted on the server");
          inst._ppUtils.removePerson(aAccount, identity);
        }
      }
      pplRefreshListener.onSuccess(null, "friendsRetrieved_Listener");
    },
    onError: function friendListener_onError(aFlockError) {
      inst._logger.error("._refreshAccount() friendsListener.onError()");
      pplRefreshListener.onError(aFlockError, "friendsRetrieved_Listener");
    }
  };

  var mailListener = {
    onSuccess: function mailListener_onSuccess(aSubject) {
      inst._logger.debug("mailListener_onSuccess");
      inst._obs.notifyObservers(aAccount.coopObj.resource(),
                                "flock-acct-refresh",
                                "user-info");
      pplRefreshListener.onSuccess(null, "mailListener");
    },
    onError: function mailListener_onError(aFlockError) {
      inst._logger.error("._refreshAccount() mailListener.onError()");
      pplRefreshListener.onError(aFlockError, "mailListener");
    }
  };

  var accountListener = {
    onSuccess: function accountListener_onSuccess(aSubject) {
      inst._logger.debug("infoListener_onSuccess");
      inst.api.updateFriends(aAccount, friendsListener);
      inst.api._getMail(aAccount, mailListener);
      inst._obs.notifyObservers(aAccount.coopObj.resource(),
                                "flock-acct-refresh",
                                "user-info");
      pplRefreshListener.onSuccess(null, "accountListener");
    },
    onError: function accountListener_onError(aFlockError) {
      inst._logger.error("._refreshAccount() accountListener.onError()");
      pplRefreshListener.onError(aFlockError, "accountListener");
    }
  };

  this.api.refreshAccount(aAccount, accountListener);
};

/**************************************************************************
 * YoutubeService: flockIRichContentDropHandler Implementation
 **************************************************************************/

// boolean handleDrop(in nsITransferable aFlavours,
//                    in nsIDOMHTMLElement aTargetElement);
YoutubeService.prototype.handleDrop =
function YoutubeService_handleDrop(aFlavours, aTargetElement) {
  this._logger.info(".handleDrop()");

  // Handle textarea drops
  if (aTargetElement instanceof CI.nsIDOMHTMLTextAreaElement) {
    var dropCallback = function youtube_dropCallback(aFlav) {
      var data = {};
      var len = {};
      aFlavours.getTransferData(aFlav, data, len);
      var caretPos = aTargetElement.selectionEnd;
      var currentValue = aTargetElement.value;

      // Add a trailing space so that we don't mangle the url
      var nextChar = currentValue.charAt(caretPos);
      var trailingSpace = ((nextChar == "") ||
                           (nextChar == " ") || 
                           (nextChar == "\n"))
                        ? ""
                        : " ";

      // We'll only add a breadcrumb if there isn't one already present
      var breadcrumb = "";
      var richDnDSvc = CC[FLOCK_RDNDS_CONTRACTID]
                       .getService(CI.flockIRichDNDService);
      if (!richDnDSvc.hasBreadcrumb(aTargetElement)) {
        breadcrumb = richDnDSvc.getBreadcrumb("plain");
      }

      aTargetElement.value = currentValue.substring(0, caretPos)
                           + data.value.QueryInterface(CI.nsISupportsString)
                                 .data.replace(/: /, ":\n")
                           + trailingSpace
                           + currentValue.substring(caretPos)
                           + breadcrumb;
    };

    return this._handleTextareaDrop(CLASS_SHORT_NAME,
                                    this._c_svc.domains,
                                    aTargetElement,
                                    dropCallback);
  }

  // Default handling otherwise
  return false;
};


/**************************************************************************
 * YoutubeService: nsIObserver Implementation
 **************************************************************************/

/**
 * void observe(nsISupports subject, char* topic, PRUnichar* data);
 * @see nsIObserver#observe
 */
YoutubeService.prototype.observe =
function YoutubeService_observe(aSubject, aTopic, aState) {
  switch (aTopic) {
    case "xpcom-shutdown":
      this._obs.removeObserver(this, "xpcom-shutdown");
      break;
  }
};


/**************************************************************************
 * YoutubeService: API methods
 **************************************************************************/

YoutubeService.prototype.call =
function YoutubeService_call(aListener, aMethod, aParams) {
  var url = "http://www.youtube.com/api2_rest?method=" + aMethod + "&dev_id=" + YT_DEVID;
  for (var p in aParams) {
    url += "&" + p + "=" + aParams[p];
  }
  var hr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(CI.nsIXMLHttpRequest)
           .QueryInterface(CI.nsIJSXMLHttpRequest);

  var inst = this;
  hr.onreadystatechange = function (aEvt) {
    if (hr.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      if (Math.floor(hr.status / 100) == 2) {
        var rsp = hr.responseXML.getElementsByTagName("ut_response")[0];
        var stat = rsp.getAttribute("status");
        if (stat != "ok") {
          var error = inst.getError('SERVICE_ERROR', hr.responseXML, null);
          inst._logger.error(".call(..., '" + aMethod + "', ...) "
                             + "status is NOT OK; status = " + stat);
          aListener.onError(error);
        } else {
          aListener.onResult(hr.responseXML);
        }
      } else {
        // HTTP errors (0 for connection lost)
        inst._logger.error(".call(..., '" + aMethod + "', ...) "
                           + "HTTP Error; status = " + hr.status);
        aListener.onError(inst.getError("HTTP_ERROR", null, hr.status));
      }
    }
  };

  hr.mozBackgroundRequest = true;
  hr.open('GET',  url,true);
  this._logger.debug(".call(..., '" + aMethod + "', ...) url = " + url);
  hr.send(null);
};

YoutubeService.prototype.getError =
function YoutubeService_getError(aErrorType, aXML, aHTTPErrorCode) {
  var errorCode;
  var serviceErrorMessage;
  var error = CC["@flock.com/error;1"].createInstance(CI.flockIError);
  if (aErrorType == "HTTP_ERROR") {
    error.errorCode = aHTTPErrorCode;
  } else if (aErrorType == "SERVICE_ERROR") {
    try {
      errorCode = aXML.getElementsByTagName("error")[0].getAttribute('code');
      serviceErrorMessage = aXML.getElementsByTagName("error")[0].getAttribute('description');
    } catch (ex) {
      errorCode = "999"; // in case the error xml is invalid
    }

    switch (errorCode) { // http://www.youtube.com/dev_error_codes
      case "1":
        error.errorCode = error.HTTP_INTERNAL_SERVER_ERROR;
      break;

      case "2": // These errors are due to Flock sending a bad request
      case "3":
      case "4":
      case "5":
      case "6":
        error.errorCode = error.PHOTOSERVICE_INVALID_QUERY;
      break;

      case "7":
      case "8":
        error.errorCode = error.PHOTOSERVICE_INVALID_API_KEY;
        break;

      case "999":
        error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
      break;

      default:
        error.errorCode = error.PHOTOSERVICE_UNKNOWN_ERROR;
      break;
    }
  }
  error.serviceErrorCode = errorCode;
  error.serviceErrorString = serviceErrorMessage;
  this._logger.error(error.errorString);
  return error;
};


/**************************************************************************
 * YoutubeService: flockISocialWebService Implementation
 **************************************************************************/

// readonly attribute long maxStatusLength;
YoutubeService.prototype.maxStatusLength = 0;

/**
 * void markAllMediaSeen(in AString aPersonUrn);
 * @see flockISocialWebService#markAllMediaSeen
 */
YoutubeService.prototype.markAllMediaSeen =
function YoutubeService_markAllMediaSeen(aIdentityUrn) {
  var identity = this._coop.get(aIdentityUrn);
  identity.unseenMedia = 0;
}; 


/**************************************************************************
 * YoutubeService: flockIWebService Implementation
 **************************************************************************/

// readonly attribute AString contractId;
// ALMOST duplicated by nsIClassInfo::contractID
// with different case: contractId != contractID
YoutubeService.prototype.contractId = CONTRACT_ID;

// readonly attribute AString icon;
YoutubeService.prototype.icon = YOUTUBE_FAVICON;

// readonly attribute AString shortName;
YoutubeService.prototype.shortName = CLASS_SHORT_NAME;

// readonly attribute AString title;
YoutubeService.prototype.title = CLASS_NAME;

// readonly attribute AString urn;
YoutubeService.prototype.urn = "urn:" + CLASS_SHORT_NAME + ":service";


/**************************************************************************
 * YoutubeService: flockIYoutubeService Implementation
 **************************************************************************/

/**
 * AString getVideoIDFromUrl(in AString aUrl);
 * @see flockIYoutubeService#getVideoIDFromUrl
 */
YoutubeService.prototype.getVideoIDFromUrl =
function youTubeService_getVideoIDFromUrl(aUrl) {
  var videoId = null;
  //http://www.youtube.com/v/lp_daXRkOH0
  //http://www.youtube.com/watch/v/YP2rgi978tw
  //http://www.youtube.com/watch?v=BjfbS_Kj-J0
  // /player2.swf?video_id=xPxDw7ajfGE&....
  var results = FlockSvcUtils.newResults();
  if (this._WebDetective.detectNoDOM("youtube",
                                     "videoId",
                                     "",
                                     aUrl,
                                     results))
  {
    videoId = results.getPropertyAsAString("videoId");
  }
  return videoId;
};


/*************************************************************************
 * Component: YoutubeAccount
 *************************************************************************/

function YoutubeAccount() {
  FlockSvcUtils.getLogger(this);
  this._logger.init("YoutubeAccount");

  FlockSvcUtils.getACUtils(this);
  FlockSvcUtils.getCoop(this);
  this.api = gYoutubeAPI;
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);

  var sa = FlockSvcUtils.flockISocialAccount;
  sa.addDefaultMethod(this, "enumerateFriends");
  sa.addDefaultMethod(this, "getInviteFriendsURL");
  sa.addDefaultMethod(this, "formatFriendActivityForDisplay");
  sa.addDefaultMethod(this, "getFormattedFriendUpdateType");
  sa.addDefaultMethod(this, "getFriendCount");

  var wsa = FlockSvcUtils.flockIWebServiceAccount;
  wsa.addDefaultMethod(this, "getAllCustomParams");
  wsa.addDefaultMethod(this, "getCustomParam");
  wsa.addDefaultMethod(this, "getParam");
  wsa.addDefaultMethod(this, "getService");
  wsa.addDefaultMethod(this, "isAuthenticated");
  wsa.addDefaultMethod(this, "logout");
  wsa.addDefaultMethod(this, "setCustomParam");
  wsa.addDefaultMethod(this, "setParam");
}

YoutubeAccount.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " Account",
  "",
  "",
  YoutubeAccount,
  0,
  [
    CI.flockIMediaAccount,
    CI.flockISocialAccount,
    CI.flockIWebServiceAccount,
    CI.flockIYoutubeAccount
  ]
);


/**************************************************************************
 * YoutubeAccount: flockISocialAccount Implementation
 **************************************************************************/

// readonly attribute boolean hasFriendActions;
YoutubeAccount.prototype.hasFriendActions = true;

// readonly attribute boolean isMyMediaFavoritesSupported;
YoutubeAccount.prototype.isMyMediaFavoritesSupported = true;

// readonly attribute boolean isPostLinkSupported;
YoutubeAccount.prototype.isPostLinkSupported = false;

// readonly attribute boolean isMeStatusSupported;
YoutubeAccount.prototype.isMeStatusSupported = false;

// readonly attribute boolean isFriendStatusSupported;
YoutubeAccount.prototype.isFriendStatusSupported = false;

// readonly attribute boolean isStatusEditable;
YoutubeAccount.prototype.isStatusEditable = false;

// DEFAULT: nsISimpleEnumerator enumerateFriends();

/**
 * AString formatStatusForDisplay(in AString aStatusMessage);
 * @see flockISocialAccount#formatStatusForDisplay
 */
YoutubeAccount.prototype.formatStatusForDisplay =
function YoutubeAccount_formatStatusForDisplay(aStatusMessage) {
  return aStatusMessage;
};

/**
 * AString getEditableStatus();
 * @see flockISocialAccount#getEditableStatus
 */
YoutubeAccount.prototype.getEditableStatus =
function YoutubeAccount_getEditableStatus() {
  return "";
};

/**
 * AString getFriendActions(in AString aFriendURN);
 * @see flockISocialAccount#getFriendActions
 */
YoutubeAccount.prototype.getFriendActions =
function YoutubeAccount_getFriendActions(aFriendURN) {
  this._logger.info(".getFriendActions('" + aFriendURN + "')");

  var actionNames = ["friendMessage",
                     "friendViewProfile",
                     "friendMediaFave",
                     "friendShareFlock"];

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

  var actions = [];
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    var c_acct = this._coop.get(this.urn);
    for each (var i in actionNames) {
      actions.push({
        label: bundle.GetStringFromName("flock.youtube.actions." + i),
        class: i,
        spec: this._WebDetective.getString("youtube", i, "")
                  .replace("%accountid%", c_acct.accountId)
                  .replace("%friendid%", c_friend.accountId)
      });
    }
  }
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(actions);
};

// DEFAULT: long getFriendCount();

/**
 * AString getMeNotifications();
 * @see flockISocialAccount#getMeNotifications
 */
YoutubeAccount.prototype.getMeNotifications =
function YoutubeAccount_getMeNotifications() {
  this._logger.info(".getMeNotifications()");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);

  var noties = [];
  var inst = this;
  function _addNotie(aType, aCount) {
    var stringName = "flock.youtube.noties."
                   + aType + "."
                   + ((parseInt(aCount) <= 0) ? "none" : "some");
    inst._logger.debug("aType " + aType
                       + " aCount " + aCount
                       + " name " + stringName);
    noties.push({
      class: aType,
      tooltip: bundle.GetStringFromName(stringName),
      metricsName: aType,
      count: aCount,
      URL: inst._WebDetective.getString("youtube", aType + "_URL", "")
    });
  }
  var c_acct = this._coop.get(this.urn);
  _addNotie("meMessages", c_acct.accountMessages);
  var nsJSON = CC["@mozilla.org/dom/json;1"].createInstance(CI.nsIJSON);
  return nsJSON.encode(noties);
};

/**
 * AString getPostLinkAction(in nsITransferable aTransferable);
 * @see flockISocialAccount#getPostLinkAction
 */
YoutubeAccount.prototype.getPostLinkAction =
function YoutubeAccount_getPostLinkAction(aTransferable) {
  return "";
};

/**
 * AString getProfileURLForFriend(in AString aFriendURN);
 * @see flockISocialAccount#getProfileURLForFriend
 */
YoutubeAccount.prototype.getProfileURLForFriend =
function YoutubeAccount_getProfileURLForFriend(aFriendURN) {
  this._logger.debug(".getProfileURLForFriend('" + aFriendURN + "')");

  var url = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    url = this._WebDetective.getString("youtube", "friendprofile", "")
                            .replace("%accountid%", c_friend.accountId);
  }
  return url;
};

/**
 * AString getSharingAction(in AString aFriendURN,
 *                          in nsITransferable aTransferable);
 * @see flockISocialAccount#getSharingAction
 */
YoutubeAccount.prototype.getSharingAction =
function YoutubeAccount_getSharingAction(aFriendURN, aTransferable) {
  this._logger.info(".getSharingAction('" + aFriendURN + "')");

  var sharingAction = "";
  var c_friend = this._coop.get(aFriendURN);
  if (c_friend) {
    var flavors = ["text/x-flock-media",
                   "text/x-moz-url",
                   "text/unicode",
                   "text/html"];

    var message = CC[FLOCK_RDNDS_CONTRACTID]
                  .getService(CI.flockIRichDNDService)
                  .getMessageFromTransferable(aTransferable,
                                              flavors.length,
                                              flavors);
    if (!message.body) {
      return sharingAction;
    } else {
      var videoId = this.getService()
                        .QueryInterface(CI.flockIYoutubeService)
                        .getVideoIDFromUrl(message.body);
      if (videoId) {
        sharingAction = this._WebDetective.getString("youtube",
                                                     "shareAction_sharevideo",
                                                     "");
      } else {
        sharingAction = this._WebDetective.getString("youtube",
                                                     "shareAction_privatemessage",
                                                     "");
      }
    }
  }
  this._logger.info(sharingAction);
  return sharingAction;
};

/**
 * void markAllMeNotificationsSeen(in AString aType);
 * @see flockISocialAccount#markAllMeNotificationsSeen
 */
YoutubeAccount.prototype.markAllMeNotificationsSeen =
function YoutubeAccount_markAllMeNotificationsSeen(aType) {
  this._logger.debug(".markAllMeNotificationsSeen('" + aType + "')");
  var c_acct = this._coop.get(this.urn);
  switch (aType) {
    case "meMessages":
      c_acct.accountMessages = 0;
      break;
    default:
      break;
  }
};

/**
 * void setStatus(in AString aStatusMessage, in flockIListener aFlockListener);
 * @see flockISocialAccount#setStatus
 */
YoutubeAccount.prototype.setStatus =
function YoutubeAccount_setStatus(aStatusMessage, aFlockListener) {
};


/**************************************************************************
 * YoutubeAccount: flockIWebServiceAccount Implementation
 **************************************************************************/

// readonly attribute AString urn;
YoutubeAccount.prototype.urn = "";

// DEFAULT: nsIPropertyBag getAllCustomParams();
// DEFAULT: nsIVariant getCustomParam(in AString aParamName);
// DEFAULT: nsIVariant getParam(in AString aParamName);
// DEFAULT: flockILoginWebService getService();
// DEFAULT: boolean isAuthenticated();

/**
 * void keep();
 * @see flockIWebServiceAccount#keep
 */
YoutubeAccount.prototype.keep =
function YoutubeAccount_keep() {
  this._logger.debug(".keep()");
  var c_acct = this._coop.get(this.urn);
  c_acct.isTransient = false;
  var key = this.getService().urn + ":" + c_acct.accountId;
  this._acUtils.makeTempPasswordPermanent(key);
};

/**
 * void login(in flockIListener aFlockListener);
 * @see flockIWebServiceAccount#login
 */
YoutubeAccount.prototype.login =
function YoutubeAccount_login(aFlockListener) {
  this._logger.debug(".login(...)");
  this._acUtils.ensureOnlyAuthenticatedAccount(this.urn);
  this.api.setAuthAccount(this.coopObj);
  // Force refresh on login
  var pollerSvc = CC["@flock.com/poller-service;1"]
                  .getService(CI.flockIPollerService);
  pollerSvc.forceRefresh(this.urn);
  if (aFlockListener) {
    aFlockListener.onSuccess(this, "login");
  }
};

// DEFAULT: void logout(in flockIListener aFlockListener);
// DEFAULT: void setCustomParam(in AString aParamName, in nsIVariant aValue);
// DEFAULT: void setParam(in AString aParamName, in nsIVariant aValue);


/**************************************************************************
 * YoutubeAccount: flockIYoutubeAccount Implementation
 **************************************************************************/

/**
 * void shareFlock(in AString aFriendURN);
 * @see flockIYoutubeAccount#shareFlock
 */
YoutubeAccount.prototype.shareFlock =
function YoutubeAccount_shareFlock(aFriendURN) {
  this._logger.info(".shareFlock('" + aFriendURN + "')");
  
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
              .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);
  var body = bundle.GetStringFromName("flock.youtube.friendShareFlock.message");
  var subj = bundle.GetStringFromName("flock.youtube.friendShareFlock.subject");
  this._composeMessage(aFriendURN, subj, body, false);
};

/**
 * void youtubePrivateMessage(in AString aFriendURN,
 *                            in nsITransferable aTransferable);
 * @see flockIYoutubeAccount#youtubePrivateMessage
 */
YoutubeAccount.prototype.youtubePrivateMessage =
function YoutubeAccount_youtubePrivateMessage(aFriendURN, aTransferable) {
  this._logger.info(".youtubePrivateMessage('" + aFriendURN + "', ...)");

  var flavors = ["text/x-flock-media",
                 "text/x-moz-url",
                 "text/unicode",
                 "text/html"];

  var message = CC[FLOCK_RDNDS_CONTRACTID]
                .getService(CI.flockIRichDNDService)
                .getMessageFromTransferable(aTransferable,
                                            flavors.length,
                                            flavors);
  if (message.body) {
    this._composeMessage(aFriendURN, message.subject, message.body, true);
  }
};

/**
 * void youtubeShareVideo(in AString aFriendURN,
 *                        in nsITransferable aTransferable);
 * @see flockIYoutubeAccount#youtubeShareVideo
 */
YoutubeAccount.prototype.youtubeShareVideo =
function YoutubeAccount_youtubeShareVideo(aFriendURN, aTransferable) {
  this._logger.info(".youtubeShareVideo('" + aFriendURN + "', ...)");

  var flavors = ["text/x-flock-media",
                 "text/x-moz-url",
                 "text/unicode",
                 "text/html"];

  var message = CC[FLOCK_RDNDS_CONTRACTID]
                .getService(CI.flockIRichDNDService)
                .getMessageFromTransferable(aTransferable,
                                            flavors.length,
                                            flavors);
  if (message.body) {
    var inst = this;
    var share_url = inst._WebDetective.getString("youtube",
                                                 "shareIframeURL", "");
    var obs = CC["@mozilla.org/observer-service;1"]
                .getService(CI.nsIObserverService);
    var videoId = this.getService()
                      .QueryInterface(CI.flockIYoutubeService)
                      .getVideoIDFromUrl(message.body);
    var url = this._WebDetective.getString("youtube", "youtubeShare_URL", "")
                  .replace("%videoid%", videoId);
    var wm = CC["@mozilla.org/appshell/window-mediator;1"]
             .getService(CI.nsIWindowMediator);
    var win = wm.getMostRecentWindow("navigator:browser");
    var dimen = inst._WebDetective.getString("youtube",
                                             "shareAction_sharevideo_features",
                                             "");
    win.open(url, "Share", dimen);
    win = wm.getMostRecentWindow("navigator:browser");
    var flockDocumentReadyObserver = {
      observe: function openShareVideo_FormFill(aSubject, aTopic, aData) {
        if (aData == share_url) {
          obs.removeObserver(this, "FlockDocumentReady");
          var contentWindow = win.gBrowser.docShell
                                 .QueryInterface(CI.nsIInterfaceRequestor)
                                 .getInterface(CI.nsIDOMWindow);

          function insertContent(aDoc, aXpathquery, aAction, aMessage) {
            var formItems = aDoc.evaluate(aXpathquery, aDoc, null,
                                          CI.nsIDOMXPathResult.ANY_TYPE, null);
            if (formItems) {
              var formItem = formItems.iterateNext();
              if (!formItem) {
                return;
              }
              switch (aAction) {
                case "click":
                  formItem.click();
                  break;

                case "username":
                  if (formItem.hasAttribute("value")) {
                    formItem.setAttribute("value", aMessage);
                  }
                  break;

                case "message":
                  var textNode = doc.createTextNode(aMessage);
                  formItem.appendChild(textNode);
                  break;
              
                default:
                  inst._logger.debug("no action associated with " + aAction);
              }
            }
          }
          var doc = contentWindow.document;
          // Insert Flock bread crumb
          var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                             .getService(CI.flockIRichDNDService)
                             .getBreadcrumb("plain");
          var webDicName = "";
          var xpathquery = "";
          if (breadcrumb) {
            webDicName = "youtubeshareVideo_message";
            xpathquery = inst._WebDetective.getString("youtube",
                                                      webDicName, "");
            insertContent(doc, xpathquery, "message", breadcrumb);
          }

          // Insert friend trying to save to into iframe
          webDicName = "youtubeshareVideo_IframeXPath";
          xpathquery = inst._WebDetective.getString("youtube",
                                                   webDicName, "");
          var formItems = doc.evaluate(xpathquery, doc, null,
                                       CI.nsIDOMXPathResult.ANY_TYPE, null);
          if (formItems) {
            var iframe = formItems.iterateNext();
            if (iframe) {
              var c_friend = inst._coop.get(aFriendURN);
              webDicName = "youtubeshareVideo_friendCheckXPath";
              xpathquery = inst._WebDetective.getString("youtube", webDicName, "")
                                             .replace("%friendname%", c_friend.name);
              insertContent(iframe.contentDocument, xpathquery, "click");
            }
          }
        }
      }
    };
    obs.addObserver(flockDocumentReadyObserver, 'FlockDocumentReady', false);
  }
};

// account helper
YoutubeAccount.prototype._addMediaToFavorites =
function YoutubeAccount__addMediaToFavorites(aAccountId) {
  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  var bundle = sbs.createBundle(YOUTUBE_STRING_BUNDLE);
  
  var svc = this.getService();

  var id = FAV_CHANNELS_ROOT + ":" + svc.shortName
         + ":favorites:" + aAccountId;
  var name = bundle.formatStringFromName("flock.youtube.favoritesLC",
                                         [aAccountId], 1);
  var item = new this._coop.MediaQuery(id, {
    name: name,
    query: "favorites:" + aAccountId,
    service: svc.shortName,
    favicon: svc.icon,
    isPollable: true,
    hasUnseenItems: false
  });

  this._coop.media_favorites.children.addOnce(item);
};

YoutubeAccount.prototype._composeMessage =
function YoutubeAccount__composeMessage(aFriendURN,
                                        aSubject,
                                        aBody,
                                        addBreadCrumb)
{
  var body = aBody;
  var subject = aSubject;
  var c_friend = this._coop.get(aFriendURN);
  var url = this._WebDetective.getString("youtube", "youtubeMessage_URL", "")
                .replace("%friendid%", c_friend.accountId);
  var wm = CC["@mozilla.org/appshell/window-mediator;1"]
             .getService(CI.nsIWindowMediator);
  var win = wm.getMostRecentWindow("navigator:browser");
  if (win) {
   var browser = win.getBrowser();
   var newTab = browser.loadOneTab(url, null, null, null, false, false);
   var obs = CC["@mozilla.org/observer-service;1"]
             .getService(CI.nsIObserverService);
   var inst = this;
   var observer = {
     observe: function openSendMessageTabForFill_observer(aContent,
                                                          aTopic,
                                                          aContextUrl)
     {
       var contentWindow = newTab.linkedBrowser.docShell
                                 .QueryInterface(CI.nsIInterfaceRequestor)
                                 .getInterface(CI.nsIDOMWindow);
       function insertContent(aWebDicString, aMessage) {
         var xpathquery = inst._WebDetective.getString("youtube", aWebDicString, "");
         var doc = contentWindow.document;
         var formItems = doc.evaluate(xpathquery, doc, null,
                                      CI.nsIDOMXPathResult.ANY_TYPE, null);
         if (formItems) {
           var formItem = formItems.iterateNext();
           formItem.value = aMessage;
         }
       }
       if (contentWindow == aContent) {
         obs.removeObserver(this, "EndDocumentLoad");
         insertContent("youtubemessage_subjectXPath", subject);
         if (addBreadCrumb) {
           // Add breadcrumb to message body
            var breadcrumb = CC[FLOCK_RDNDS_CONTRACTID]
                               .getService(CI.flockIRichDNDService)
                               .getBreadcrumb("plain");
           if (breadcrumb) {
             body += breadcrumb;
           }
        }
         insertContent("youtubemessage_bodyXPath", body);
       }
     }
   };
   obs.addObserver(observer, "EndDocumentLoad", false);
  }
};

/*************************************************************************
 * Component: YoutubeAPI
 *************************************************************************/

function YoutubeAPI() {
  this._acUtils = CC["@flock.com/account-utils;1"]
                  .getService(CI.flockIAccountUtils);
  this._WebDetective = CC["@flock.com/web-detective;1"]
                       .getService(CI.flockIWebDetective);
  this._logger = CC["@flock.com/logger;1"].createInstance(CI.flockILogger);
  this._logger.init("youtubeAPI");

  var sbs = CC["@mozilla.org/intl/stringbundle;1"]
            .getService(CI.nsIStringBundleService);
  this._WebDetective = this._acUtils.useWebDetective("youtube.xml");
  this._ppSvc = CC["@flock.com/people-service;1"]
                .getService(CI.flockIPeopleService);
  this._ppUtils = CC["@flock.com/people-utils;1"]
                  .getService(CI.flockIPeopleUtils);
  this._authKey = null;
  this._authAccount = null;
  this._flockGoogleAuth = new flockGoogleAuth("youtube");
};

YoutubeAPI.prototype = new FlockXPCOMUtils.genericComponent(
  CLASS_NAME + " API",
  "",
  "",
  YoutubeAPI,
  0,
  [CI.flockIAuthenticatedAPI]
);

/*************************************************************************
 * YoutubeAPI: flockIAuthenticatedAPI Implementation
 *************************************************************************/

YoutubeAPI.prototype.setAuthAccount =
function YoutubeAPI_setAuthAccount(aCoopObj) {
  this._authAccount = aCoopObj;
};

/**
 * void deauthenticate();
 * @see flockIAuthenticatedAPI#deauthenticate
 */
YoutubeAPI.prototype.deauthenticate =
function YoutubeAPI_deauthenticate() {
  this._logger.debug("deauthenticate()");
  this._authKey = null;
  this._googleLogin = false;
  this._authAccount = null;
};

/**
 * void authenticate(in nsILoginInfo aCredentials,
 *                   in flockIListener aFlockListener);
 * @see flockIAuthenticatedAPI#authenticate
 */
YoutubeAPI.prototype.authenticate =
function YoutubeAPI_authenticate(aCredentials, aFlockListener) {
  var api = this;
  this._logger.debug("authenticate(" + aCredentials.username + ")");
  var dcListener = {
    onSuccess: function dcl_onSuccess(aAuthKey) {
      api._logger.debug("dcl_onSuccess() - " + aAuthKey);
      if (aAuthKey) {
        api._logger.debug("setting authKey = " + aAuthKey);
        api._authKey = aAuthKey;
        if (aFlockListener) {
          aFlockListener.onSuccess(null, null);
        }
      } else {
        if (aFlockListener) {
          api._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
          aFlockListener.onError(null, null);
        }
      }
    },
    onError: function dcl_onError(aFlockError) {
      api._acUtils.markAllAccountsAsLoggedOut(CONTRACT_ID);
      api._logger.error(".authenticate() dcListener.onError()");
      if (aFlockListener) {
        aFlockListener.onError(aFlockError, null);
      }
    }
  };
  var loginUrl = this._googleLogin ? null : LOGIN_URL;
  this._flockGoogleAuth.authenticate(dcListener, aCredentials, loginUrl);
};

YoutubeAPI.prototype.doCall =
function YoutubeAPI_doCall(aListener, aUrl, aParams) {
  this._logger.debug("doCall(" + aUrl + ")");
  this._logger.debug("_authKey = " + this._authKey);
  if (!this._authKey && this._authAccount) {
    // We have an account but no auth key yet so go get one.
    this._logger.debug("We need to re-authorize");
    this._authNeeded(aListener, "doCall", aListener, aUrl, aParams);
    return;
  }
  this._flockGoogleAuth.doCall(this._authKey, aListener, aUrl, aParams);
};

YoutubeAPI.prototype._doXMLCall =
function YoutubeAPI__doXMLCall(aListener, aUrl, aParams, aXml, aMethod) {
  this._logger.debug("_doXMLCall()");
  if (!this._authKey && this._authAccount) {
    // We have an account but no auth key yet so go get one.
    this._authNeeded(aUploadListener,
                     "_doXMLCall",
                     aUrl,
                     aParams,
                     aXml,
                     aMethod);
    return;
  }
  this._flockGoogleAuth.doXMLCall(this._authKey,
                                  aListener,
                                  aUrl,
                                  aParams,
                                  aXml,
                                  aMethod);
};

YoutubeAPI.prototype._authNeeded =
function YoutubeAPI__authNeeded(aListener, aMethod, aP1, aP2, aP3, aP4, aP5) {
  var api = this;
  var listener = {
    onSuccess: function doCall_onSucess(aSubject, aTopic) {
      api[aMethod](aP1, aP2, aP3, aP4, aP5);
    },
    onError: function doCall_onError(aError) {
      api._logger.error("._authNeeded() listener.onError()");
      aListener.onError(aError);
    }
  }
  var paramList = this._authAccount.accountParamList;
  // authAccountId is supposed to be set to loginName and username to accountId
  var credentials = {
    authAccountId: paramList.getParam("loginName").value,
    username: this._authAccount.accountId,
    service: "youtube",
    urn: this.svc.urn
  };
  this.authenticate(credentials, listener);
};

YoutubeAPI.prototype.refreshAccount =
function YoutubeAPI_refreshAccount(aAccount, aListener) {
  this._logger.debug("refreshAccount ");
  var inst = this;
  var infoListener = {
    onResult: function refreshAccount_onSuccess(aSubject, aTopic) {
      inst._logger.debug("Account info retrieved successfully");
      var entry = aSubject.getElementsByTagName("entry")[0];
      if (entry) {
        var nameXML = entry.getElementsByTagName("name");
        if (nameXML && nameXML[0]) {
          aAccount.setParam("name", nameXML[0].firstChild.nodeValue);
        }

        var thumbnailXML = entry.getElementsByTagNameNS(GPHOTO_NAMESPACE,
                                                        "thumbnail");
        if (thumbnailXML && thumbnailXML[0]) {
          aAccount.setParam("avatar", thumbnailXML[0].getAttribute("url"));
        }
      }
      aListener.onSuccess(null, "refreshAccount");
    },
    onError: function refreshAccount_onError(aFlockError) {
      inst._logger.error(".refreshAccount() infoListener.onError()");
      aListener.onError(aFlockError, "refreshAccount");
    }
  };
  this.doCall(infoListener,
              "http://gdata.youtube.com/feeds/api/users/"
                + aAccount.getParam("accountId"),
              null);
};

YoutubeAPI.prototype._updatePerson =
function YoutubeAPI__updatePerson(aPersonXML, aAccount) {
  this._logger.debug("_updatePerson");
  var name = aPersonXML.getElementsByTagName("name")[0]
                       .firstChild.nodeValue;
  var person = this._getPerson(aAccount, name);
  if (!person) {
    var person = CC["@flock.com/person;1"].createInstance(CI.flockIPerson);
    person.name = name;
    person.accountId = person.name;
    person.screenName = person.name;
  }
  
  var thumbnailXML = aPersonXML.getElementsByTagNameNS(GPHOTO_NAMESPACE,
                                                       "thumbnail");
  if (thumbnailXML && thumbnailXML[0]) {
    person.avatar = thumbnailXML[0].getAttribute("url");
  }

  person.profileURL = aPersonXML.getElementsByTagName("link")[0]
                                .getAttribute("href");
  person.lastUpdate = aPersonXML.getElementsByTagName("updated")[0]
                                .firstChild.nodeValue;
  return person;
};

YoutubeAPI.prototype.updateFriends =
function YoutubeAPI_updateFriends(aAcct, aListener) {
  this._logger.debug("updateFriends");
  var inst = this;
  var getFriendsListener = {
    onResult: function getFriendsListener_onResult(aXml) {
      var friends = aXml.getElementsByTagName("entry");
      var friendsToProcess = [];
      // We have retrieved our list of friends, but some friends have not been
      // confirmed yet so we need to build a new list of *real* friends.
      for (var f = 0; f < friends.length; f++) {
        var friendStatus = friends[f]
                           .getElementsByTagNameNS(YT_NAMESPACE,"status")[0]
                           .firstChild.nodeValue;
        if (friendStatus == "requested") {
          continue;
        }
        friendsToProcess.push({name: friends[f]
                                     .getElementsByTagName("title")[0]
                                     .firstChild.nodeValue,
                               status: friendStatus,
                               apiLink: friends[f]
                                        .getElementsByTagName("link")[0]
                                        .getAttribute("href")});
      }

      // We now have a list of friends for which we must now retrieve detailed
      // friend information (such as avatar) and media information.
      var currentFriendsList = [];
      var friendsProcessedCount = 0;
      for (var f = 0; f < friendsToProcess.length; f++) {
        currentFriendsList[friendsToProcess[f].name] = friendsToProcess[f].name;
        var infoListener = {
          onResult: function refreshAccount_onSuccess(aXml) {
            inst._logger.debug("Successfully retrieved detailed friend info.");
            friendsProcessedCount++;

            var xml = aXml.getElementsByTagName("entry")[0];
            // Create a flockIPerson from the data.
            var person = inst._updatePerson(xml, aAcct);
            var serializer = CC["@mozilla.org/xmlextras/xmlserializer;1"]
                             .createInstance(CI.nsIDOMSerializer);
            var xmlStr = serializer.serializeToString(aXml.documentElement);
            var xmlData = new XML(xmlStr);
            var gd = new Namespace(GDATA_NAMESPACE);
            var usrUpload = inst._WebDetective
                                .getString("youtube",
                                           "youtubeUserUploadNamespace",
                                           "");
            var feedlink = xmlData.gd::feedLink
                                  .(@rel == YT_NAMESPACE + usrUpload);
            var totalMedia = feedlink.@countHint;

            if (parseInt(totalMedia, 10) > person.totalMedia) {
              // Get the friend's media.
              var url = feedlink.@href;
              var mediaListener = {
                onSuccess: function m_onSuccess() {
                  inst._logger.debug("Successfully updated media.");
                  person.totalMedia = totalMedia;
                  person.unseenMedia++;
                  inst._ppUtils.addPerson(aAcct, person);
                },
                onError: function m_onError(aFlockError) {
                  // Error occurred while grabbing new media
                  // we still need to add this friend
                  inst._logger.error(".updateFriends() mediaListener.onError()");
                  inst._ppUtils.addPerson(aAcct, person);
                }
              }
              inst._updateFriendMedia(person, url, mediaListener);
            } else {
              // We will not be getting media for this friend so add to
              // datastore now.
              inst._ppUtils.addPerson(aAcct, person);
            }

            // Wait till we have got all our friend data before giving the okay.
            if (friendsProcessedCount >= friendsToProcess.length) {
              inst._logger.debug("Completed retrieving detailed info for all friends");
              aListener.onSuccess(currentFriendsList, "updateFriends");
            }
          },
          onError: function refreshAccount_onError(aFlockError) {
            inst._logger.error(".updateFriends() infoListener.onError()");
            friendsProcessedCount++;
            // Wait till we have got all our friend data before giving the okay.
            if (friendsProcessedCount >= friendsToProcess.length) {
              inst._logger.debug("Completed retrieving detailed info for all friends");
              aListener.onSuccess(currentFriendsList, "updateFriends");
            }
          }
        };
        inst.doCall(infoListener, friendsToProcess[f].apiLink, null);
      }
    },
    onError: function getFriendsListener_onError(aXML) {
      inst._logger.error(".updateFriends() getFriendsListener.onError()");
      aListener.onError(null, "updateFriends");
    }
  };
  this.doCall(getFriendsListener,
              "http://gdata.youtube.com/feeds/api/users/default/contacts",
              null);
};

YoutubeAPI.prototype._updateFriendMedia =
function YoutubeAPI__updateFriendMedia(aPerson, aMediaUrl, aMediaListener) {
  var inst = this;
  var mediaListener = {
    onResult: function mediaListener_onSuccess(aXml) {
      var entry = aXml.getElementsByTagName("entry")[0];
      var updated = entry.getElementsByTagName("updated")[0]
                         .firstChild.nodeValue;
      var date = ISO8601DateUtils.parse(updated);
      aPerson.lastUpdate = date.getTime()/ 1000;
      aPerson.lastUpdateType = "media";
      aMediaListener.onSuccess();
    },
    onError: function mediaListener_onError(aFlockError) {
      inst._logger.error("._updateFriendMedia() mediaListener.onError()");
      aMediaListener.onError(aFlockError);
    }
  };
  this.doCall(mediaListener,
              aMediaUrl,
              null);
};

YoutubeAPI.prototype._getPerson =
function YoutubeAPI__getPerson(aAccount, aPersonName) {
  this._logger.debug("_getPerson('" + aAccount.urn + "', '"
                     + aPersonName + "'");
  var personUrn = _getIdentityUrn(aAccount, aPersonName);
  var person = null;
  if (personUrn) {
    person = this._ppUtils.getPerson(personUrn);
  }
  return person;
};

YoutubeAPI.prototype._getMail =
function YoutubeAPI__getMail(aAcct, aListener) {
  this._logger.debug("_getMail " + aAcct.getParam("accountId"));
  // Get new message count
  var inst = this;
  var hr = CC["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(CI.nsIXMLHttpRequest);
  hr.mozBackgroundRequest = true
  hr.onreadystatechange = function updateMail_onreadystatechange(aEvent) {
    if (hr.readyState == XMLHTTPREQUEST_READYSTATE_COMPLETED) {
      var results = FlockSvcUtils.newResults();
      if (inst._WebDetective.detectNoDOM("youtube",
                                         "messages",
                                         "",
                                         hr.responseText,
                                         results))
      {
        // Set the message count
        var messages = "0";
        try {
          messages = results.getPropertyAsAString("messagecount");
          if (!messages || !messages.length) {
            messages = "0";
          }
        } catch (ex) {
          inst._logger.error("._getMail() aListener.onError(): " + ex.message);
          aListener.onError(null, "_getMail");
        }
        if (aAcct.getParam("accountMessages") < messages) {
          inst._ppSvc.togglePeopleIcon(true);
        }
        aAcct.setParam("accountMessages", messages);
        aListener.onSuccess(null, "_getMail");
      }
    }
  }
  var url = this._WebDetective.getString(CLASS_SHORT_NAME, "userprofile", "")
                .replace("%accountid%", aAcct.getParam("accountId"));
  hr.open("GET", url);
  hr.send(null);
};

/*************************************************************************
 * XPCOM Support - Module Construction
 *************************************************************************/

// Create array of components.
var componentsArray = [YoutubeService];

// Generate a module for XPCOM to find.
var NSGetModule = FlockXPCOMUtils.generateNSGetModule(MODULE_NAME,
                                                      componentsArray);
