conduit r1511 - in trunk: . conduit/datatypes conduit/modules/GoogleModule conduit/modules/GoogleModule/atom conduit/modules/GoogleModule/gdata conduit/modules/GoogleModule/gdata/apps conduit/modules/GoogleModule/gdata/blogger conduit/modules/GoogleModule/gdata/calendar conduit/modules/GoogleModule/gdata/media conduit/modules/GoogleModule/gdata/youtube



Author: jstowers
Date: Sat Jun  7 07:56:04 2008
New Revision: 1511
URL: http://svn.gnome.org/viewvc/conduit?rev=1511&view=rev

Log:
Merge youtube video upload branch

Added:
   trunk/conduit/modules/GoogleModule/atom/mock_service.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/gdata/blogger/
   trunk/conduit/modules/GoogleModule/gdata/blogger/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/blogger/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/blogger/service.py
   trunk/conduit/modules/GoogleModule/gdata/youtube/
   trunk/conduit/modules/GoogleModule/gdata/youtube/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/youtube/__init__.py   (contents, props changed)
   trunk/conduit/modules/GoogleModule/gdata/youtube/service.py
Modified:
   trunk/   (props changed)
   trunk/ChangeLog
   trunk/NEWS
   trunk/conduit/datatypes/Video.py
   trunk/conduit/modules/GoogleModule/GoogleModule.py
   trunk/conduit/modules/GoogleModule/atom/Makefile.am
   trunk/conduit/modules/GoogleModule/atom/__init__.py
   trunk/conduit/modules/GoogleModule/atom/service.py
   trunk/conduit/modules/GoogleModule/gdata/Makefile.am
   trunk/conduit/modules/GoogleModule/gdata/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/apps/service.py
   trunk/conduit/modules/GoogleModule/gdata/auth.py
   trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/calendar/service.py
   trunk/conduit/modules/GoogleModule/gdata/media/__init__.py
   trunk/conduit/modules/GoogleModule/gdata/service.py
   trunk/conduit/modules/GoogleModule/gdata/test_data.py
   trunk/conduit/modules/GoogleModule/gdata/urlfetch.py
   trunk/conduit/modules/GoogleModule/youtube-config.glade
   trunk/configure.ac

Modified: trunk/NEWS
==============================================================================
--- trunk/NEWS	(original)
+++ trunk/NEWS	Sat Jun  7 07:56:04 2008
@@ -1,6 +1,7 @@
 NEW in 0.3.12:
 ==============
 * Support google documents upload/sync
+* Support youtube video upload
 
 NEW in 0.3.11.2:
 ==============

Modified: trunk/conduit/datatypes/Video.py
==============================================================================
--- trunk/conduit/datatypes/Video.py	(original)
+++ trunk/conduit/datatypes/Video.py	Sat Jun  7 07:56:04 2008
@@ -29,6 +29,8 @@
 
     def __init__(self, URI, **kwargs):
         File.File.__init__(self, URI, **kwargs)
+        self._title = None
+        self._description = None
 
     def get_video_duration(self):
         return None
@@ -36,4 +38,22 @@
     def get_video_size(self):
         return None,None
 
+    def get_description(self):
+        """
+        @returns: the video's description
+        """
+        return self._description
+
+    def set_description(self, description):
+        self._description = description
+        
+    def __getstate__(self):
+        data = File.File.__getstate__(self)
+        data["description"] = self._description
+        return data
+
+    def __setstate__(self, data):
+        self.pb = None
+        self._description = data["description"]
+        File.File.__setstate__(self, data)
 

Modified: trunk/conduit/modules/GoogleModule/GoogleModule.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/GoogleModule.py	(original)
+++ trunk/conduit/modules/GoogleModule/GoogleModule.py	Sat Jun  7 07:56:04 2008
@@ -33,11 +33,12 @@
     import gdata.calendar.service
     import gdata.contacts.service
     import gdata.docs.service
+    import gdata.youtube.service
 
     MODULES = {
         "GoogleCalendarTwoWay" : { "type": "dataprovider" },
         "PicasaTwoWay" :         { "type": "dataprovider" },
-        "YouTubeSource" :        { "type": "dataprovider" },    
+        "YouTubeTwoWay" :        { "type": "dataprovider" },    
         "ContactsTwoWay" :       { "type": "dataprovider" },
         "DocumentsSink" :        { "type": "dataprovider" },
     }
@@ -1287,7 +1288,30 @@
             self._set_password(password.get_text())
         dlg.destroy()   
 
-class YouTubeSource(DataProvider.DataSource):
+class VideoUploadInfo:
+    """
+    Upload information container, this way we can add info
+    and keep the _upload_info method on the VideoSink retain
+    its API
+    
+    Default category for videos is People/Blogs (for lack of a better
+    general category).
+    
+    Default name and description are placeholders, or the generated XML is invalid
+    as the corresponding elements automatically self-close.
+    
+    Default keyword is "miscellaneous" as the upload fails if no keywords
+    are specified.
+    """
+    def __init__ (self, url, mimeType, name=None, keywords=None, description=None, category=None):
+        self.url = url
+        self.mimeType = mimeType
+        self.name = name or _("Unknown")
+        self.keywords = keywords or (_("miscellaneous"),)
+        self.description = description or _("No description.")
+        self.category = category or "People" # Note: don't translate this; it's an identifier
+
+class YouTubeTwoWay(_GoogleBase, DataProvider.TwoWay):
     """
     Downloads YouTube videos using the gdata API.
     Based on youtube client from : Philippe Normand (phil at base-art dot net)
@@ -1296,7 +1320,8 @@
     _name_ = _("YouTube")
     _description_ = _("Sync data from YouTube")
     _category_ = conduit.dataproviders.CATEGORY_MISC
-    _module_type_ = "source"
+    _module_type_ = "twoway"
+    _in_type_ = "file/video"
     _out_type_ = "file/video"
     _icon_ = "youtube"
 
@@ -1310,49 +1335,44 @@
     UPLOAD_URL="http://uploads.gdata.youtube.com/feeds/api/users/%(username)s/uploads"
 
     def __init__(self, *args):
-        DataProvider.DataSource.__init__(self)
+        youtube_service = gdata.youtube.service.YouTubeService()
+        youtube_service.client_id = self.UPLOAD_CLIENT_ID
+        youtube_service.developer_key = self.UPLOAD_DEVELOPER_KEY
+
+        _GoogleBase.__init__(self,youtube_service)
+        DataProvider.TwoWay.__init__(self)
+
         self.entries = None
-        self.username = ""
         self.max_downloads = 0
-        #filter type {0 = mostviewed, 1 = toprated, 2 = user}
+        #filter type {0 = mostviewed, 1 = toprated, 2 = user upload, 3 = user favorites}
         self.filter_type = 0
-        #filter user type {0 = upload, 1 = favorites}
-        self.user_filter_type = 0
-
-    def initialize(self):
-        return True
 
     def configure(self, window):
         tree = Utils.dataprovider_glade_get_widget (
                 __file__,
                 "youtube-config.glade",
-                "YouTubeSourceConfigDialog") 
+                "YouTubeTwoWayConfigDialog") 
 
-        dlg = tree.get_widget ("YouTubeSourceConfigDialog")
+        dlg = tree.get_widget ("YouTubeTwoWayConfigDialog")
         mostviewedRb = tree.get_widget("mostviewed")
         topratedRb = tree.get_widget("toprated")
-        byuserRb = tree.get_widget("byuser")
-        user_frame = tree.get_widget("frame")
         uploadedbyRb = tree.get_widget("uploadedby")
         favoritesofRb = tree.get_widget("favoritesof")
-        user = tree.get_widget("user")
-        maxdownloads = tree.get_widget("maxdownloads")
-
-        byuserRb.connect("toggled", self._filter_user_toggled_cb, user_frame)
+        max_downloads = tree.get_widget("maxdownloads")
+        username = tree.get_widget("username")
+        password = tree.get_widget("password")
 
         if self.filter_type == 0:
             mostviewedRb.set_active(True)
         elif self.filter_type == 1:
             topratedRb.set_active(True)
+        elif self.filter_type == 2:
+            uploadedbyRb.set_active(True)
         else:
-            byuserRb.set_active(True)
-            user_frame.set_sensitive(True)
-            if self.user_filter_type == 0:
-                uploadedbyRb.set_active(True)
-            else:
-                favoritesofRb.set_active(True)
-            user.set_text(self.username)
-        maxdownloads.set_value(self.max_downloads)
+            favoritesofRb.set_active(True)
+        max_downloads.set_value(self.max_downloads)
+        username.set_text(self.username)
+        password.set_text(self.password)
 
         response = Utils.run_dialog(dlg, window)
         if response == True:
@@ -1360,32 +1380,58 @@
                 self.filter_type = 0
             elif topratedRb.get_active():
                 self.filter_type = 1
-            else:
+            elif uploadedbyRb.get_active():
                 self.filter_type = 2
-                if uploadedbyRb.get_active():
-                    self.user_filter_type = 0
-                else:
-                    self.user_filter_type = 1
-                self.username = user.get_text()
-            self.max_downloads = int(maxdownloads.get_value())
+            else:
+                self.filter_type = 3
+            self.max_downloads = int(max_downloads.get_value())
+            self.username = username.get_text()
+            self.password = password.get_text()
 
         dlg.destroy()
 
+    def _get_video_info (self, id):
+        if self.entries.has_key(id):
+            return self.entries[id]
+        else:
+            return None
+
+    def _do_login(self):
+        # The YouTube login URL is slightly different to the normal Google one
+        self.service.ClientLogin(self.username, self.password, auth_service_url="https://www.google.com/youtube/accounts/ClientLogin";)
+
+    def _upload_video (self, uploadInfo):
+        try:
+            self.gvideo = gdata.youtube.YouTubeVideoEntry()
+            self.gvideo.media = gdata.media.Group(
+                                title = gdata.media.Title(text=uploadInfo.name),
+                                description = gdata.media.Description(text=uploadInfo.description),
+                                category = gdata.media.Category(text=uploadInfo.category),
+                                keywords = gdata.media.Keywords(text=','.join(uploadInfo.keywords)))
+
+            gvideo = self.service.InsertVideoEntry(
+                                self.gvideo,
+                                uploadInfo.url)
+            return Rid(uid=gvideo.id.text)
+        except Exception, e:
+            raise Exceptions.SyncronizeError("YouTube Upload Error.")
+
+    def _replace_video (self, uploadInfo):
+        raise Exceptions.NotSupportedError("FIXME: Not supported yet")
+
     def refresh(self):
-        DataProvider.DataSource.refresh(self)
+        DataProvider.TwoWay.refresh(self)
 
         self.entries = {}
         try:
-            feedUrl = ""
             if self.filter_type == 0:
                 videos = self._most_viewed ()
             elif self.filter_type == 1:
                 videos = self._top_rated ()
+            elif self.filter_type == 2:
+                videos = self._videos_upload_by (self.username)
             else:
-                if self.user_filter_type == 0:
-                    videos = self._videos_upload_by (self.username)
-                else:
-                    videos = self._favorite_videos (self.username)
+                videos = self._favorite_videos (self.username)
 
             for video in videos:
                 self.entries[video.title.text] = self._get_flv_video_url (video.link[0].href)
@@ -1397,7 +1443,7 @@
         return self.entries.keys()
 
     def get(self, LUID):
-        DataProvider.DataSource.get(self, LUID)
+        DataProvider.TwoWay.get(self, LUID)
         url = self.entries[LUID]
         log.debug("Title: '%s', Url: '%s'"%(LUID, url))
 
@@ -1408,24 +1454,68 @@
 
         return f
 
+    def put(self, video, overwrite, LUID=None):
+        """
+        Based off the ImageTwoWay put method.
+        Accepts a VFS file. Must be made local.
+        I also store a MD5 of the video's URI to check for duplicates
+        """
+        DataProvider.TwoWay.put(self, video, overwrite, LUID)
+
+        self._login()
+
+        originalName = video.get_filename()
+        #Gets the local URI (/foo/bar). If this is a remote file then
+        #it is first transferred to the local filesystem
+        videoURI = video.get_local_uri()
+        mimeType = video.get_mimetype()
+        keywords = video.get_tags ()
+        description = video.get_description()
+
+        uploadInfo = VideoUploadInfo(videoURI, mimeType, originalName, keywords, description)
+
+        #Check if we have already uploaded the video
+        if LUID != None:
+            url = self._get_video_info(LUID)
+            #Check if a video exists at that UID
+            if url != None:
+                if overwrite == True:
+                    #Replace the video
+                    return self._replace_video(LUID, uploadInfo)
+                else:
+                    #Only upload the video if it is newer than the remote one
+                    url = self._get_flv_video_url(url)
+                    remoteFile = File.File(url)
+
+                    #This is a limited test for equality type comparison
+                    comp = video.compare(remoteFile,True)
+                    log.debug("Compared %s with %s to check if they are the same (size). Result = %s" % 
+                            (video.get_filename(),remoteFile.get_filename(),comp))
+                    if comp != conduit.datatypes.COMPARISON_EQUAL:
+                        raise Exceptions.SynchronizeConflictError(comp, video, remoteFile)
+                    else:
+                        return conduit.datatypes.Rid(uid=LUID)
+
+        log.debug("Uploading video URI = %s, Mimetype = %s, Original Name = %s" % (videoURI, mimeType, originalName))
+
+        #Upload the file
+        return self._upload_video (uploadInfo)
+
     def finish(self, aborted, error, conflict):
-        DataProvider.DataSource.finish(self)
+        DataProvider.TwoWay.finish(self)
         self.files = None
 
     def get_configuration(self):
         return {
             "filter_type"       :   self.filter_type,
-            "user_filter_type"  :   self.user_filter_type,
+            "max_downloads"     :   self.max_downloads,
             "username"          :   self.username,
-            "max_downloads"     :   self.max_downloads
+            "password"          :   self.password
         }
 
     def get_UID(self):
         return Utils.get_user_string()
 
-    def _filter_user_toggled_cb (self, toggle, frame):
-        frame.set_sensitive(toggle.get_active())
-
     def _format_url (self, url):
         if self.max_downloads > 0:
             url = ("%s?max-results=%d" % (url, self.max_downloads))
@@ -1436,19 +1526,19 @@
         return service.Get(feed % params)
 
     def _top_rated(self):
-        url = self._format_url ("%s/top_rated" % YouTubeSource.STD_FEEDS)
+        url = self._format_url ("%s/top_rated" % YouTubeTwoWay.STD_FEEDS)
         return self._request(url).entry
 
     def _most_viewed(self):
-        url = self._format_url ("%s/most_viewed" % YouTubeSource.STD_FEEDS)
+        url = self._format_url ("%s/most_viewed" % YouTubeTwoWay.STD_FEEDS)
         return self._request(url).entry
 
     def _videos_upload_by(self, username):
-        url = self._format_url ("%s/%s/uploads" % (YouTubeSource.USERS_FEED, username))
+        url = self._format_url ("%s/%s/uploads" % (YouTubeTwoWay.USERS_FEED, username))
         return self._request(url).entry
 
     def _favorite_videos(self, username):
-        url = self._format_url ("%s/%s/favorites" % (YouTubeSource.USERS_FEED, username))
+        url = self._format_url ("%s/%s/favorites" % (YouTubeTwoWay.USERS_FEED, username))
         return self._request(url).entry
 
     # Generic extract step
@@ -1459,7 +1549,7 @@
         data = doc.read()
 
         # extract video name
-        match = YouTubeSource.VIDEO_NAME_RE.search(data)
+        match = YouTubeTwoWay.VIDEO_NAME_RE.search(data)
         if match is not None:
             video_name = match.group(1)
 

Modified: trunk/conduit/modules/GoogleModule/atom/Makefile.am
==============================================================================
--- trunk/conduit/modules/GoogleModule/atom/Makefile.am	(original)
+++ trunk/conduit/modules/GoogleModule/atom/Makefile.am	Sat Jun  7 07:56:04 2008
@@ -1,5 +1,5 @@
 conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/atom
-conduit_handlers_PYTHON = __init__.py service.py
+conduit_handlers_PYTHON = __init__.py service.py mock_service.py
 
 clean-local:
 	rm -rf *.pyc *.pyo

Modified: trunk/conduit/modules/GoogleModule/atom/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/atom/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/atom/__init__.py	Sat Jun  7 07:56:04 2008
@@ -165,8 +165,8 @@
       child._BecomeChildElement(tree)
     for attribute, value in self.extension_attributes.iteritems():
       if value:
-        # Encode the value in the desired type (default UTF-8).
-        tree.attrib[attribute] = value.encode(MEMBER_STRING_ENCODING)
+        # Decode the value from the desired encoding (default UTF-8).
+        tree.attrib[attribute] = value.decode(MEMBER_STRING_ENCODING)
     if self.text and not isinstance(self.text, unicode):
       tree.text = self.text.decode(MEMBER_STRING_ENCODING)
     else:

Added: trunk/conduit/modules/GoogleModule/atom/mock_service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/atom/mock_service.py	Sat Jun  7 07:56:04 2008
@@ -0,0 +1,243 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""MockService provides CRUD ops. for mocking calls to AtomPub services.
+
+  MockService: Exposes the publicly used methods of AtomService to provide
+      a mock interface which can be used in unit tests.
+"""
+
+import atom.service
+import pickle
+
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+# Recordings contains pairings of HTTP MockRequest objects with MockHttpResponse objects.
+recordings = []
+# If set, the mock service HttpRequest are actually made through this object.
+real_request_handler = None
+
+def ConcealValueWithSha(source):
+  import sha
+  return sha.new(source[:-5]).hexdigest()
+
+def DumpRecordings(conceal_func=ConcealValueWithSha):
+  if conceal_func:
+    for recording_pair in recordings:
+      recording_pair[0].ConcealSecrets(conceal_func)
+  return pickle.dumps(recordings)
+
+def LoadRecordings(recordings_file_or_string):
+  if isinstance(recordings_file_or_string, str):
+    atom.mock_service.recordings =  pickle.loads(recordings_file_or_string)
+  elif hasattr(recordings_file_or_string, 'read'):
+    atom.mock_service.recordings = pickle.loads(
+      recordings_file_or_string.read())
+
+def HttpRequest(service, operation, data, uri, extra_headers=None,
+    url_params=None, escape_params=True, content_type='application/atom+xml'):
+  """Simulates an HTTP call to the server, makes an actual HTTP request if 
+  real_request_handler is set.
+
+  This function operates in two different modes depending on if 
+  real_request_handler is set or not. If real_request_handler is not set,
+  HttpRequest will look in this module's recordings list to find a response
+  which matches the parameters in the function call. If real_request_handler
+  is set, this function will call real_request_handler.HttpRequest, add the
+  response to the recordings list, and respond with the actual response.
+
+  Args:
+    service: atom.AtomService object which contains some of the parameters
+        needed to make the request. The following members are used to
+        construct the HTTP call: server (str), additional_headers (dict),
+        port (int), and ssl (bool).
+    operation: str The HTTP operation to be performed. This is usually one of
+        'GET', 'POST', 'PUT', or 'DELETE'
+    data: ElementTree, filestream, list of parts, or other object which can be
+        converted to a string.
+        Should be set to None when performing a GET or PUT.
+        If data is a file-like object which can be read, this method will read
+        a chunk of 100K bytes at a time and send them.
+        If the data is a list of parts to be sent, each part will be evaluated
+        and sent.
+    uri: The beginning of the URL to which the request should be sent.
+        Examples: '/', '/base/feeds/snippets',
+        '/m8/feeds/contacts/default/base'
+    extra_headers: dict of strings. HTTP headers which should be sent
+        in the request. These headers are in addition to those stored in
+        service.additional_headers.
+    url_params: dict of strings. Key value pairs to be added to the URL as
+        URL parameters. For example {'foo':'bar', 'test':'param'} will
+        become ?foo=bar&test=param.
+    escape_params: bool default True. If true, the keys and values in
+        url_params will be URL escaped when the form is constructed
+        (Special characters converted to %XX form.)
+    content_type: str The MIME type for the data being sent. Defaults to
+        'application/atom+xml', this is only used if data is set.
+  """
+  full_uri = atom.service.BuildUri(uri, url_params, escape_params)
+  (server, port, ssl, uri) = atom.service.ProcessUrl(service, uri)
+  current_request = MockRequest(operation, full_uri, host=server, ssl=ssl, 
+      data=data, extra_headers=extra_headers, url_params=url_params, 
+      escape_params=escape_params, content_type=content_type)
+  # If the request handler is set, we should actually make the request using 
+  # the request handler and record the response to replay later.
+  if real_request_handler:
+    response = real_request_handler.HttpRequest(service, operation, data, uri,
+        extra_headers=extra_headers, url_params=url_params, 
+        escape_params=escape_params, content_type=content_type)
+    # TODO: need to copy the HTTP headers from the real response into the
+    # recorded_response.
+    recorded_response = MockHttpResponse(body=response.read(), 
+        status=response.status, reason=response.reason)
+    # Insert a tuple which maps the request to the response object returned
+    # when making an HTTP call using the real_request_handler.
+    recordings.append((current_request, recorded_response))
+    return recorded_response
+  else:
+    # Look through available recordings to see if one matches the current 
+    # request.
+    for request_response_pair in recordings:
+      if request_response_pair[0].IsMatch(current_request):
+        return request_response_pair[1]
+  return None
+
+
+class MockRequest(object):
+  """Represents a request made to an AtomPub server.
+  
+  These objects are used to determine if a client request matches a recorded
+  HTTP request to determine what the mock server's response will be. 
+  """
+
+  def __init__(self, operation, uri, host=None, ssl=False, port=None, 
+      data=None, extra_headers=None, url_params=None, escape_params=True,
+      content_type='application/atom+xml'):
+    """Constructor for a MockRequest
+    
+    Args:
+      operation: str One of 'GET', 'POST', 'PUT', or 'DELETE' this is the
+          HTTP operation requested on the resource.
+      uri: str The URL describing the resource to be modified or feed to be
+          retrieved. This should include the protocol (http/https) and the host
+          (aka domain). For example, these are some valud full_uris:
+          'http://example.com', 'https://www.google.com/accounts/ClientLogin'
+      host: str (optional) The server name which will be placed at the 
+          beginning of the URL if the uri parameter does not begin with 'http'.
+          Examples include 'example.com', 'www.google.com', 'www.blogger.com'.
+      ssl: boolean (optional) If true, the request URL will begin with https 
+          instead of http.
+      data: ElementTree, filestream, list of parts, or other object which can be
+          converted to a string. (optional)
+          Should be set to None when performing a GET or PUT.
+          If data is a file-like object which can be read, the constructor 
+          will read the entire file into memory. If the data is a list of 
+          parts to be sent, each part will be evaluated and stored.
+      extra_headers: dict (optional) HTTP headers included in the request.
+      url_params: dict (optional) Key value pairs which should be added to 
+          the URL as URL parameters in the request. For example uri='/', 
+          url_parameters={'foo':'1','bar':'2'} could become '/?foo=1&bar=2'.
+      escape_params: boolean (optional) Perform URL escaping on the keys and 
+          values specified in url_params. Defaults to True.
+      content_type: str (optional) Provides the MIME type of the data being 
+          sent.
+    """
+    self.operation = operation
+    self.uri = _ConstructFullUrlBase(uri, host=host, ssl=ssl)
+    self.data = data
+    self.extra_headers = extra_headers
+    self.url_params = url_params or {}
+    self.escape_params = escape_params
+    self.content_type = content_type
+
+  def ConcealSecrets(self, conceal_func):
+    """Conceal secret data in this request."""
+    if self.extra_headers.has_key('Authorization'):
+      self.extra_headers['Authorization'] = conceal_func(
+        self.extra_headers['Authorization'])
+
+  def IsMatch(self, other_request):
+    """Check to see if the other_request is equivalent to this request.
+    
+    Used to determine if a recording matches an incoming request so that a
+    recorded response should be sent to the client.
+
+    The matching is not exact, only the operation and URL are examined 
+    currently.
+
+    Args:
+      other_request: MockRequest The request which we want to check this
+          (self) MockRequest against to see if they are equivalent.
+    """
+    # More accurate matching logic will likely be required.
+    return (self.operation == other_request.operation and self.uri == 
+        other_request.uri)
+
+
+def _ConstructFullUrlBase(uri, host=None, ssl=False):
+  """Puts URL components into the form http(s)://full.host.strinf/uri/path
+  
+  Used to construct a roughly canonical URL so that URLs which begin with 
+  'http://example.com/' can be compared to a uri of '/' when the host is 
+  set to 'example.com'
+
+  If the uri contains 'http://host' already, the host and ssl parameters
+  are ignored.
+
+  Args:
+    uri: str The path component of the URL, examples include '/'
+    host: str (optional) The host name which should prepend the URL. Example:
+        'example.com'
+    ssl: boolean (optional) If true, the returned URL will begin with https
+        instead of http.
+
+  Returns:
+    String which has the form http(s)://example.com/uri/string/contents
+  """
+  if uri.startswith('http'):
+    return uri
+  if ssl:
+    return 'https://%s%s' % (host, uri)
+  else:
+    return 'http://%s%s' % (host, uri)
+
+
+class MockHttpResponse(object):
+  """Returned from MockService crud methods as the server's response."""
+
+  def __init__(self, body=None, status=None, reason=None, headers=None):
+    """Construct a mock HTTPResponse and set members.
+
+    Args:
+      body: str (optional) The HTTP body of the server's response. 
+      status: int (optional) 
+      reason: str (optional)
+      headers: dict (optional)
+    """
+    self.body = body
+    self.status = status
+    self.reason = reason
+    self.headers = headers or {}
+
+  def read(self):
+    return self.body
+
+  def getheader(self, header_name):
+    return self.headers[header_name]
+

Modified: trunk/conduit/modules/GoogleModule/atom/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/atom/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/atom/service.py	Sat Jun  7 07:56:04 2008
@@ -499,13 +499,18 @@
   """Processes a passed URL.  If the URL does not begin with https?, then
   the default value for server is used"""
 
-  server = service.server
-  if for_proxy:
-    port = 80
-    ssl = False
+  server = None
+  port = 80
+  ssl = False
+  if hasattr(service, 'server'):
+    server = service.server
   else:
-    port = service.port
-    ssl = service.ssl
+    server = service
+  if not for_proxy:
+    if hasattr(service, 'port'):
+      port = service.port
+    if hasattr(service, 'ssl'):
+      ssl = service.ssl
   uri = url
 
   m = URL_REGEX.match(url)

Modified: trunk/conduit/modules/GoogleModule/gdata/Makefile.am
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/Makefile.am	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/Makefile.am	Sat Jun  7 07:56:04 2008
@@ -1,4 +1,4 @@
-SUBDIRS = apps base calendar codesearch contacts docs exif geo media photos spreadsheet
+SUBDIRS = apps base blogger calendar codesearch contacts docs exif geo media photos spreadsheet youtube
 
 conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata
 conduit_handlers_PYTHON = __init__.py auth.py service.py

Modified: trunk/conduit/modules/GoogleModule/gdata/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/__init__.py	Sat Jun  7 07:56:04 2008
@@ -252,7 +252,7 @@
   # text node.
   def __SetId(self, id):
     self.__id = id
-    if id is not None:
+    if id is not None and id.text is not None:
       self.__id.text = id.text.strip()
 
   id = property(__GetId, __SetId)
@@ -302,7 +302,7 @@
 
   def __SetId(self, id):
     self.__id = id
-    if id is not None:
+    if id is not None and id.text is not None:
       self.__id.text = id.text.strip()
 
   id = property(__GetId, __SetId)

Modified: trunk/conduit/modules/GoogleModule/gdata/apps/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/apps/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/apps/service.py	Sat Jun  7 07:56:04 2008
@@ -24,7 +24,7 @@
   except ImportError:
     try:
       from xml.etree import ElementTree
-    except Import Error:
+    except ImportError:
       from elementtree import ElementTree
 import urllib
 import gdata
@@ -91,6 +91,15 @@
   def _baseURL(self):
     return "/a/feeds/%s" % self.domain 
 
+  def GetGenaratorFromLinkFinder(self, link_finder, func):
+    """returns a generator for pagination"""
+    yield link_finder
+    next = link_finder.GetNextLink()
+    while next is not None:
+      next_feed = func(str(self.Get(next.href)))
+      yield next_feed
+      next = next_feed.GetNextLink()
+
   def AddAllElementsFromAllPages(self, link_finder, func):
     """retrieve all pages and add all elements"""
     next = link_finder.GetNextLink()
@@ -204,7 +213,6 @@
     uri = "%s/emailList/%s" % (self._baseURL(), API_VER)
     email_list_entry = gdata.apps.EmailListEntry()
     email_list_entry.email_list = gdata.apps.EmailList(name=list_name)
-
     try: 
       return gdata.apps.EmailListEntryFromString(
         str(self.Post(email_list_entry, uri)))
@@ -361,10 +369,17 @@
     except gdata.service.RequestError, e:
       raise AppsForYourDomainException(e.args[0])
 
+  def GetGeneratorForAllUsers(self):
+    """Retrieve a generator for all users in this domain."""
+    first_page = self.RetrievePageOfUsers()
+    return self.GetGenaratorFromLinkFinder(first_page,
+                                           gdata.apps.UserFeedFromString)
+
   def RetrieveAllUsers(self):
-    """Retrieve all users in this domain."""
+    """Retrieve all users in this domain. OBSOLETE"""
 
     ret = self.RetrievePageOfUsers()
     # pagination
     return self.AddAllElementsFromAllPages(
       ret, gdata.apps.UserFeedFromString)
+

Modified: trunk/conduit/modules/GoogleModule/gdata/auth.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/auth.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/auth.py	Sat Jun  7 07:56:04 2008
@@ -75,7 +75,8 @@
   """
   for response_line in http_body.splitlines():
     if response_line.startswith('Auth='):
-      return 'GoogleLogin auth=%s' % response_line.lstrip('Auth=')
+      # Strip off the leading Auth= and return the Authorization value.
+      return 'GoogleLogin auth=%s' % response_line[5:]
   return None
 
 
@@ -107,10 +108,11 @@
     if response_line.startswith('Error=CaptchaRequired'):
       contains_captcha_challenge = True
     elif response_line.startswith('CaptchaToken='):
-      captcha_parameters['token'] = response_line.lstrip('CaptchaToken=')
+      # Strip off the leading CaptchaToken=
+      captcha_parameters['token'] = response_line[13:]
     elif response_line.startswith('CaptchaUrl='):
       captcha_parameters['url'] = '%s%s' % (captcha_base_url,
-          response_line.lstrip('CaptchaUrl='))
+          response_line[11:])
   if contains_captcha_challenge:
     return captcha_parameters
   else:
@@ -191,6 +193,7 @@
   """
   for response_line in http_body.splitlines():
     if response_line.startswith('Token='):
-      auth_token = response_line.lstrip('Token=')
+      # Strip off Token= and construct the Authorization value.
+      auth_token = response_line[6:]
       return 'AuthSub token=%s' % auth_token
   return None

Added: trunk/conduit/modules/GoogleModule/gdata/blogger/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/blogger/Makefile.am	Sat Jun  7 07:56:04 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/blogger
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/blogger/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/blogger/__init__.py	Sat Jun  7 07:56:04 2008
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2007, 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""Contains extensions to Atom objects used with Blogger."""
+
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+
+import atom
+import gdata
+import re
+
+
+LABEL_SCHEME = 'http://www.blogger.com/atom/ns#'
+
+
+class BloggerEntry(gdata.GDataEntry):
+  """Adds convenience methods inherited by all Blogger entries."""
+
+  blog_name_pattern = re.compile('(http://)(\w*)')
+  blog_id_pattern = re.compile('(tag:blogger.com,1999:blog-)(\w*)')
+
+  def GetBlogId(self):
+    """Extracts the Blogger id of this blog.
+    This method is useful when contructing URLs by hand. The blog id is
+    often used in blogger operation URLs. This should not be confused with
+    the id member of a BloggerBlog. The id element is the Atom id XML element.
+    The blog id which this method returns is a part of the Atom id.
+
+    Returns:
+      The blog's unique id as a string.
+    """
+    if self.id.text:
+      return self.blog_id_pattern.match(self.id.text).group(2)
+    return None
+
+  def GetBlogName(self):
+    """Finds the name of this blog as used in the 'alternate' URL.
+    An alternate URL is in the form 'http://blogName.blogspot.com/'. For an
+    entry representing the above example, this method would return 'blogName'.
+
+    Returns:
+      The blog's URL name component as a string.
+    """
+    for link in self.link:
+      if link.rel == 'alternate':
+        return self.blog_name_pattern.match(link.href).group(2)
+    return None
+
+
+class BlogCommentEntry(BloggerEntry):
+  """Describes a blog comment entry in the feed of a blog's comments.
+
+  """
+  pass
+
+
+def BlogCommentEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(BlogCommentEntry, xml_string)
+
+
+class BlogCommentFeed(gdata.GDataFeed):
+  """Describes a feed of a blog's comments.
+
+  """
+  pass
+
+
+def BlogCommentFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(BlogCommentFeed, xml_string)
+
+
+class BlogEntry(BloggerEntry):
+  """Describes a blog entry in the feed of a user's blogs.
+
+  """
+  pass
+
+
+def BlogEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(BlogEntry, xml_string)
+
+
+class BlogFeed(gdata.GDataFeed):
+  """Describes a feed of a user's blogs.
+
+  """
+  
+
+
+def BlogFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(BlogFeed, xml_string)
+
+
+class BlogPostEntry(BloggerEntry):
+  """Describes a blog post entry in the feed of a blog's posts.
+
+  """
+
+  def AddLabel(self, label):
+    """Adds a label to the blog post. 
+
+    The label is represented by an Atom category element, so this method
+    is shorthand for appending a new atom.Category object.
+
+    Args:
+      label: str
+    """
+    self.category.append(atom.Category(scheme=LABEL_SCHEME, term=label))
+
+
+def BlogPostEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(BlogPostEntry, xml_string)
+
+
+class BlogPostFeed(gdata.GDataFeed):
+  """Describes a feed of a blog's posts.
+
+  """
+  pass
+
+
+def BlogPostFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(BlogPostFeed, xml_string)
+
+
+class BloggerLink(atom.Link):
+  """Extends the base Link class with Blogger extensions.
+
+  """
+  pass
+
+
+def BloggerLinkFromString(xml_string):
+  return atom.CreateClassFromXMLString(BloggerLink, xml_string)
+
+
+class PostCommentEntry(BloggerEntry):
+  """Describes a blog post comment entry in the feed of a blog post's comments.
+
+  """
+  pass
+
+
+def PostCommentEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(PostCommentEntry, xml_string)
+
+
+class PostCommentFeed(gdata.GDataFeed):
+  """Describes a feed of a blog post's comments.
+
+  """
+  pass
+
+
+def PostCommentFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(PostCommentFeed, xml_string)
+
+

Added: trunk/conduit/modules/GoogleModule/gdata/blogger/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/blogger/service.py	Sat Jun  7 07:56:04 2008
@@ -0,0 +1,62 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Classes to interact with the Blogger server."""
+
+__author__ = 'api.jscudder (Jeffrey Scudder)'
+
+import gdata.service
+import gdata.blogger
+
+
+class BloggerService(gdata.service.GDataService):
+
+  def __init__(self, email=None, password=None, source=None,
+               server=None, api_key=None,
+               additional_headers=None):
+    gdata.service.GDataService.__init__(self, email=email, password=password,
+                                        service='blogger', source=source,
+                                        server=server,
+                                        additional_headers=additional_headers)
+
+  def GetBlogFeed(self, uri):
+    return self.Get(uri, converter=gdata.blogger.BlogFeedFromString)
+
+  def GetBlogCommentFeed(self, uri):
+    return self.Get(uri, converter=gdata.blogger.BlogCommentFeedFromString)
+
+  def GetBlogPostFeed(self, uri):
+    return self.Get(uri, converter=gdata.blogger.BlogPostFeedFromString)
+
+  def GetPostCommentFeed(self, uri):
+    return self.Get(uri, converter=gdata.blogger.PostCommentFeedFromString)
+
+  def AddPost(self, entry, blog_id=None, uri=None):
+    if blog_id:
+      uri = 'http://www.blogger.com/feeds/%s/posts/default' % blog_id
+    return self.Post(entry, uri, 
+                     converter=gdata.blogger.BlogPostEntryFromString)
+
+  def UpdatePost(self, entry, uri=None):
+    if not uri:
+      uri = entry.GetEditLink().href
+    return self.Put(entry, uri, 
+                    converter=gdata.blogger.BlogPostEntryFromString)
+
+  def PostComment(self, comment_entry, blog_id=None, post_id=None, uri=None):
+    # TODO
+    pass
+

Modified: trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/calendar/__init__.py	Sat Jun  7 07:56:04 2008
@@ -880,12 +880,13 @@
   _attributes = gdata.BatchFeed._attributes.copy()
   _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', 
                                                   [CalendarEventEntry])
+  _children['{%s}timezone' % GCAL_NAMESPACE] = ('timezone', Timezone)
 
   def __init__(self, author=None, category=None, contributor=None,
       generator=None, icon=None, atom_id=None, link=None, logo=None, 
       rights=None, subtitle=None, title=None, updated=None, entry=None, 
       total_results=None, start_index=None, items_per_page=None,
-      interrupted=None,
+      interrupted=None, timezone=None,
       extension_elements=None, extension_attributes=None, text=None):
      gdata.BatchFeed.__init__(self, author=author, category=category,
                               contributor=contributor, generator=generator,
@@ -899,6 +900,7 @@
                               extension_elements=extension_elements,
                               extension_attributes=extension_attributes,
                               text=text)
+     self.timezone = timezone
 
 
 def CalendarListEntryFromString(xml_string):

Modified: trunk/conduit/modules/GoogleModule/gdata/calendar/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/calendar/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/calendar/service.py	Sat Jun  7 07:56:04 2008
@@ -440,10 +440,12 @@
 
   def __init__(self, user='default', visibility='private', projection='full',
                text_query=None, params=None, categories=None):
-    gdata.service.Query.__init__(self, feed='http://www.google.com/calendar/feeds/'+
-                           '%s/%s/%s' % (user, visibility, projection,),
-                           text_query=text_query, params=params, 
-                           categories=categories)
+    gdata.service.Query.__init__(self, 
+        feed='http://www.google.com/calendar/feeds/%s/%s/%s' % (
+            urllib.quote(user), 
+            urllib.quote(visibility), 
+            urllib.quote(projection)),
+        text_query=text_query, params=params, categories=categories)
     
   def _GetStartMin(self):
     if 'start-min' in self.keys():

Modified: trunk/conduit/modules/GoogleModule/gdata/media/__init__.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/media/__init__.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/media/__init__.py	Sat Jun  7 07:56:04 2008
@@ -53,9 +53,11 @@
 import gdata
 
 MEDIA_NAMESPACE = 'http://search.yahoo.com/mrss/'
+YOUTUBE_NAMESPACE = 'http://gdata.youtube.com/schemas/2007'
 
 class MediaBaseElement(atom.AtomBase):
-  """Base class for elements in the MEDIA_NAMESPACE. To add new elements, you only need to add the element tag name to self._tag
+  """Base class for elements in the MEDIA_NAMESPACE. 
+  To add new elements, you only need to add the element tag name to self._tag
   """
   
   _tag = ''
@@ -101,18 +103,20 @@
   _attributes['medium'] = 'medium'
   _attributes['type'] = 'type'
   _attributes['fileSize'] = 'fileSize'
+
   def __init__(self, url=None, width=None, height=None,
-      medium=None, content_type=None, fileSize=None,
+      medium=None, content_type=None, fileSize=None, format=None,
       extension_elements=None, extension_attributes=None, text=None):
     MediaBaseElement.__init__(self, extension_elements=extension_elements,
-                            extension_attributes=extension_attributes,
-                            text=text)
+                              extension_attributes=extension_attributes,
+                              text=text)
     self.url = url
     self.width = width
     self.height = height
     self.medium = medium
     self.type = content_type
     self.fileSize = fileSize
+
 def ContentFromString(xml_string):
   return atom.CreateClassFromXMLString(Content, xml_string)
 
@@ -150,8 +154,8 @@
   def __init__(self, description_type=None, 
       extension_elements=None, extension_attributes=None, text=None):
     MediaBaseElement.__init__(self, extension_elements=extension_elements,
-                            extension_attributes=extension_attributes,
-                            text=text)
+                              extension_attributes=extension_attributes,
+                              text=text)
     
     self.type = description_type
 def DescriptionFromString(xml_string):
@@ -197,8 +201,8 @@
   def __init__(self, url=None, width=None, height=None,
       extension_attributes=None, text=None, extension_elements=None):
     MediaBaseElement.__init__(self, extension_elements=extension_elements,
-                            extension_attributes=extension_attributes,
-                            text=text)
+                              extension_attributes=extension_attributes,
+                              text=text)
     self.url = url
     self.width = width
     self.height = height
@@ -218,15 +222,76 @@
   def __init__(self, title_type=None, 
       extension_attributes=None, text=None, extension_elements=None):
     MediaBaseElement.__init__(self, extension_elements=extension_elements,
-                            extension_attributes=extension_attributes,
-                            text=text)
+                              extension_attributes=extension_attributes,
+                              text=text)
     self.type = title_type
 def TitleFromString(xml_string):
   return atom.CreateClassFromXMLString(Title, xml_string)
 
+class Player(MediaBaseElement):
+  """(string) Contains the embeddable player URL for the entry's media content 
+  if the media is a video.
+  
+  Attributes:
+  url: Always set to plain
+  """
+  
+  _tag = 'player'
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['url'] = 'url'
+  
+  def __init__(self, player_url=None, 
+      extension_attributes=None, extension_elements=None):
+    MediaBaseElement.__init__(self, extension_elements=extension_elements,
+                              extension_attributes=extension_attributes)
+    self.url= player_url
+
+class Private(atom.AtomBase):
+  """The YouTube Private element"""
+  _tag = 'private'
+  _namespace = YOUTUBE_NAMESPACE
+
+class Duration(atom.AtomBase):
+  """The YouTube Duration element"""
+  _tag = 'duration'
+  _namespace = YOUTUBE_NAMESPACE
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['seconds'] = 'seconds'
+
+class Category(MediaBaseElement):
+  """The mediagroup:category element"""
+
+  _tag = 'category'
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['term'] = 'term'
+  _attributes['scheme'] = 'scheme'
+  _attributes['label'] = 'label'
+
+  def __init__(self, term=None, scheme=None, label=None, text=None, 
+               extension_elements=None, extension_attributes=None):
+    """Constructor for Category
+
+    Args:
+      term: str
+      scheme: str
+      label: str
+      text: str The text data in the this element
+      extension_elements: list A  list of ExtensionElement instances
+      extension_attributes: dict A dictionary of attribute value string pairs
+    """
+
+    self.term = term
+    self.scheme = scheme
+    self.label = label
+    self.text = text
+    self.extension_elements = extension_elements or []
+    self.extension_attributes = extension_attributes or {}
+
+
 class Group(MediaBaseElement):
   """Container element for all media elements.
-  The <media:group> element can appear as a child of an album or photo entry."""
+  The <media:group> element can appear as a child of an album, photo or 
+  video entry."""
 
   _tag = 'group'
   _children = atom.AtomBase._children.copy()
@@ -236,18 +301,29 @@
   _children['{%s}keywords' % MEDIA_NAMESPACE] = ('keywords', Keywords) 
   _children['{%s}thumbnail' % MEDIA_NAMESPACE] = ('thumbnail', [Thumbnail,])
   _children['{%s}title' % MEDIA_NAMESPACE] = ('title', Title) 
+  _children['{%s}category' % MEDIA_NAMESPACE] = ('category', Category) 
+  _children['{%s}duration' % YOUTUBE_NAMESPACE] = ('duration', Duration)
+  _children['{%s}private' % YOUTUBE_NAMESPACE] = ('private', Private)
+  _children['{%s}player' % MEDIA_NAMESPACE] = ('player', Player)
 
   def __init__(self, content=None, credit=None, description=None, keywords=None,
-      thumbnail=None, title=None,
-      extension_elements=None, extension_attributes=None, text=None):
+               thumbnail=None, title=None, duration=None, private=None, 
+               category=None, player=None, extension_elements=None, 
+               extension_attributes=None, text=None):
+
     MediaBaseElement.__init__(self, extension_elements=extension_elements,
-                            extension_attributes=extension_attributes,
-                            text=text)
+                              extension_attributes=extension_attributes,
+                              text=text)
     self.content=content
     self.credit=credit
     self.description=description
     self.keywords=keywords
     self.thumbnail=thumbnail or []
     self.title=title
+    self.duration=duration
+    self.private=private
+    self.category=category
+    self.player=player
+
 def GroupFromString(xml_string):
   return atom.CreateClassFromXMLString(Group, xml_string)

Modified: trunk/conduit/modules/GoogleModule/gdata/service.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/service.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/service.py	Sat Jun  7 07:56:04 2008
@@ -114,6 +114,8 @@
 class UnexpectedReturnType(Error):
   pass
 
+class BadAuthenticationServiceURL(Error):
+  pass
 
 class GDataService(atom.service.AtomService):
   """Contains elements needed for GData login and CRUD request headers.
@@ -123,7 +125,7 @@
   """
 
   def __init__(self, email=None, password=None, account_type='HOSTED_OR_GOOGLE',
-               service=None, source=None, server=None, 
+               service=None, auth_service_url=None, source=None, server=None, 
                additional_headers=None, handler=None):
     """Creates an object of type GDataService.
 
@@ -138,6 +140,8 @@
           GOOGLE account. Default value: 'HOSTED_OR_GOOGLE'.
       service: string (optional) The desired service for which credentials
           will be obtained.
+      auth_service_url: string (optional) User-defined auth token request URL
+          allows users to explicitly specify where to send auth token requests.
       source: string (optional) The name of the user's application.
       server: string (optional) The name of the server to which a connection
           will be opened. Default value: 'base.google.com'.
@@ -152,6 +156,7 @@
     self.password = password
     self.account_type = account_type
     self.service = service
+    self.auth_service_url = auth_service_url
     self.server = server
     self.additional_headers = additional_headers or {}
     self.handler = handler or http_request_handler
@@ -233,6 +238,17 @@
       doc="""Get the captcha URL for a login request.""")
 
   def GetAuthSubToken(self):
+    """Returns the AuthSub Token after removing the AuthSub Authorization
+    Label.
+     
+    The AuthSub Authorization Label reads: "AuthSub token"
+
+    Returns:
+      If the AuthSub Token is set AND it begins with the AuthSub 
+      Authorization Label, the AuthSub Token is returned minus the AuthSub
+      Label. If the AuthSub Token does not start with the AuthSub
+      Authorization Label or it is not set, None is returned.
+    """
     if self.__auth_token.startswith(AUTHSUB_AUTH_LABEL):
       # Strip off the leading 'AUTHSUB_AUTH_LABEL=' and just return the
       # token value.
@@ -261,7 +277,8 @@
   def __SetSource(self, new_source):
     self.__source = new_source
     # Update the UserAgent header to include the new application name.
-    self.additional_headers['User-Agent'] = '%s GData-Python/1.0.12.1' % self.__source
+    self.additional_headers['User-Agent'] = '%s GData-Python/1.0.13' % (
+        self.__source)
 
   source = property(__GetSource, __SetSource, 
       doc="""The source is the name of the application making the request. 
@@ -295,8 +312,15 @@
         self.password, self.service, self.source, self.account_type, 
         captcha_token, captcha_response)
 
+    # If the user has defined their own authentication service URL, 
+    # send the ClientLogin requests to this URL:
+    if not self.auth_service_url:
+        auth_request_url = AUTH_SERVER_HOST + '/accounts/ClientLogin' 
+    else:
+        auth_request_url = self.auth_service_url
+
     auth_response = self.handler.HttpRequest(self, 'POST', request_body, 
-        AUTH_SERVER_HOST + '/accounts/ClientLogin', 
+        auth_request_url,
         extra_headers={'Content-Length':str(len(request_body))},
         content_type='application/x-www-form-urlencoded')
     response_body = auth_response.read()
@@ -311,7 +335,7 @@
       # Examine each line to find the error type and the captcha token and
       # captch URL if they are present.
       captcha_parameters = gdata.auth.GetCaptchChallenge(response_body, 
-          captcha_base_url='%saccounts/' % AUTH_SERVER_HOST)
+          captcha_base_url='%s/accounts/' % AUTH_SERVER_HOST)
       if captcha_parameters:
         self.__captcha_token = captcha_parameters['token']
         self.__captcha_url = captcha_parameters['url']
@@ -324,9 +348,16 @@
         self.__captcha_token = None
         self.__captcha_url = None
         raise Error, 'Server responded with a 403 code'
+    elif auth_response.status == 302:
+      self.__captcha_token = None
+      self.__captcha_url = None
+      # Google tries to redirect all bad URLs back to 
+      # http://www.google.<locale>. If a redirect
+      # attempt is made, assume the user has supplied an incorrect authentication URL
+      raise BadAuthenticationServiceURL, 'Server responded with a 302 code.'
 
   def ClientLogin(self, username, password, account_type=None, service=None,
-      source=None, captcha_token=None, captcha_response=None):
+      auth_service_url=None, source=None, captcha_token=None, captcha_response=None):
     """Convenience method for authenticating using ProgrammaticLogin. 
     
     Sets values for email, password, and other optional members.
@@ -336,6 +367,7 @@
       password:
       account_type: string (optional)
       service: string (optional)
+      auth_service_url: string (optional)
       captcha_token: string (optional)
       captcha_response: string (optional)
     """
@@ -348,6 +380,8 @@
       self.service = service
     if source:
       self.source = source
+    if auth_service_url:
+      self.auth_service_url = auth_service_url
 
     self.ProgrammaticLogin(captcha_token, captcha_response)
 
@@ -522,7 +556,8 @@
     """
     response_handle = self.handler.HttpRequest(self, 'GET', None, uri, 
         extra_headers=extra_headers)
-    return gdata.MediaSource(response_handle, response_handle.getheader('Content-Type'),
+    return gdata.MediaSource(response_handle, response_handle.getheader(
+            'Content-Type'),
         response_handle.getheader('Content-Length'))
 
   def GetEntry(self, uri, extra_headers=None):
@@ -908,7 +943,7 @@
     """
     
     self.feed = feed
-    self.categories = categories or []
+    self.categories = []
     if text_query:
       self.text_query = text_query
     if isinstance(params, dict):

Modified: trunk/conduit/modules/GoogleModule/gdata/test_data.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/test_data.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/test_data.py	Sat Jun  7 07:56:04 2008
@@ -2074,153 +2074,188 @@
 
 </feed>"""
 
-YOU_TUBE_VIDEO_FEED = """<?xml version='1.0' encoding='UTF-8'?>
-<feed xmlns='http://www.w3.org/2005/Atom'
-         xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
-         xmlns:gml='http://www.opengis.net/gml'
-         xmlns:georss='http://www.georss.org/georss'
-         xmlns:media='http://search.yahoo.com/mrss/'
-         xmlns:yt='http://gdata.youtube.com/schemas/2007'
-         xmlns:gd='http://schemas.google.com/g/2005'>
-  <id>http://gdata.youtube.com/feeds/api/standardfeeds/top_rated</id>
-  <updated>2008-02-21T18:57:10.801Z</updated>
+YOUTUBE_VIDEO_FEED = """<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gml='http://www.opengis.net/gml' xmlns:georss='http://www.georss.org/georss' xmlns:media='http://search.yahoo.com/mrss/' xmlns:yt='http://gdata.youtube.com/schemas/2007' xmlns:gd='http://schemas.google.com/g/2005'><id>http://gdata.youtube.com/feeds/api/standardfeeds/top_rated</id><updated>2008-05-14T02:24:07.000-07:00</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#video'/><title type='text'>Top Rated</title><logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo><link rel='alternate' type='text/html' href='http://www.youtube.com/browse?s=tr'/><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated'/><link rel='self' type='application/atom+xml' href='ht
 tp://gdata.youtube.com/feeds/api/standardfeeds/top_rated?start-index=1&amp;max-results=25'/><link rel='next' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?start-index=26&amp;max-results=25'/><author><name>YouTube</name><uri>http://www.youtube.com/</uri></author><generator version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator><openSearch:totalResults>100</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+<entry><id>http://gdata.youtube.com/feeds/api/videos/C71ypXYGho8</id><published>2008-03-20T10:17:27.000-07:00</published><updated>2008-05-14T04:26:37.000-07:00</updated><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='karyn'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='garcia'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='me'/><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#video'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='boyfriend'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='por'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='te'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='odeio'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='amar'/><category scheme='http://gdata.youtube.com/
 schemas/2007/categories.cat' term='Music' label='Music'/><title type='text'>Me odeio por te amar - KARYN GARCIA</title><content type='text'>http://www.karyngarcia.com.br</content><link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=C71ypXYGho8'/><link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/C71ypXYGho8/related'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated/C71ypXYGho8'/><author><name>TvKarynGarcia</name><uri>http://gdata.youtube.com/feeds/api/users/tvkaryngarcia</uri></author><media:group><media:title type='plain'>Me odeio por te amar - KARYN GARCIA</media:title><media:description type='plain'>http://www.karyngarcia.com.br</media:description><media:keywords>amar, boyfriend, garcia, karyn, me, odeio, por, te</media:keywords><yt:duration seconds='203'/><media:category label='Music' scheme='http://g
 data.youtube.com/schemas/2007/categories.cat'>Music</media:category><media:content url='http://www.youtube.com/v/C71ypXYGho8' type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full' duration='203' yt:format='5'/><media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQmPhgZ2pXK9CxMYDSANFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='203' yt:format='1'/><media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQmPhgZ2pXK9CxMYESARFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='203' yt:format='6'/><media:player url='http://www.youtube.com/watch?v=C71ypXYGho8'/><media:thumbnail url='http://img.youtube.com/vi/C71ypXYGho8/2.jpg' height='97' width='130' time='00:01:41.500'/><media:thumbnail url='http://img.youtube.com/vi/C71ypXYGho8/1.jpg' height='97' width='130' time='00:00:50.750'/><media:thumbnail url='http://img.youtube.com/vi/C71ypXYGho8/3.jpg' height='97' width='130'
  time='00:02:32.250'/><media:thumbnail url='http://img.youtube.com/vi/C71ypXYGho8/0.jpg' height='240' width='320' time='00:01:41.500'/></media:group><yt:statistics viewCount='138864' favoriteCount='2474'/><gd:rating min='1' max='5' numRaters='4626' average='4.95'/><gd:comments><gd:feedLink href='http://gdata.youtube.com/feeds/api/videos/C71ypXYGho8/comments' countHint='27'/></gd:comments></entry>
+<entry><id>http://gdata.youtube.com/feeds/api/videos/gsVaTyb1tBw</id><published>2008-02-15T04:31:45.000-08:00</published><updated>2008-05-14T05:09:42.000-07:00</updated><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='extreme'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='cam'/><category scheme='http://gdata.youtube.com/schemas/2007/categories.cat' term='Sports' label='Sports'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='alcala'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='kani'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='helmet'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='campillo'/><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#video'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='pato'/><category scheme='
 http://gdata.youtube.com/schemas/2007/keywords.cat' term='dirt'/><title type='text'>extreme helmet cam Kani, Keil and Pato</title><content type='text'>trimmed</content><link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=gsVaTyb1tBw'/><link rel='http://gdata.youtube.com/schemas/2007#video.responses' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/gsVaTyb1tBw/responses'/><link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/gsVaTyb1tBw/related'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/standardfeeds/recently_featured/gsVaTyb1tBw'/><author><name>peraltamagic</name><uri>http://gdata.youtube.com/feeds/api/users/peraltamagic</uri></author><media:group><media:title type='plain'>extreme helmet cam Kani, Keil and Pato</media:title><media:description type='plain'>trimmed</media:description><media:keywords
 >alcala, cam, campillo, dirt, extreme, helmet, kani, pato</media:keywords><yt:duration seconds='31'/><media:category label='Sports' scheme='http://gdata.youtube.com/schemas/2007/categories.cat'>Sports</media:category><media:content url='http://www.youtube.com/v/gsVaTyb1tBw' type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full' duration='31' yt:format='5'/><media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQkctPUmT1rFghMYDSANFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='31' yt:format='1'/><media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQkctPUmT1rFghMYESARFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='31' yt:format='6'/><media:player url='http://www.youtube.com/watch?v=gsVaTyb1tBw'/><media:thumbnail url='http://img.youtube.com/vi/gsVaTyb1tBw/2.jpg' height='97' width='130' time='00:00:15.500'/><media:thumbnail url='http://img.youtube.com/vi/gsVaTyb1
 tBw/1.jpg' height='97' width='130' time='00:00:07.750'/><media:thumbnail url='http://img.youtube.com/vi/gsVaTyb1tBw/3.jpg' height='97' width='130' time='00:00:23.250'/><media:thumbnail url='http://img.youtube.com/vi/gsVaTyb1tBw/0.jpg' height='240' width='320' time='00:00:15.500'/></media:group><yt:statistics viewCount='489941' favoriteCount='561'/><gd:rating min='1' max='5' numRaters='1255' average='4.11'/><gd:comments><gd:feedLink href='http://gdata.youtube.com/feeds/api/videos/gsVaTyb1tBw/comments' countHint='1116'/></gd:comments></entry>
+</feed>"""
+
+YOUTUBE_ENTRY_PRIVATE = """<?xml version='1.0' encoding='utf-8'?>
+<entry xmlns='http://www.w3.org/2005/Atom' 
+xmlns:media='http://search.yahoo.com/mrss/' 
+xmlns:gd='http://schemas.google.com/g/2005' 
+xmlns:yt='http://gdata.youtube.com/schemas/2007' 
+xmlns:gml='http://www.opengis.net/gml' 
+xmlns:georss='http://www.georss.org/georss'
+xmlns:app='http://purl.org/atom/app#'>
+  <id>http://gdata.youtube.com/feeds/videos/UMFI1hdm96E</id>
+  <published>2007-01-07T01:50:15.000Z</published>
+  <updated>2007-01-07T01:50:15.000Z</updated>
   <category scheme='http://schemas.google.com/g/2005#kind'
-    term='http://gdata.youtube.com/schemas/2007#video'/>
-  <title type='text'>Top Rated</title>
-  <logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo>
-  <link rel='alternate' type='text/html'
-    href='http://www.youtube.com/browser?s=tr'/>
-  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml'
-    href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated'/>
-  <link rel='self' type='application/atom+xml'
-    href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?start_index=1&amp;max-results=25'/>
+  term='http://gdata.youtube.com/schemas/2007#video' />
+  <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat'
+  term='barkley' />
+  <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat'
+  term='singing' />
+  <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat'
+  term='acoustic' />
+  <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat'
+  term='cover' />
+  <category scheme='http://gdata.youtube.com/schemas/2007/categories.cat'
+  term='Music' label='Music' />
+  <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat'
+  term='gnarls' />
+  <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat'
+  term='music' />
+  <title type='text'>"Crazy (Gnarles Barkley)" - Acoustic Cover</title>
+  <content type='html'>&lt;div style="color: #000000;font-family:
+  Arial, Helvetica, sans-serif; font-size:12px; font-size: 12px;
+  width: 555px;"&gt;&lt;table cellspacing="0" cellpadding="0"
+  border="0"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td width="140"
+  valign="top" rowspan="2"&gt;&lt;div style="border: 1px solid
+  #999999; margin: 0px 10px 5px 0px;"&gt;&lt;a
+  href="http://www.youtube.com/watch?v=UMFI1hdm96E"&gt;&lt;img
+  alt=""
+  src="http://img.youtube.com/vi/UMFI1hdm96E/2.jpg"&gt;&lt;/a&gt;&lt;/div&gt;&lt;/td&gt;
+  &lt;td width="256" valign="top"&gt;&lt;div style="font-size:
+  12px; font-weight: bold;"&gt;&lt;a style="font-size: 15px;
+  font-weight: bold; font-decoration: none;"
+  href="http://www.youtube.com/watch?v=UMFI1hdm96E"&gt;&amp;quot;Crazy
+  (Gnarles Barkley)&amp;quot; - Acoustic Cover&lt;/a&gt;
+  &lt;br&gt;&lt;/div&gt; &lt;div style="font-size: 12px; margin:
+  3px 0px;"&gt;&lt;span&gt;Gnarles Barkley acoustic cover
+  http://www.myspace.com/davidchoimusic&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;
+  &lt;td style="font-size: 11px; line-height: 1.4em; padding-left:
+  20px; padding-top: 1px;" width="146"
+  valign="top"&gt;&lt;div&gt;&lt;span style="color: #666666;
+  font-size: 11px;"&gt;From:&lt;/span&gt; &lt;a
+  href="http://www.youtube.com/profile?user=davidchoimusic"&gt;davidchoimusic&lt;/a&gt;&lt;/div&gt;
+  &lt;div&gt;&lt;span style="color: #666666; font-size:
+  11px;"&gt;Views:&lt;/span&gt; 113321&lt;/div&gt; &lt;div
+  style="white-space: nowrap;text-align: left"&gt;&lt;img
+  style="border: 0px none; margin: 0px; padding: 0px;
+  vertical-align: middle; font-size: 11px;" align="top" alt=""
+  src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"&gt;
+  &lt;img style="border: 0px none; margin: 0px; padding: 0px;
+  vertical-align: middle; font-size: 11px;" align="top" alt=""
+  src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"&gt;
+  &lt;img style="border: 0px none; margin: 0px; padding: 0px;
+  vertical-align: middle; font-size: 11px;" align="top" alt=""
+  src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"&gt;
+  &lt;img style="border: 0px none; margin: 0px; padding: 0px;
+  vertical-align: middle; font-size: 11px;" align="top" alt=""
+  src="http://gdata.youtube.com/static/images/icn_star_full_11x11.gif"&gt;
+  &lt;img style="border: 0px none; margin: 0px; padding: 0px;
+  vertical-align: middle; font-size: 11px;" align="top" alt=""
+  src="http://gdata.youtube.com/static/images/icn_star_half_11x11.gif"&gt;&lt;/div&gt;
+  &lt;div style="font-size: 11px;"&gt;1005 &lt;span style="color:
+  #666666; font-size:
+  11px;"&gt;ratings&lt;/span&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
+  &lt;tr&gt;&lt;td&gt;&lt;span style="color: #666666; font-size:
+  11px;"&gt;Time:&lt;/span&gt; &lt;span style="color: #000000;
+  font-size: 11px; font-weight:
+  bold;"&gt;04:15&lt;/span&gt;&lt;/td&gt; &lt;td style="font-size:
+  11px; padding-left: 20px;"&gt;&lt;span style="color: #666666;
+  font-size: 11px;"&gt;More in&lt;/span&gt; &lt;a
+  href="http://www.youtube.com/categories_portal?c=10"&gt;Music&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;</content>
   <link rel='self' type='application/atom+xml'
-    href='http://gdata.youtube.com/feeds/api/standardfeeds/top_rated?start_index=26&amp;max-results=25'/>
+  href='http://gdata.youtube.com/feeds/videos/UMFI1hdm96E' />
+  <link rel='alternate' type='text/html'
+  href='http://www.youtube.com/watch?v=UMFI1hdm96E' />
+  <link rel='http://gdata.youtube.com/schemas/2007#video.responses'
+  type='application/atom+xml'
+  href='http://gdata.youtube.com/feeds/videos/UMFI1hdm96E/responses' />
+  <link rel='http://gdata.youtube.com/schemas/2007#video.related'
+  type='application/atom+xml'
+  href='http://gdata.youtube.com/feeds/videos/UMFI1hdm96E/related' />
   <author>
-    <name>YouTube</name>
-    <uri>http://www.youtube.com/</uri>
+    <name>davidchoimusic</name>
+    <uri>http://gdata.youtube.com/feeds/users/davidchoimusic</uri>
   </author>
-  <generator version='beta'
-    uri='http://gdata.youtube.com/'>YouTube data API</generator>
-  <openSearch:totalResults>99</openSearch:totalResults>
-  <openSearch:startIndex>1</openSearch:startIndex>
-  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
-  <entry>
-    <id>http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b</id>
-    <published>2007-02-16T20:22:57.000Z</published>
-    <updated>2007-02-16T20:22:57.000Z</updated>
-    <category scheme="http://schemas.google.com/g/2005#kind";
-      term="http://gdata.youtube.com/schemas/2007#video"/>
-    <category scheme="http://gdata.youtube.com/schemas/2007/keywords.cat";
-      term="Steventon"/>
-    <category scheme="http://gdata.youtube.com/schemas/2007/keywords.cat";
-      term="walk"/>
-    <category scheme="http://gdata.youtube.com/schemas/2007/keywords.cat";
-      term="Darcy"/>
-    <category scheme="http://gdata.youtube.com/schemas/2007/categories.cat";
-      term="Entertainment" label="Entertainment"/>
-    <title type="text">My walk with Mr. Darcy</title>
-    <content type="html"><div ... html content trimmed ...></content>
-    <link rel="self" type="application/atom+xml"
-      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b"/>
-    <link rel="alternate" type="text/html"
-      href="http://www.youtube.com/watch?v=ZTUVgYoeN_b"/>
-    <link rel="http://gdata.youtube.com/schemas/2007#video.responses";
-      type="application/atom+xml"
-      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/responses"/>
-    <link rel="http://gdata.youtube.com/schemas/2007#video.ratings";
-      type="application/atom+xml"
-      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/ratings"/>
-    <link rel="http://gdata.youtube.com/schemas/2007#video.complaints";
-      type="application/atom+xml"
-      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/complaints"/>
-    <link rel="http://gdata.youtube.com/schemas/2007#video.related";
-      type="application/atom+xml"
-      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/related"/>
-    <author>
-      <name>Andy Samplo</name>
-      <uri>http://gdata.youtube.com/feeds/api/users/andyland74</uri>
-    </author>
   <media:group>
-    <media:title type="plain">Shopping for Coats</media:title>
-    <media:description type="plain">
-      What could make for more exciting video?
-    </media:description>
-    <media:keywords>Shopping, parkas</media:keywords>
-    <yt:duration seconds="79"/>
-    <media:category label="People"
-      scheme="http://gdata.youtube.com/schemas/2007/categories.cat";>People
-    </media:category>
-    <media:content
-      url='http://www.youtube.com/v/ZTUVgYoeN_b'
-      type='application/x-shockwave-flash' medium='video' 
-      isDefault='true' expression="full" duration='215' yt:format="5"/>
-    <media:content
-      url='rtsp://rtsp2.youtube.com/ChoLENy73bIAEQ1k30OPEgGDA==/0/0/0/video.3gp'
-      type='video/3gpp' medium='video' 
-      expression="full" duration='215' yt:format="1"/>
-    <media:content
-      url='rtsp://rtsp2.youtube.com/ChoLENy73bIAEQ1k30OPEgGDA==/0/0/0/video.3gp'
-      type='video/3gpp' medium='video' 
-      expression="full" duration='215' yt:format="6"/>
-    <media:player url="http://www.youtube.com/watch?v=ZTUVgYoeN_b"/>
-    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/2.jpg";
-      height="97" width="130" time="00:00:03.500"/>
-    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/1.jpg";
-      height="97" width="130" time="00:00:01.750"/>
-    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/3.jpg";
-      height="97" width="130" time="00:00:05.250"/>
-    <media:thumbnail url="http://img.youtube.com/vi/ZTUVgYoeN_b/0.jpg";
-      height="240" width="320" time="00:00:03.500"/>
+    <media:title type='plain'>"Crazy (Gnarles Barkley)" - Acoustic Cover</media:title>
+    <media:description type='plain'>Gnarles Barkley acoustic cover http://www.myspace.com/davidchoimusic</media:description>
+    <media:keywords>music, singing, gnarls, barkley, acoustic, cover</media:keywords>
+    <yt:duration seconds='255' />
+    <media:category label='Music'
+    scheme='http://gdata.youtube.com/schemas/2007/categories.cat'>
+    Music</media:category>
+    <media:category 
+    scheme='http://gdata.youtube.com/schemas/2007/developertags.cat'>
+    DeveloperTag1</media:category>
+    <media:content url='http://www.youtube.com/v/UMFI1hdm96E'
+    type='application/x-shockwave-flash' medium='video'
+    isDefault='true' expression='full' duration='255'
+    yt:format='5' />
+    <media:player url='http://www.youtube.com/watch?v=UMFI1hdm96E' />
+    <media:thumbnail url='http://img.youtube.com/vi/UMFI1hdm96E/2.jpg'
+    height='97' width='130' time='00:02:07.500' />
+    <media:thumbnail url='http://img.youtube.com/vi/UMFI1hdm96E/1.jpg'
+    height='97' width='130' time='00:01:03.750' />
+    <media:thumbnail url='http://img.youtube.com/vi/UMFI1hdm96E/3.jpg'
+    height='97' width='130' time='00:03:11.250' />
+    <media:thumbnail url='http://img.youtube.com/vi/UMFI1hdm96E/0.jpg'
+    height='240' width='320' time='00:02:07.500' />
+    <yt:private />
   </media:group>
-  <yt:statistics viewCount="93"/>
-  <gd:rating min='1' max='5' numRaters='435' average='4.94'/>
+  <yt:statistics viewCount='113321' />
+  <gd:rating min='1' max='5' numRaters='1005' average='4.77' />
+  <georss:where>
+    <gml:Point>
+      <gml:pos>37.398529052734375 -122.0635986328125</gml:pos>
+    </gml:Point>
+  </georss:where>
   <gd:comments>
-    <gd:feedLink
-      href="http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments";
-      countHint='2197'/>
+    <gd:feedLink href='http://gdata.youtube.com/feeds/videos/UMFI1hdm96E/comments' />
   </gd:comments>
-</entry>
-</feed>"""
+  <yt:noembed />
+      <app:control>
+        <app:draft>yes</app:draft>
+        <yt:state
+          name="rejected"
+          reasonCode="inappropriate"
+          helpUrl="http://www.youtube.com/t/community_guidelines";>
+          The content of this video may violate the terms of use.</yt:state>
+      </app:control>
+</entry>"""
 
-YOU_TUBE_COMMENT_FEED = """<?xml version='1.0' encoding='UTF-8'?>
-<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'>
-  <id>http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments?start-index=1&amp;max-results=25</id>
-  <updated>2008-02-25T23:14:03.148Z</updated>
-  <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#comment'/>
-  <title type='text'>Comments on 'My walk with Mr. Darcy'</title>
-  <logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo>
-  <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b'/>
-  <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=ZTUVgYoeN_b'/>
-  <link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments'/>
-  <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments?start-index=1&amp;max-results=25'/>
-  <link rel='next' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments?start-index=26&amp;max-results=25'/>
-  <author>                                                    
-    <name>YouTube</name>                                 
-    <uri>http://www.youtube.com/</uri>
-  </author>                                                           
-  <generator version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator>
-  <openSearch:totalResults>100</openSearch:totalResults>
-  <openSearch:startIndex>1</openSearch:startIndex>
-  <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+YOUTUBE_COMMENT_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+<feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'><id>http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments</id><updated>2008-05-19T21:45:45.261Z</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#comment'/><title type='text'>Comments</title><logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo><link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU'/><link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=2Idhz9ef5oU'/><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments?start-index=1&amp;max-results=25'/><author><name>YouTube</name><uri>http://www.youtube.com/</uri></author><generato
 r version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator><openSearch:totalResults>0</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage>
   <entry>
-    <id>http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments/7F2BAAD03653A691</id>
-    <published>2007-05-23T00:21:59.000-07:00</published>
-    <updated>2007-05-23T00:21:59.000-07:00</updated>
+    <id>http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/91F809A3DE2EB81B</id>
+    <published>2008-02-22T15:27:15.000-08:00</published><updated>2008-02-22T15:27:15.000-08:00</updated>
     <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#comment'/>
-    <title type='text'>Walking is fun.</title>
-    <content type='text'>Walking is fun.</content>
-    <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b'/>
-    <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=ZTUVgYoeN_b'/>
-    <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/ZTUVgYoeN_b/comments/7F2BAAD03653A691'/>
-    <author>
-      <name>andyland744</name>
-      <uri>http://gdata.youtube.com/feeds/api/users/andyland744</uri> 
-    </author>                                                                                    
-  </entry>                
+    <title type='text'>test66</title>
+      <content type='text'>test66</content>
+      <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU'/>
+      <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=2Idhz9ef5oU'/>
+      <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/91F809A3DE2EB81B'/>
+      <author><name>apitestjhartmann</name><uri>http://gdata.youtube.com/feeds/users/apitestjhartmann</uri></author>
+   </entry>
+   <entry>
+    <id>http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/A261AEEFD23674AA</id>
+    <published>2008-02-22T15:27:01.000-08:00</published><updated>2008-02-22T15:27:01.000-08:00</updated>
+    <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#comment'/>
+    <title type='text'>test333</title>
+      <content type='text'>test333</content>
+        <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU'/>
+        <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=2Idhz9ef5oU'/>
+        <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/A261AEEFD23674AA'/>
+        <author><name>apitestjhartmann</name><uri>http://gdata.youtube.com/feeds/users/apitestjhartmann</uri></author>
+    </entry>
+    <entry>
+      <id>http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/0DCF1E3531B3FF85</id>
+      <published>2008-02-22T15:11:06.000-08:00</published><updated>2008-02-22T15:11:06.000-08:00</updated>
+      <category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#comment'/>
+      <title type='text'>test2</title>
+      <content type='text'>test2</content>
+        <link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU'/>
+        <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=2Idhz9ef5oU'/>
+        <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2Idhz9ef5oU/comments/0DCF1E3531B3FF85'/>
+        <author><name>apitestjhartmann</name><uri>http://gdata.youtube.com/feeds/users/apitestjhartmann</uri></author>
+    </entry>
 </feed>"""
 
-YOU_TUBE_PLAYLIST_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+YOUTUBE_PLAYLIST_FEED = """<?xml version='1.0' encoding='UTF-8'?>
 <feed xmlns='http://www.w3.org/2005/Atom'
     xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' 
     xmlns:media='http://search.yahoo.com/mrss/' 
@@ -2244,6 +2279,8 @@
   <openSearch:startIndex>1</openSearch:startIndex>
   <openSearch:itemsPerPage>25</openSearch:itemsPerPage>
   <entry>
+    <yt:description>My new playlist Description</yt:description>
+    <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#playlist' href='http://gdata.youtube.com/feeds/playlists/8BCDD04DE8F771B2'/>
     <id>http://gdata.youtube.com/feeds/users/andyland74/playlists/8BCDD04DE8F771B2</id>
     <published>2007-11-04T17:30:27.000-08:00</published>
     <updated>2008-02-22T09:55:14.000-08:00</updated>
@@ -2257,12 +2294,24 @@
       <name>andyland74</name>                              
       <uri>http://gdata.youtube.com/feeds/users/andyland74</uri>
     </author>
-    <yt:description>My new playlist Description</yt:description>
-    <gd:feedLink rel='http://gdata.youtube.com/schemas/2007#playlist' href='http://gdata.youtube.com/feeds/playlists/8BCDD04DE8F771B2'/>
   </entry>              
 </feed>"""
 
-YOU_TUBE_SUBSCRIPTIONS_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+YOUTUBE_PLAYLIST_VIDEO_FEED = """<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gml='http://www.opengis.net/gml' xmlns:georss='http://www.georss.org/georss' xmlns:media='http://search.yahoo.com/mrss/' xmlns:yt='http://gdata.youtube.com/schemas/2007' xmlns:gd='http://schemas.google.com/g/2005'><id>http://gdata.youtube.com/feeds/api/playlists/BCB3BB96DF51B505</id><updated>2008-05-16T12:03:17.000-07:00</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#playlist'/><category scheme='http://gdata.youtube.com/schemas/2007/tags.cat' term='videos'/><category scheme='http://gdata.youtube.com/schemas/2007/tags.cat' term='python'/><title type='text'>Test Playlist</title><subtitle type='text'>Test playlist 1</subtitle><logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo><link rel='alternate' type='text/html' href='http:/
 /www.youtube.com/view_play_list?p=BCB3BB96DF51B505'/><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/playlists/BCB3BB96DF51B505'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/playlists/BCB3BB96DF51B505?start-index=1&amp;max-results=25'/><author><name>gdpython</name><uri>http://gdata.youtube.com/feeds/api/users/gdpython</uri></author><generator version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator><openSearch:totalResults>1</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage><media:group><media:title type='plain'>Test Playlist</media:title><media:description type='plain'>Test playlist 1</media:description><media:content url='http://www.youtube.com/ep.swf?id=BCB3BB96DF51B505' type='application/x-shockwave-flash' yt:format='5'/></media:group><entry><id>http://gdata.youtube.c
 om/feeds/api/playlists/BCB3BB96DF51B505/B0F29389E537F888</id><updated>2008-05-16T20:54:08.520Z</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#playlist'/><title type='text'>Uploading YouTube Videos with the PHP Client Library</title><content type='text'>Jochen Hartmann demonstrates the basics of how to use the PHP Client Library with the YouTube Data API.
+
+PHP Developer's Guide:
+http://code.google.com/apis/youtube/developers_guide_php.html
+
+Other documentation:
+http://code.google.com/apis/youtube/</content><link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=iIp7OnHXBlo'/><link rel='http://gdata.youtube.com/schemas/2007#video.responses' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/iIp7OnHXBlo/responses'/><link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/iIp7OnHXBlo/related'/><link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/videos/iIp7OnHXBlo'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/api/playlists/BCB3BB96DF51B505/B0F29389E537F888'/><author><name>GoogleDevelopers</name><uri>http://gdata.youtube.com/feeds/api/users/googledevelopers</uri></author><media:group><media:title type='plain'>Uploading YouTube Videos with the PHP Client Library</media:title><media:description type='plain'>Jochen Hartmann demonstrates the
  basics of how to use the PHP Client Library with the YouTube Data API.
+
+PHP Developer's Guide:
+http://code.google.com/apis/youtube/developers_guide_php.html
+
+Other documentation:
+http://code.google.com/apis/youtube/</media:description><media:keywords>api, data, demo, php, screencast, tutorial, uploading, walkthrough, youtube</media:keywords><yt:duration seconds='466'/><media:category label='Education' scheme='http://gdata.youtube.com/schemas/2007/categories.cat'>Education</media:category><media:content url='http://www.youtube.com/v/iIp7OnHXBlo' type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full' duration='466' yt:format='5'/><media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQlaBtdxOnuKiBMYDSANFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='466' yt:format='1'/><media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQlaBtdxOnuKiBMYESARFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='466' yt:format='6'/><media:player url='http://www.youtube.com/watch?v=iIp7OnHXBlo'/><media:thumbnail url='http://img.youtube.com/vi/iIp7OnHXBlo/2.jpg' h
 eight='97' width='130' time='00:03:53'/><media:thumbnail url='http://img.youtube.com/vi/iIp7OnHXBlo/1.jpg' height='97' width='130' time='00:01:56.500'/><media:thumbnail url='http://img.youtube.com/vi/iIp7OnHXBlo/3.jpg' height='97' width='130' time='00:05:49.500'/><media:thumbnail url='http://img.youtube.com/vi/iIp7OnHXBlo/0.jpg' height='240' width='320' time='00:03:53'/></media:group><yt:statistics viewCount='1550' favoriteCount='5'/><gd:rating min='1' max='5' numRaters='3' average='4.67'/><yt:location>undefined</yt:location><gd:comments><gd:feedLink href='http://gdata.youtube.com/feeds/api/videos/iIp7OnHXBlo/comments' countHint='2'/></gd:comments><yt:position>1</yt:position></entry></feed>"""
+
+YOUTUBE_SUBSCRIPTION_FEED = """<?xml version='1.0' encoding='UTF-8'?>
 <feed xmlns='http://www.w3.org/2005/Atom'
     xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'
     xmlns:media='http://search.yahoo.com/mrss/'
@@ -2315,7 +2364,53 @@
   </entry>
 </feed>"""
 
-YOU_TUBE_PROFILE = """<?xml version='1.0' encoding='UTF-8'?>
+YOUTUBE_VIDEO_RESPONSE_FEED = """<?xml version='1.0' encoding='UTF-8'?>
+  <feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:gml='http://www.opengis.net/gml' xmlns:georss='http://www.georss.org/georss' xmlns:media='http://search.yahoo.com/mrss/' xmlns:yt='http://gdata.youtube.com/schemas/2007' xmlns:gd='http://schemas.google.com/g/2005'>
+  <id>http://gdata.youtube.com/feeds/videos/2c3q9K4cHzY/responses</id><updated>2008-05-19T22:37:34.076Z</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#video'/><title type='text'>Videos responses to 'Giant NES controller coffee table'</title><logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo><link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2c3q9K4cHzY'/><link rel='alternate' type='text/html' href='http://www.youtube.com/video_response_view_all?v=2c3q9K4cHzY'/><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2c3q9K4cHzY/responses'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2c3q9K4cHzY/responses?start-index=1&amp;max-results=25'/><author><name>YouTube</name><uri>http://www.youtube.com/</uri></author><generator version='beta' uri='http://gdat
 a.youtube.com/'>YouTube data API</generator><openSearch:totalResults>8</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+    <entry>
+      <id>http://gdata.youtube.com/feeds/videos/7b9EnRI9VbY</id><published>2008-03-11T19:08:53.000-07:00</published><updated>2008-05-18T21:33:10.000-07:00</updated>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='OD'/><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#video'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='chat'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='Uncle'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='sex'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='catmint'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='kato'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='kissa'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='katt'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/categories.cat' term='Animals' label='Pets &amp; Animals'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='kat'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='cat'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='cats'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='kedi'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='gato'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='Brattman'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='drug'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='overdose'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='catnip'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='party'/>
+      <category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='Katze'/><category scheme='http://gdata.youtube.com/schemas/2007/keywords.cat' term='gatto'/>
+      <title type='text'>Catnip Party</title><content type='html'>snipped</content>
+      <link rel='alternate' type='text/html' href='http://www.youtube.com/watch?v=7b9EnRI9VbY'/>
+      <link rel='http://gdata.youtube.com/schemas/2007#video.responses' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/7b9EnRI9VbY/responses'/>
+      <link rel='http://gdata.youtube.com/schemas/2007#video.related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/7b9EnRI9VbY/related'/>
+      <link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/videos/2c3q9K4cHzY/responses/7b9EnRI9VbY'/>
+      <author><name>PismoBeach</name><uri>http://gdata.youtube.com/feeds/users/pismobeach</uri></author>
+        <media:group>
+          <media:title type='plain'>Catnip Party</media:title>
+          <media:description type='plain'>Uncle, Hillary, Hankette, and B4 all but overdose on the patio</media:description><media:keywords>Brattman, cat, catmint, catnip, cats, chat, drug, gato, gatto, kat, kato, katt, Katze, kedi, kissa, OD, overdose, party, sex, Uncle</media:keywords>
+          <yt:duration seconds='139'/>
+          <media:category label='Pets &amp; Animals' scheme='http://gdata.youtube.com/schemas/2007/categories.cat'>Animals</media:category>
+          <media:content url='http://www.youtube.com/v/7b9EnRI9VbY' type='application/x-shockwave-flash' medium='video' isDefault='true' expression='full' duration='139' yt:format='5'/>
+          <media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQm2VT0SnUS_7RMYDSANFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='139' yt:format='1'/>
+          <media:content url='rtsp://rtsp2.youtube.com/ChoLENy73wIaEQm2VT0SnUS_7RMYESARFEgGDA==/0/0/0/video.3gp' type='video/3gpp' medium='video' expression='full' duration='139' yt:format='6'/>
+          <media:player url='http://www.youtube.com/watch?v=7b9EnRI9VbY'/>
+          <media:thumbnail url='http://img.youtube.com/vi/7b9EnRI9VbY/2.jpg' height='97' width='130' time='00:01:09.500'/>
+          <media:thumbnail url='http://img.youtube.com/vi/7b9EnRI9VbY/1.jpg' height='97' width='130' time='00:00:34.750'/>
+          <media:thumbnail url='http://img.youtube.com/vi/7b9EnRI9VbY/3.jpg' height='97' width='130' time='00:01:44.250'/>
+          <media:thumbnail url='http://img.youtube.com/vi/7b9EnRI9VbY/0.jpg' height='240' width='320' time='00:01:09.500'/>
+        </media:group>
+        <yt:statistics viewCount='4235' favoriteCount='3'/>
+        <gd:rating min='1' max='5' numRaters='24' average='3.54'/>
+        <gd:comments>
+          <gd:feedLink href='http://gdata.youtube.com/feeds/videos/7b9EnRI9VbY/comments' countHint='14'/>
+        </gd:comments>
+        </entry>
+</feed>
+"""
+
+
+YOUTUBE_PROFILE = """<?xml version='1.0' encoding='UTF-8'?>
 <entry xmlns='http://www.w3.org/2005/Atom'
     xmlns:media='http://search.yahoo.com/mrss/'
     xmlns:yt='http://gdata.youtube.com/schemas/2007'
@@ -2338,10 +2433,13 @@
   </author>
   <yt:age>33</yt:age>
   <yt:username>andyland74</yt:username>
+  <yt:firstName>andy</yt:firstName>
+  <yt:lastName>example</yt:lastName>
   <yt:books>Catch-22</yt:books>
   <yt:gender>m</yt:gender>
   <yt:company>Google</yt:company>
   <yt:hobbies>Testing YouTube APIs</yt:hobbies>
+  <yt:hometown>Somewhere</yt:hometown>
   <yt:location>US</yt:location>
   <yt:movies>Aqua Teen Hungerforce</yt:movies>
   <yt:music>Elliott Smith</yt:music>
@@ -2364,6 +2462,15 @@
     href='http://gdata.youtube.com/feeds/users/andyland74/uploads' countHint='1'/>
 </entry>"""
 
+YOUTUBE_CONTACTS_FEED = """<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:yt='http://gdata.youtube.com/schemas/2007' xmlns:gd='http://schemas.google.com/g/2005'>
+  <id>http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts</id><updated>2008-05-16T19:24:34.916Z</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#friend'/><title type='text'>apitestjhartmann's Contacts</title><logo>http://www.youtube.com/img/pic_youtubelogo_123x63.gif</logo><link rel='alternate' type='text/html' href='http://www.youtube.com/profile_friends?user=apitestjhartmann'/><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts'/><link rel='http://schemas.google.com/g/2005#post' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts?start-index=1&amp;max-results=25'/><author><name>apitestjhartmann</name><uri>http://gdata.youtube.com/feeds/users/apitestjh
 artmann</uri></author><generator version='beta' uri='http://gdata.youtube.com/'>YouTube data API</generator><openSearch:totalResults>2</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>25</openSearch:itemsPerPage>
+    <entry>
+      <id>http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/test89899090</id><published>2008-02-04T11:27:54.000-08:00</published><updated>2008-05-16T19:24:34.916Z</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#friend'/><title type='text'>test89899090</title><link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/test89899090'/><link rel='alternate' type='text/html' href='http://www.youtube.com/profile?user=test89899090'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/test89899090'/><link rel='edit' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/test89899090'/><author><name>apitestjhartmann</name><uri>http://gdata.youtube.com/feeds/users/apitestjhartmann</uri></author><yt:username>test89899090</yt:username><yt:status>requested</yt:status></entry>
+    <entry>
+      <id>http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/testjfisher</id><published>2008-02-26T14:13:03.000-08:00</published><updated>2008-05-16T19:24:34.916Z</updated><category scheme='http://schemas.google.com/g/2005#kind' term='http://gdata.youtube.com/schemas/2007#friend'/><title type='text'>testjfisher</title><link rel='related' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/testjfisher'/><link rel='alternate' type='text/html' href='http://www.youtube.com/profile?user=testjfisher'/><link rel='self' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/testjfisher'/><link rel='edit' type='application/atom+xml' href='http://gdata.youtube.com/feeds/users/apitestjhartmann/contacts/testjfisher'/><author><name>apitestjhartmann</name><uri>http://gdata.youtube.com/feeds/users/apitestjhartmann</uri></author><yt:username>testjfisher</yt:username><yt:status>pending</yt:status></entry>
+</feed>"""
+
+
 NEW_CONTACT = """<?xml version='1.0' encoding='UTF-8'?>
 <atom:entry xmlns:atom='http://www.w3.org/2005/Atom'
     xmlns:gd='http://schemas.google.com/g/2005'>
@@ -2424,3 +2531,117 @@
       primary='true'>456</gd:phoneNumber>
   </entry>
 </feed>"""
+
+BLOG_ENTRY = """<entry xmlns='http://www.w3.org/2005/Atom'>
+  <id>tag:blogger.com,1999:blog-blogID.post-postID</id>
+  <published>2006-08-02T18:44:43.089-07:00</published>
+  <updated>2006-11-08T18:10:23.020-08:00</updated>
+  <title type='text'>Lizzy's Diary</title>
+  <summary type='html'>Being the journal of Elizabeth Bennet</summary>
+  <link rel='alternate' type='text/html'
+    href='http://blogName.blogspot.com/'>
+  </link>
+  <link rel='http://schemas.google.com/g/2005#feed'
+    type='application/atom+xml'
+    href='http://blogName.blogspot.com/feeds/posts/default'>
+  </link>
+  <link rel='http://schemas.google.com/g/2005#post'
+    type='application/atom+xml'
+    href='http://www.blogger.com/feeds/blogID/posts/default'>
+  </link>
+  <link rel='self' type='application/atom+xml'
+    href='http://www.blogger.com/feeds/userID/blogs/blogID'>
+  </link>
+  <link rel='edit' type='application/atom+xml' 
+      href='http://www.blogger.com/feeds/userID/blogs/blogID'>
+  </link>
+  <author>
+    <name>Elizabeth Bennet</name>
+    <email>liz gmail com</email>
+  </author>
+</entry>"""
+
+BLOG_POST = """<entry xmlns='http://www.w3.org/2005/Atom'>
+  <title type='text'>Marriage!</title>
+  <content type='xhtml'>
+    <div xmlns="http://www.w3.org/1999/xhtml";>
+      <p>Mr. Darcy has <em>proposed marriage</em> to me!</p>
+      <p>He is the last man on earth I would ever desire to marry.</p>
+      <p>Whatever shall I do?</p>
+    </div>
+  </content>
+  <author>
+    <name>Elizabeth Bennet</name>
+    <email>liz gmail com</email>
+  </author>
+</entry>"""
+
+BLOG_POSTS_FEED = """<feed xmlns='http://www.w3.org/2005/Atom'>
+  <id>tag:blogger.com,1999:blog-blogID</id>
+  <updated>2006-11-08T18:10:23.020-08:00</updated>
+  <title type='text'>Lizzy's Diary</title>
+  <link rel='alternate' type='text/html'
+    href='http://blogName.blogspot.com/index.html'>
+  </link>
+  <link rel='http://schemas.google.com/g/2005#feed'
+    type='application/atom+xml'
+    href='http://blogName.blogspot.com/feeds/posts/default'>
+  </link>
+  <link rel='self' type='application/atom+xml'
+    href='http://blogName.blogspot.com/feeds/posts/default'>
+  </link>
+  <author>
+    <name>Elizabeth Bennet</name>
+    <email>liz gmail com</email>
+  </author>
+  <generator version='7.00' uri='http://www2.blogger.com'>Blogger</generator>
+  <entry>
+    <id>tag:blogger.com,1999:blog-blogID.post-postID</id>
+    <published>2006-11-08T18:10:00.000-08:00</published>
+    <updated>2006-11-08T18:10:14.954-08:00</updated>
+    <title type='text'>Quite disagreeable</title>
+    <content type='html'>&lt;p&gt;I met Mr. Bingley's friend Mr. Darcy
+      this evening. I found him quite disagreeable.&lt;/p&gt;</content>
+    <link rel='alternate' type='text/html'
+      href='http://blogName.blogspot.com/2006/11/quite-disagreeable.html'>
+    </link>
+    <link rel='self' type='application/atom+xml'
+      href='http://blogName.blogspot.com/feeds/posts/default/postID'>
+    </link>
+    <link rel='edit' type='application/atom+xml'
+      href='http://www.blogger.com/feeds/blogID/posts/default/postID'>
+    </link>
+    <author>
+      <name>Elizabeth Bennet</name>
+      <email>liz gmail com</email>
+    </author>
+  </entry>
+</feed>"""
+
+BLOG_COMMENTS_FEED = """<feed xmlns="http://www.w3.org/2005/Atom"; xmlns:openSearch="http://a9.com/-/spec/opensearchrss/1.0/";>
+  <id>tag:blogger.com,1999:blog-blogID.postpostID..comments</id>
+  <updated>2007-04-04T21:56:29.803-07:00</updated>
+  <title type="text">My Blog : Time to relax</title>
+  <link rel="alternate" type="text/html" href="http://blogName.blogspot.com/2007/04/first-post.html"/>
+  <link rel="http://schemas.google.com/g/2005#feed"; type="application/atom+xml" href="http://blogName.blogspot.com/feeds/postID/comments/default"/>
+  <link rel="self" type="application/atom+xml" href="http://blogName.blogspot.com/feeds/postID/comments/default"/>
+  <author>
+    <name>Blog Author name</name>
+  </author>
+  <generator version="7.00" uri="http://www2.blogger.com";>Blogger</generator>
+  <openSearch:totalResults>1</openSearch:totalResults>
+  <openSearch:startIndex>1</openSearch:startIndex>
+  <entry>
+    <id>tag:blogger.com,1999:blog-blogID.post-commentID</id>
+    <published>2007-04-04T21:56:00.000-07:00</published>
+    <updated>2007-04-04T21:56:29.803-07:00</updated>
+    <title type="text">This is my first comment</title>
+    <content type="html">This is my first comment</content>
+    <link rel="alternate" type="text/html" href="http://blogName.blogspot.com/2007/04/first-post.html#commentID"/>
+    <link rel="self" type="application/atom+xml" href="http://blogName.blogspot.com/feeds/postID/comments/default/commentID"/>
+    <link rel="edit" type="application/atom+xml" href="http://www.blogger.com/feeds/blogID/postID/comments/default/commentID"/>
+    <author>
+      <name>Blog Author name</name>
+    </author>
+  </entry>
+</feed>"""

Modified: trunk/conduit/modules/GoogleModule/gdata/urlfetch.py
==============================================================================
--- trunk/conduit/modules/GoogleModule/gdata/urlfetch.py	(original)
+++ trunk/conduit/modules/GoogleModule/gdata/urlfetch.py	Sat Jun  7 07:56:04 2008
@@ -50,7 +50,7 @@
         port (int), and ssl (bool).
     operation: str The HTTP operation to be performed. This is usually one of
         'GET', 'POST', 'PUT', or 'DELETE'
-    data: ElementTree, filestream, list of parts, or other object which can be
+    data: filestream, list of parts, or other object which can be
         converted to a string.
         Should be set to None when performing a GET or PUT.
         If data is a file-like object which can be read, this method will read
@@ -150,5 +150,7 @@
       return self.body.read(length)
 
   def getheader(self, name):
+    if not self.headers.has_key(name):
+      return self.headers[name.lower()]
     return self.headers[name]
     

Added: trunk/conduit/modules/GoogleModule/gdata/youtube/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/youtube/Makefile.am	Sat Jun  7 07:56:04 2008
@@ -0,0 +1,5 @@
+conduit_handlersdir = $(libdir)/conduit/modules/GoogleModule/gdata/youtube
+conduit_handlers_PYTHON = __init__.py service.py
+
+clean-local:
+	rm -rf *.pyc *.pyo

Added: trunk/conduit/modules/GoogleModule/gdata/youtube/__init__.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/youtube/__init__.py	Sat Jun  7 07:56:04 2008
@@ -0,0 +1,615 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+__author__ = ('api stephaniel gmail com (Stephanie Liu)'
+              ', api jhartmann gmail com (Jochen Hartmann)')
+
+import atom
+import gdata
+import gdata.media as Media
+import gdata.geo as Geo
+
+# XML namespaces which are often used in YouTube entities.
+YOUTUBE_NAMESPACE = 'http://gdata.youtube.com/schemas/2007'
+YOUTUBE_TEMPLATE = '{http://gdata.youtube.com/schemas/2007}%s'
+YOUTUBE_FORMAT = '{http://gdata.youtube.com/schemas/2007}format'
+
+class Username(atom.AtomBase):
+  """The YouTube Username element"""
+  _tag = 'username'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class FirstName(atom.AtomBase):
+  """The YouTube FirstName element"""
+  _tag = 'firstName'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class LastName(atom.AtomBase):
+  """The YouTube LastName element"""
+  _tag = 'lastName'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class Age(atom.AtomBase):
+  """The YouTube Age element"""
+  _tag = 'age'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class Books(atom.AtomBase):
+  """The YouTube Books element"""
+  _tag = 'books'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Gender(atom.AtomBase):
+  """The YouTube Gender element"""
+  _tag = 'gender'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Company(atom.AtomBase):
+  """The YouTube Company element"""
+  _tag = 'company'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Hobbies(atom.AtomBase):
+  """The YouTube Hobbies element"""
+  _tag = 'hobbies'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Hometown(atom.AtomBase):
+  """The YouTube Hometown element"""
+  _tag = 'hometown'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Location(atom.AtomBase):
+  """The YouTube Location element"""
+  _tag = 'location'
+  _namespace = YOUTUBE_NAMESPACE 
+
+
+class Movies(atom.AtomBase):
+  """The YouTube Movies element"""
+  _tag = 'movies'
+  _namespace = YOUTUBE_NAMESPACE    
+
+
+class Music(atom.AtomBase):
+  """The YouTube Music element"""
+  _tag = 'music'
+  _namespace = YOUTUBE_NAMESPACE    
+
+
+class Occupation(atom.AtomBase):
+  """The YouTube Occupation element"""
+  _tag = 'occupation'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class School(atom.AtomBase):
+  """The YouTube School element"""
+  _tag = 'school'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Relationship(atom.AtomBase):
+  """The YouTube Relationship element"""
+  _tag = 'relationship'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Recorded(atom.AtomBase):
+  """The YouTube Recorded element"""
+  _tag = 'recorded'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class Statistics(atom.AtomBase):
+  """The YouTube Statistics element"""
+  _tag = 'statistics'
+  _namespace = YOUTUBE_NAMESPACE
+  _attributes = atom.AtomBase._attributes.copy() 
+  _attributes['viewCount'] = 'view_count'
+  _attributes['videoWatchCount'] = 'video_watch_count'
+  _attributes['subscriberCount'] = 'subscriber_count'
+  _attributes['lastWebAccess'] = 'last_web_access'
+  _attributes['favoriteCount'] = 'favorite_count'
+
+  def __init__(self, view_count=None, video_watch_count=None,
+               favorite_count=None, subscriber_count=None, last_web_access=None,
+               extension_elements=None, extension_attributes=None, text=None):
+
+    self.view_count = view_count
+    self.video_watch_count = video_watch_count
+    self.subscriber_count = subscriber_count
+    self.last_web_access = last_web_access
+    self.favorite_count = favorite_count
+
+    atom.AtomBase.__init__(self, extension_elements=extension_elements,
+                           extension_attributes=extension_attributes, text=text)
+
+
+class Status(atom.AtomBase):
+  """The YouTube Status element"""
+  _tag = 'status'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class Position(atom.AtomBase):
+  """The YouTube Position element. The position in a playlist feed."""
+  _tag = 'position'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Racy(atom.AtomBase):
+  """The YouTube Racy element."""
+  _tag = 'racy'
+  _namespace = YOUTUBE_NAMESPACE  
+
+class Description(atom.AtomBase):
+  """The YouTube Description element."""
+  _tag = 'description'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class Private(atom.AtomBase):
+  """The YouTube Private element."""
+  _tag = 'private'
+  _namespace = YOUTUBE_NAMESPACE
+
+
+class NoEmbed(atom.AtomBase):
+  """The YouTube VideoShare element. Whether a video can be embedded or not."""
+  _tag = 'noembed'
+  _namespace = YOUTUBE_NAMESPACE  
+
+
+class Comments(atom.AtomBase):
+  """The GData Comments element"""
+  _tag = 'comments'
+  _namespace = gdata.GDATA_NAMESPACE
+  _children = atom.AtomBase._children.copy()
+  _attributes = atom.AtomBase._attributes.copy()
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
+                                                        [gdata.FeedLink])
+
+  def __init__(self, feed_link=None, extension_elements=None,
+               extension_attributes=None, text=None):
+
+    self.feed_link = feed_link
+    atom.AtomBase.__init__(self, extension_elements=extension_elements,
+                           extension_attributes=extension_attributes, text=text)
+
+
+class Rating(atom.AtomBase):
+  """The GData Rating element"""
+  _tag = 'rating'
+  _namespace = gdata.GDATA_NAMESPACE
+  _attributes = atom.AtomBase._attributes.copy()
+  _attributes['min'] = 'min'
+  _attributes['max'] = 'max'
+  _attributes['numRaters'] = 'num_raters'
+  _attributes['average'] = 'average'
+
+  def __init__(self, min=None, max=None,
+               num_raters=None, average=None, extension_elements=None,
+               extension_attributes=None, text=None):
+
+    self.min = min
+    self.max = max
+    self.num_raters = num_raters
+    self.average = average
+
+    atom.AtomBase.__init__(self, extension_elements=extension_elements,
+                           extension_attributes=extension_attributes, text=text)
+
+
+class YouTubePlaylistVideoEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
+                                                        [gdata.FeedLink])
+  _children['{%s}description' % YOUTUBE_NAMESPACE] = ('description',
+                                                       Description)
+  _children['{%s}rating' % gdata.GDATA_NAMESPACE] = ('rating', Rating)
+  _children['{%s}comments' % gdata.GDATA_NAMESPACE] = ('comments', Comments)
+  _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics)
+  _children['{%s}location' % YOUTUBE_NAMESPACE] = ('location', Location)
+  _children['{%s}position' % YOUTUBE_NAMESPACE] = ('position', Position)
+  _children['{%s}group' % gdata.media.MEDIA_NAMESPACE] = ('media', Media.Group)
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None, title=None,
+               updated=None, feed_link=None, description=None,
+               rating=None, comments=None, statistics=None,
+               location=None, position=None, media=None,
+               extension_elements=None, extension_attributes=None):
+
+    self.feed_link = feed_link
+    self.description = description
+    self.rating = rating
+    self.comments = comments
+    self.statistics = statistics
+    self.location = location
+    self.position = position
+    self.media = media
+
+    gdata.GDataEntry.__init__(self, author=author, category=category,
+                              content=content, atom_id=atom_id,
+                              link=link, published=published, title=title,
+                              updated=updated,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes)
+
+
+class YouTubeVideoCommentEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+
+
+class YouTubeSubscriptionEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}username' % YOUTUBE_NAMESPACE] = ('username', Username)
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
+                                                        [gdata.FeedLink])
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None, title=None,
+               updated=None, username=None, feed_link=None,
+               extension_elements=None, extension_attributes=None):
+
+    gdata.GDataEntry.__init__(self, author=author, category=category,
+                              content=content, atom_id=atom_id, link=link,
+                              published=published, title=title, updated=updated)
+
+    self.username = username
+    self.feed_link = feed_link
+
+
+class YouTubeVideoResponseEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}rating' % gdata.GDATA_NAMESPACE] = ('rating', Rating)
+  _children['{%s}noembed' % YOUTUBE_NAMESPACE] = ('noembed', NoEmbed)
+  _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics)
+  _children['{%s}racy' % YOUTUBE_NAMESPACE] = ('racy', Racy)
+  _children['{%s}group' % gdata.media.MEDIA_NAMESPACE] = ('media', Media.Group)
+
+  def __init__(self, author=None, category=None, content=None, atom_id=None,
+               link=None, published=None, title=None, updated=None, rating=None,
+               noembed=None, statistics=None, racy=None, media=None,
+               extension_elements=None, extension_attributes=None):
+
+    gdata.GDataEntry.__init__(self, author=author, category=category,
+                              content=content, atom_id=atom_id, link=link,
+                              published=published, title=title, updated=updated)
+
+    self.rating = rating
+    self.noembed = noembed
+    self.statistics = statistics
+    self.racy = racy
+    self.media = media or Media.Group()
+
+
+class YouTubeContactEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}username' % YOUTUBE_NAMESPACE] = ('username', Username)
+  _children['{%s}status' % YOUTUBE_NAMESPACE] = ('status', Status)
+
+
+  def __init__(self, author=None, category=None, content=None, atom_id=None,
+               link=None, published=None, title=None, updated=None,
+               username=None, status=None, extension_elements=None, 
+               extension_attributes=None, text=None):
+
+    gdata.GDataEntry.__init__(self, author=author, category=category,
+                              content=content, atom_id=atom_id, link=link,
+                              published=published, title=title, updated=updated)
+
+    self.username = username
+    self.status = status
+
+
+class YouTubeVideoEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}rating' % gdata.GDATA_NAMESPACE] = ('rating', Rating)
+  _children['{%s}comments' % gdata.GDATA_NAMESPACE] = ('comments', Comments)
+  _children['{%s}noembed' % YOUTUBE_NAMESPACE] = ('noembed', NoEmbed)
+  _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics)
+  _children['{%s}recorded' % YOUTUBE_NAMESPACE] = ('recorded', Recorded)
+  _children['{%s}racy' % YOUTUBE_NAMESPACE] = ('racy', Racy)
+  _children['{%s}group' % gdata.media.MEDIA_NAMESPACE] = ('media', Media.Group)
+  _children['{%s}where' % gdata.geo.GEORSS_NAMESPACE] = ('geo', Geo.Where)
+
+  def __init__(self, author=None, category=None, content=None, atom_id=None,
+               link=None, published=None, title=None, updated=None, rating=None,
+               noembed=None, statistics=None, racy=None, media=None, geo=None,
+               recorded=None, comments=None, extension_elements=None, 
+               extension_attributes=None):
+
+    self.rating = rating
+    self.noembed = noembed
+    self.statistics = statistics
+    self.racy = racy
+    self.comments = comments
+    self.media = media or Media.Group()
+    self.geo = geo
+    self.recorded = recorded
+
+    gdata.GDataEntry.__init__(self, author=author, category=category,
+                              content=content, atom_id=atom_id, link=link,
+                              published=published, title=title, updated=updated,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes)
+
+  def GetSwfUrl(self):
+    """Return the URL for the embeddable Video
+
+      Returns:
+          URL of the embeddable video
+    """
+    if self.media.content:
+      for content in self.media.content:
+        if content.extension_attributes[YOUTUBE_FORMAT] == '5':
+          return content.url
+    else:
+      return None
+
+
+class YouTubeUserEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}username' % YOUTUBE_NAMESPACE] = ('username', Username)
+  _children['{%s}firstName' % YOUTUBE_NAMESPACE] = ('first_name', FirstName)
+  _children['{%s}lastName' % YOUTUBE_NAMESPACE] = ('last_name', LastName)
+  _children['{%s}age' % YOUTUBE_NAMESPACE] = ('age', Age)
+  _children['{%s}books' % YOUTUBE_NAMESPACE] = ('books', Books)
+  _children['{%s}gender' % YOUTUBE_NAMESPACE] = ('gender', Gender)
+  _children['{%s}company' % YOUTUBE_NAMESPACE] = ('company', Company)
+  _children['{%s}description' % YOUTUBE_NAMESPACE] = ('description',
+                                                       Description)
+  _children['{%s}hobbies' % YOUTUBE_NAMESPACE] = ('hobbies', Hobbies)
+  _children['{%s}hometown' % YOUTUBE_NAMESPACE] = ('hometown', Hometown)
+  _children['{%s}location' % YOUTUBE_NAMESPACE] = ('location', Location)
+  _children['{%s}movies' % YOUTUBE_NAMESPACE] = ('movies', Movies)
+  _children['{%s}music' % YOUTUBE_NAMESPACE] = ('music', Music)
+  _children['{%s}occupation' % YOUTUBE_NAMESPACE] = ('occupation', Occupation)
+  _children['{%s}school' % YOUTUBE_NAMESPACE] = ('school', School)
+  _children['{%s}relationship' % YOUTUBE_NAMESPACE] = ('relationship',
+                                                        Relationship)
+  _children['{%s}statistics' % YOUTUBE_NAMESPACE] = ('statistics', Statistics)
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
+                                                        [gdata.FeedLink])
+  _children['{%s}thumbnail' % gdata.media.MEDIA_NAMESPACE] = ('thumbnail',
+                                                               Media.Thumbnail)
+
+  def __init__(self, author=None, category=None, content=None, atom_id=None,
+               link=None, published=None, title=None, updated=None,
+               username=None, first_name=None, last_name=None, age=None,
+               books=None, gender=None, company=None, description=None,
+               hobbies=None, hometown=None, location=None, movies=None,
+               music=None, occupation=None, school=None, relationship=None,
+               statistics=None, feed_link=None, extension_elements=None,
+               extension_attributes=None, text=None):
+
+    self.username = username
+    self.first_name = first_name
+    self.last_name = last_name
+    self.age = age
+    self.books = books
+    self.gender = gender
+    self.company = company
+    self.description = description
+    self.hobbies = hobbies
+    self.hometown = hometown
+    self.location = location
+    self.movies = movies
+    self.music = music
+    self.occupation = occupation
+    self.school = school
+    self.relationship = relationship
+    self.statistics = statistics
+    self.feed_link = feed_link
+
+    gdata.GDataEntry.__init__(self, author=author, category=category,
+                              content=content, atom_id=atom_id,
+                              link=link, published=published,
+                              title=title, updated=updated,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes,
+                              text=text)
+
+
+class YouTubeVideoFeed(gdata.GDataFeed, gdata.LinkFinder):
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry', [YouTubeVideoEntry])
+
+class YouTubePlaylistEntry(gdata.GDataEntry):
+  _tag = gdata.GDataEntry._tag
+  _namespace = gdata.GDataEntry._namespace
+  _children = gdata.GDataEntry._children.copy()
+  _attributes = gdata.GDataEntry._attributes.copy()
+  _children['{%s}description' % YOUTUBE_NAMESPACE] = ('description',
+                                                       Description)
+  _children['{%s}private' % YOUTUBE_NAMESPACE] = ('private',
+                                                  Private)
+  _children['{%s}feedLink' % gdata.GDATA_NAMESPACE] = ('feed_link',
+                                                        [gdata.FeedLink])
+
+  def __init__(self, author=None, category=None, content=None,
+               atom_id=None, link=None, published=None, title=None,
+               updated=None, private=None, feed_link=None,
+               description=None, extension_elements=None,
+               extension_attributes=None):
+
+    self.description = description
+    self.private = private
+    self.feed_link = feed_link
+
+    gdata.GDataEntry.__init__(self, author=author, category=category,
+                              content=content, atom_id=atom_id,
+                              link=link, published=published, title=title, 
+                              updated=updated,
+                              extension_elements=extension_elements,
+                              extension_attributes=extension_attributes)
+
+
+
+class YouTubePlaylistFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """ A feed of a user's playlists """
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
+                                                  [YouTubePlaylistEntry])
+
+
+class YouTubePlaylistVideoFeed(gdata.GDataFeed, gdata.LinkFinder):
+  """ A feed of videos in a user's playlist """
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
+                                                  [YouTubePlaylistVideoEntry])
+
+
+class YouTubeContactFeed(gdata.GDataFeed, gdata.LinkFinder):
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
+                                                  [YouTubeContactEntry])
+
+
+class YouTubeSubscriptionFeed(gdata.GDataFeed, gdata.LinkFinder):
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
+                                                  [YouTubeSubscriptionEntry])
+
+
+class YouTubeVideoCommentFeed(gdata.GDataFeed, gdata.LinkFinder):
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
+                                                  [YouTubeVideoCommentEntry])
+
+
+class YouTubeVideoResponseFeed(gdata.GDataFeed, gdata.LinkFinder):
+  _tag = gdata.GDataFeed._tag
+  _namespace = gdata.GDataFeed._namespace
+  _children = gdata.GDataFeed._children.copy()
+  _attributes = gdata.GDataFeed._attributes.copy()
+  _children['{%s}entry' % atom.ATOM_NAMESPACE] = ('entry',
+                                                  [YouTubeVideoResponseEntry])
+
+
+def YouTubeVideoFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeVideoFeed, xml_string)
+
+
+def YouTubeVideoEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeVideoEntry, xml_string)
+
+
+def YouTubeContactFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeContactFeed, xml_string)
+
+
+def YouTubeContactEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeContactEntry, xml_string)
+
+
+def YouTubeVideoCommentFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeVideoCommentFeed, xml_string)
+
+
+def YouTubeVideoCommentEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeVideoCommentEntry, xml_string)
+
+
+def YouTubeUserFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeVideoFeed, xml_string)
+
+
+def YouTubeUserEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeUserEntry, xml_string)
+
+
+def YouTubePlaylistFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubePlaylistFeed, xml_string)
+
+
+def YouTubePlaylistVideoFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubePlaylistVideoFeed, xml_string)
+
+
+def YouTubePlaylistEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubePlaylistEntry, xml_string)
+
+
+def YouTubePlaylistVideoEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubePlaylistVideoEntry, xml_string)
+
+
+def YouTubeSubscriptionFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeSubscriptionFeed, xml_string)
+
+
+def YouTubeSubscriptionEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeSubscriptionEntry, xml_string)
+
+
+def YouTubeVideoResponseFeedFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeVideoResponseFeed, xml_string)
+
+
+def YouTubeVideoResponseEntryFromString(xml_string):
+  return atom.CreateClassFromXMLString(YouTubeVideoResponseEntry, xml_string)

Added: trunk/conduit/modules/GoogleModule/gdata/youtube/service.py
==============================================================================
--- (empty file)
+++ trunk/conduit/modules/GoogleModule/gdata/youtube/service.py	Sat Jun  7 07:56:04 2008
@@ -0,0 +1,1016 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2006 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""YouTubeService extends GDataService to streamline YouTube operations.
+
+  YouTubeService: Provides methods to perform CRUD operations on YouTube feeds. 
+  Extends GDataService.
+
+"""
+
+__author__ = ('api stephaniel gmail com (Stephanie Liu), '
+              'api jhartmann gmail com (Jochen Hartmann)')
+
+try:
+  from xml.etree import ElementTree
+except ImportError:
+  from elementtree import ElementTree
+import urllib
+import os
+import gdata
+import atom
+import gdata.service
+import gdata.youtube
+# TODO (jhartmann) - rewrite query class structure + allow passing in projections
+
+YOUTUBE_SERVER = 'gdata.youtube.com'
+YOUTUBE_SERVICE = 'youtube'
+YOUTUBE_SUPPORTED_UPLOAD_TYPES = ('mov', 'avi', 'wmv', 'mpg', 'quicktime')
+YOUTUBE_QUERY_VALID_TIME_PARAMETERS = ('today', 'this_week', 'this_month',
+                                        'all_time')
+YOUTUBE_QUERY_VALID_ORDERBY_PARAMETERS = ('updated', 'viewCount', 'rating',
+                                           'relevance')
+YOUTUBE_QUERY_VALID_RACY_PARAMETERS = ('include', 'exclude')
+YOUTUBE_QUERY_VALID_FORMAT_PARAMETERS = ('1', '5', '6')
+YOUTUBE_STANDARDFEEDS = ('most_recent', 'recently_featured',
+                          'top_rated', 'most_viewed','watch_on_mobile')
+
+YOUTUBE_UPLOAD_TOKEN_URI = 'http://gdata.youtube.com/action/GetUploadToken'
+YOUTUBE_VIDEO_URI = 'http://gdata.youtube.com/feeds/api/videos'
+YOUTUBE_USER_FEED_URI = 'http://gdata.youtube.com/feeds/api/users/'
+
+YOUTUBE_STANDARD_FEEDS = 'http://gdata.youtube.com/feeds/api/standardfeeds'
+YOUTUBE_STANDARD_TOP_RATED_URI = YOUTUBE_STANDARD_FEEDS + '/top_rated'
+YOUTUBE_STANDARD_MOST_VIEWED_URI = YOUTUBE_STANDARD_FEEDS + '/most_viewed'
+YOUTUBE_STANDARD_RECENTLY_FEATURED_URI = YOUTUBE_STANDARD_FEEDS + (
+    '/recently_featured')
+YOUTUBE_STANDARD_WATCH_ON_MOBILE_URI = YOUTUBE_STANDARD_FEEDS + (
+    '/watch_on_mobile')
+YOUTUBE_STANDARD_TOP_FAVORITES_URI = YOUTUBE_STANDARD_FEEDS + '/top_favorites'
+YOUTUBE_STANDARD_MOST_RECENT_URI = YOUTUBE_STANDARD_FEEDS + '/most_recent'
+YOUTUBE_STANDARD_MOST_DISCUSSED_URI = YOUTUBE_STANDARD_FEEDS + '/most_discussed'
+YOUTUBE_STANDARD_MOST_LINKED_URI = YOUTUBE_STANDARD_FEEDS + '/most_linked'
+YOUTUBE_STANDARD_MOST_RESPONDED_URI = YOUTUBE_STANDARD_FEEDS + '/most_responded'
+
+YOUTUBE_RATING_LINK_REL = 'http://gdata.youtube.com/schemas/2007#video.ratings'
+YOUTUBE_COMPLAINT_CATEGORY_SCHEME = 'http://gdata.youtube.com/schemas/2007/complaint-reasons.cat'
+YOUTUBE_COMPLAINT_CATEGORY_TERMS = ('PORN', 'VIOLENCE', 'HATE', 'DANGEROUS', 
+                                   'RIGHTS', 'SPAM')
+YOUTUBE_CONTACT_STATUS = ('accepted', 'rejected')
+YOUTUBE_CONTACT_CATEGORY = ('Friends', 'Family')
+
+UNKOWN_ERROR=1000
+YOUTUBE_BAD_REQUEST=400
+YOUTUBE_CONFLICT=409
+YOUTUBE_INTERNAL_SERVER_ERROR=500
+YOUTUBE_INVALID_ARGUMENT=601
+YOUTUBE_INVALID_CONTENT_TYPE=602
+YOUTUBE_NOT_A_VIDEO=603
+YOUTUBE_INVALID_KIND=604
+
+class Error(Exception):
+  pass
+
+class RequestError(Error):
+  pass
+
+class YouTubeError(Error):
+  pass
+
+class YouTubeService(gdata.service.GDataService):
+  """Client for the YouTube service."""
+
+  def __init__(self, email=None, password=None, source=None,
+               server=YOUTUBE_SERVER, additional_headers=None, client_id=None,
+               developer_key=None):
+    if client_id and developer_key:
+      self.client_id = client_id
+      self.developer_key = developer_key
+      self.additional_headers = {'X-Gdata-Client': self.client_id,
+                                 'X-GData-Key': 'key=' + self.developer_key}
+      gdata.service.GDataService.__init__(
+          self, email=email, password=password,
+          service=YOUTUBE_SERVICE, source=source, server=server,
+          additional_headers=self.additional_headers)
+    elif developer_key and not client_id:
+      raise YouTubeError('You must also specify the clientId')
+    else:
+      gdata.service.GDataService.__init__(
+          self, email=email, password=password,
+          service=YOUTUBE_SERVICE, source=source, server=server,
+          additional_headers=additional_headers)
+
+  def GetYouTubeVideoFeed(self, uri):
+    return self.Get(uri, converter=gdata.youtube.YouTubeVideoFeedFromString)
+
+  def GetYouTubeVideoEntry(self, uri=None, video_id=None):
+    if not uri and not video_id:
+      raise YouTubeError('You must provide at least a uri or a video_id '
+                         'to the GetYouTubeVideoEntry() method')
+    elif video_id and not uri:
+      uri = YOUTUBE_VIDEO_URI + '/' + video_id
+
+    return self.Get(uri, converter=gdata.youtube.YouTubeVideoEntryFromString)
+
+  def GetYouTubeContactFeed(self, uri=None, username=None):
+    if not uri and not username:
+      raise YouTubeError('You must provide at least a uri or a username '
+                         'to the GetYouTubeContactFeed() method')
+    elif username and not uri:
+      uri = YOUTUBE_USER_FEED_URI + username + '/contacts'
+
+    return self.Get(uri, converter=gdata.youtube.YouTubeContactFeedFromString)
+
+  def GetYouTubeContactEntry(self, uri=None):
+    return self.Get(uri, converter=gdata.youtube.YouTubeContactEntryFromString)
+
+  def GetYouTubeVideoCommentFeed(self, uri=None, video_id=None):
+    if not uri and not video_id:
+      raise YouTubeError('You must provide at least a uri or a video_id '
+                         'to the GetYouTubeVideoCommentFeed() method')
+    elif video_id and not uri:
+      uri = YOUTUBE_VIDEO_URI + '/' + video_id + '/comments'
+
+    return self.Get(
+        uri,
+        converter=gdata.youtube.YouTubeVideoCommentFeedFromString)
+
+  def GetYouTubeVideoCommentEntry(self, uri):
+    return self.Get(
+        uri,
+        converter=gdata.youtube.YouTubeVideoCommentEntryFromString)
+
+  def GetYouTubeUserFeed(self, uri=None, username=None):
+    if not uri and not username:
+      raise YouTubeError('You must provide at least a uri or a username '
+                         'to the GetYouTubeUserFeed() method')
+    elif username and not uri:
+      uri = YOUTUBE_USER_FEED_URI + username + '/uploads'
+
+    return self.Get(uri, converter=gdata.youtube.YouTubeUserFeedFromString)
+
+  def GetYouTubeUserEntry(self, uri=None, username=None):
+    if not uri and not username:
+      raise YouTubeError('You must provide at least a uri or a username '
+                         'to the GetYouTubeUserEntry() method')
+    elif username and not uri:
+      uri = YOUTUBE_USER_FEED_URI + username
+
+    return self.Get(uri, converter=gdata.youtube.YouTubeUserEntryFromString)
+
+  def GetYouTubePlaylistFeed(self, uri=None, username=None):
+    if not uri and not username:
+      raise YouTubeError('You must provide at least a uri or a username '
+                         'to the GetYouTubePlaylistFeed() method')
+    elif username and not uri:
+      uri = YOUTUBE_USER_FEED_URI + username + '/playlists'
+
+    return self.Get(uri, converter=gdata.youtube.YouTubePlaylistFeedFromString)
+
+  def GetYouTubePlaylistEntry(self, uri):
+    return self.Get(uri, converter=gdata.youtube.YouTubePlaylistEntryFromString)
+
+  def GetYouTubePlaylistVideoFeed(self, uri=None, playlist_id=None):
+    if not uri and not playlist_id:
+      raise YouTubeError('You must provide at least a uri or a playlist_id '
+                         'to the GetYouTubePlaylistVideoFeed() method')
+    elif playlist_id and not uri:
+      uri = 'http://gdata.youtube.com/feeds/api/playlists/' + playlist_id
+
+    return self.Get(
+        uri,
+        converter=gdata.youtube.YouTubePlaylistVideoFeedFromString)
+
+  def GetYouTubeVideoResponseFeed(self, uri=None, video_id=None):
+    if not uri and not video_id:
+      raise YouTubeError('You must provide at least a uri or a video_id '
+                         'to the GetYouTubeVideoResponseFeed() method')
+    elif video_id and not uri:
+      uri = YOUTUBE_VIDEO_URI + '/' + video_id + '/responses'
+
+    return self.Get(uri,
+                    converter=gdata.youtube.YouTubeVideoResponseFeedFromString)
+
+  def GetYouTubeVideoResponseEntry(self, uri):
+    return self.Get(uri,
+                    converter=gdata.youtube.YouTubeVideoResponseEntryFromString)
+
+  def GetYouTubeSubscriptionFeed(self, uri=None, username=None):
+    if not uri and not username:
+      raise YouTubeError('You must provide at least a uri or a username '
+                         'to the GetYouTubeSubscriptionFeed() method')
+    elif username and not uri:
+      uri = ('http://gdata.youtube.com'
+             '/feeds/users/') + username + '/subscriptions'
+
+    return self.Get(
+        uri,
+        converter=gdata.youtube.YouTubeSubscriptionFeedFromString)
+
+  def GetYouTubeSubscriptionEntry(self, uri):
+    return self.Get(uri,
+                    converter=gdata.youtube.YouTubeSubscriptionEntryFromString)
+
+  def GetYouTubeRelatedVideoFeed(self, uri=None, video_id=None):
+    if not uri and not video_id:
+      raise YouTubeError('You must provide at least a uri or a video_id '
+                         'to the GetYouTubeRelatedVideoFeed() method')
+    elif video_id and not uri:
+      uri = YOUTUBE_VIDEO_URI + '/' + video_id + '/related'
+
+    return self.Get(uri,
+                    converter=gdata.youtube.YouTubeVideoFeedFromString)
+
+  def GetTopRatedVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_TOP_RATED_URI)
+
+  def GetMostViewedVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_VIEWED_URI)
+
+  def GetRecentlyFeaturedVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_RECENTLY_FEATURED_URI)
+
+  def GetWatchOnMobileVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_WATCH_ON_MOBILE_URI)
+
+  def GetTopFavoritesVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_TOP_FAVORITES_URI)
+
+  def GetMostRecentVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_RECENT_URI)
+
+  def GetMostDiscussedVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_DISCUSSED_URI)
+
+  def GetMostLinkedVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_LINKED_URI)
+
+  def GetMostRespondedVideoFeed(self):
+    return self.GetYouTubeVideoFeed(YOUTUBE_STANDARD_MOST_RESPONDED_URI)
+
+  def GetUserFavoritesFeed(self, username='default'):
+    return self.GetYouTubeVideoFeed('http://gdata.youtube.com/feeds/api/users/'
+                                    + username + '/favorites')
+
+  def InsertVideoEntry(self, video_entry, filename_or_handle,
+                       youtube_username='default',
+                       content_type='video/quicktime'):
+    """Upload a new video to YouTube using the direct upload mechanism
+
+    Needs authentication.
+
+    Arguments:
+      video_entry: The YouTubeVideoEntry to upload
+      filename_or_handle: A file-like object or file name where the video
+          will be read from
+      youtube_username: (optional) Username into whose account this video is
+          to be uploaded to. Defaults to the currently authenticated user.
+      content_type (optional): Internet media type (a.k.a. mime type) of
+          media object. Currently the YouTube API supports these types:
+            o video/mpeg
+            o video/quicktime
+            o video/x-msvideo
+            o video/mp4
+
+    Returns:
+      The newly created YouTubeVideoEntry or a YouTubeError
+
+    """
+
+    # check to make sure we have a valid video entry
+    try:
+      assert(isinstance(video_entry, gdata.youtube.YouTubeVideoEntry))
+    except AssertionError:
+      raise YouTubeError({'status':YOUTUBE_INVALID_ARGUMENT,
+          'body':'`video_entry` must be a gdata.youtube.VideoEntry instance',
+          'reason':'Found %s, not VideoEntry' % type(video_entry)
+          })
+
+    # check to make sure the MIME type is supported
+    try:
+      majtype, mintype = content_type.split('/')
+      assert(mintype in YOUTUBE_SUPPORTED_UPLOAD_TYPES)
+    except (ValueError, AssertionError):
+      raise YouTubeError({'status':YOUTUBE_INVALID_CONTENT_TYPE,
+          'body':'This is not a valid content type: %s' % content_type,
+          'reason':'Accepted content types: %s' %
+              ['video/' + t for t in YOUTUBE_SUPPORTED_UPLOAD_TYPES]
+          })
+    # check that the video file is valid and readable
+    if (isinstance(filename_or_handle, (str, unicode)) 
+        and os.path.exists(filename_or_handle)):
+      mediasource = gdata.MediaSource()
+      mediasource.setFile(filename_or_handle, content_type)
+    elif hasattr(filename_or_handle, 'read'):
+      if hasattr(filename_or_handle, 'seek'):
+        filename_or_handle.seek(0)
+      file_handle = StringIO.StringIO(filename_or_handle.read())
+      name = 'video'
+      if hasattr(filename_or_handle, 'name'):
+        name = filename_or_handle.name
+      mediasource = gdata.MediaSource(file_handle, content_type,
+          content_length=file_handle.len, file_name=name)
+    else:
+      raise YouTubeError({'status':YOUTUBE_INVALID_ARGUMENT, 'body':
+          '`filename_or_handle` must be a path name or a file-like object',
+          'reason': ('Found %s, not path name or object '
+                     'with a .read() method' % type(filename_or_handle))})
+
+    upload_uri = ('http://uploads.gdata.youtube.com/feeds/api/users/' + 
+                  youtube_username + '/uploads')
+
+    self.additional_headers['Slug'] = mediasource.file_name
+
+    # post the video file
+    try:
+      try:
+        return self.Post(video_entry, uri=upload_uri, media_source=mediasource,
+                         converter=gdata.youtube.YouTubeVideoEntryFromString)
+      except gdata.service.RequestError, e:
+        raise YouTubeError(e.args[0])
+    finally:
+      del(self.additional_headers['Slug'])
+
+  def CheckUploadStatus(self, video_entry=None, video_id=None):
+    """Check upload status on a recently uploaded video entry
+
+    Needs authentication.
+
+    Arguments:
+      video_entry: (optional) The YouTubeVideoEntry to upload
+      video_id: (optional) The videoId of a recently uploaded entry. One of
+          these two arguments will need to be present.
+
+    Returns:
+      A tuple containing (video_upload_state, detailed_message) or None if
+          no status information is found.
+    """
+    if not video_entry and not video_id:
+      raise YouTubeError('You must provide at least a uri or a video_id '
+                         'to the CheckUploadStatus() method')
+    elif video_id and not video_entry:
+       video_entry = self.GetYouTubeVideoEntry(video_id=video_id)
+
+    control = video_entry.control
+    if control is not None:
+      draft = control.draft
+      if draft is not None:
+        if draft.text == 'yes':
+          yt_state = control.extension_elements[0]
+          if yt_state is not None:
+            state_value = yt_state.attributes['name']
+            message = ''
+            if yt_state.text is not None:
+              message = yt_state.text
+
+            return (state_value, message)
+
+  def GetFormUploadToken(self, video_entry, uri=YOUTUBE_UPLOAD_TOKEN_URI):
+    """Receives a YouTube Token and a YouTube PostUrl with which to construct
+    the HTML Upload form for browser-based video uploads
+
+    Needs authentication.
+
+    Arguments:
+        video_entry: The YouTubeVideoEntry to upload (meta-data only)
+        uri: (optional) A url from where to fetch the token information
+
+    Returns:
+        A tuple containing (post_url, youtube_token)
+    """
+    response = self.Post(video_entry, uri)
+    tree = ElementTree.fromstring(response)
+
+    for child in tree:
+      if child.tag == 'url':
+        post_url = child.text
+      elif child.tag == 'token':
+        youtube_token = child.text
+
+    return (post_url, youtube_token)
+
+  def UpdateVideoEntry(self, video_entry):
+    """Updates a video entry's meta-data
+
+    Needs authentication.
+
+    Arguments:
+        video_entry: The YouTubeVideoEntry to update, containing updated 
+            meta-data
+
+    Returns:
+        An updated YouTubeVideoEntry on success or None
+    """
+    for link in video_entry.link:
+      if link.rel == 'edit':
+        edit_uri = link.href
+
+    return self.Put(video_entry, uri=edit_uri,
+                    converter=gdata.youtube.YouTubeVideoEntryFromString)
+
+  def DeleteVideoEntry(self, video_entry):
+    """Deletes a video entry
+
+    Needs authentication.
+
+    Arguments:
+        video_entry: The YouTubeVideoEntry to be deleted
+
+    Returns:
+        True if entry was deleted successfully
+    """
+    for link in video_entry.link:
+      if link.rel == 'edit':
+        edit_uri = link.href
+
+    return self.Delete(edit_uri)
+
+  def AddRating(self, rating_value, video_entry):
+    """Add a rating to a video entry
+
+    Needs authentication.
+
+    Arguments:
+        rating_value: The value for the rating (between 1 and 5)
+        video_entry: The YouTubeVideoEntry to be rated
+
+    Returns:
+      True if the rating was added successfully
+    """
+
+    if rating_value < 1 or rating_value > 5:
+      raise YouTubeError('AddRating: rating_value must be between 1 and 5')
+
+    entry = gdata.GDataEntry()
+    rating = gdata.youtube.Rating(min='1', max='5')
+    rating.extension_attributes['name'] = 'value'
+    rating.extension_attributes['value'] = str(rating_value)
+    entry.extension_elements.append(rating)
+
+    for link in video_entry.link:
+      if link.rel == YOUTUBE_RATING_LINK_REL:
+        rating_uri = link.href
+
+    return self.Post(entry, uri=rating_uri)
+
+  def AddComment(self, comment_text, video_entry):
+    """Add a comment to a video entry
+
+    Needs authentication.
+
+    Arguments:
+        comment_text: The text of the comment
+        video_entry: The YouTubeVideoEntry to be commented on
+
+    Returns:
+      True if the comment was added successfully
+    """
+    content = atom.Content(text=comment_text)
+    comment_entry = gdata.youtube.YouTubeVideoCommentEntry(content=content)
+    comment_post_uri = video_entry.comments.feed_link[0].href
+
+    return self.Post(comment_entry, uri=comment_post_uri)
+
+  def AddVideoResponse(self, video_id_to_respond_to, video_response):
+    """Add a video response
+
+    Needs authentication.
+
+    Arguments:
+        video_id_to_respond_to: Id of the YouTubeVideoEntry to be responded to
+        video_response: YouTubeVideoEntry to be posted as a response
+
+    Returns:
+        True if video response was posted successfully
+    """
+    post_uri = YOUTUBE_VIDEO_URI + '/' + video_id_to_respond_to + '/responses'
+    return self.Post(video_response, uri=post_uri)
+
+  def DeleteVideoResponse(self, video_id, response_video_id):
+    """Delete a video response
+
+    Needs authentication.
+
+    Arguments:
+        video_id: Id of YouTubeVideoEntry that contains the response
+        response_video_id: Id of the YouTubeVideoEntry posted as response
+
+    Returns:
+        True if video response was deleted succcessfully
+    """
+    delete_uri = (YOUTUBE_VIDEO_URI + '/' + video_id + 
+                  '/responses/' + response_video_id)
+
+    return self.Delete(delete_uri)
+
+  def AddComplaint(self, complaint_text, complaint_term, video_id):
+    """Add a complaint for a particular video entry
+
+    Needs authentication.
+
+    Arguments:
+        complaint_text: Text explaining the complaint
+        complaint_term: Complaint category term
+        video_id: Id of YouTubeVideoEntry to complain about
+
+    Returns:
+        True if posted successfully
+    """
+    if complaint_term not in YOUTUBE_COMPLAINT_CATEGORY_TERMS:
+      raise YouTubeError('Your complaint must be a valid term')
+
+    content = atom.Content(text=complaint_text)
+    category = atom.Category(term=complaint_term,
+                             scheme=YOUTUBE_COMPLAINT_CATEGORY_SCHEME)
+
+    complaint_entry = gdata.GDataEntry(content=content, category=[category])
+    post_uri = YOUTUBE_VIDEO_URI + '/' + video_id + '/complaints'
+
+    return self.Post(complaint_entry, post_uri)
+
+  def AddVideoEntryToFavorites(self, video_entry, username='default'):
+    """Add a video entry to a users favorite feed
+
+    Needs authentication.
+
+    Arguments:
+        video_entry: The YouTubeVideoEntry to add
+        username: (optional) The username to whose favorite feed you wish to
+            add the entry. Your client must be authenticated to the username's
+            account.
+    Returns:
+        A GDataEntry if posted successfully
+    """
+    post_uri = ('http://gdata.youtube.com/feeds/api/users/' + 
+                username + '/favorites')
+
+    return self.Post(video_entry, post_uri)
+
+  def DeleteVideoEntryFromFavorites(self, video_id, username='default'):
+    """Delete a video entry from the users favorite feed
+
+    Needs authentication.
+
+    Arguments:
+        video_id: The Id for the YouTubeVideoEntry to be removed
+        username: (optional) The username of the user's favorite feed. Defaults
+            to the currently authenticated user.
+
+    Returns:
+        True if entry was successfully deleted
+    """
+    edit_link = YOUTUBE_USER_FEED_URI + username + '/favorites/' + video_id
+    return self.Delete(edit_link)
+
+  def AddPlaylist(self, playlist_title, playlist_description, 
+                  playlist_private=None):
+    """Add a new playlist to the currently authenticated users account
+
+    Needs authentication
+
+    Arguments:
+        playlist_title: The title for the new playlist
+        playlist_description: The description for the playlist
+        playlist_private: (optiona) Submit as True if the playlist is to be
+            private
+    Returns:
+        A new YouTubePlaylistEntry if successfully posted
+    """
+    playlist_entry = gdata.youtube.YouTubePlaylistEntry(
+        title=atom.Title(text=playlist_title),
+        description=gdata.youtube.Description(text=playlist_description))
+    if playlist_private:
+      playlist_entry.private = gdata.youtube.Private()
+
+    playlist_post_uri = YOUTUBE_USER_FEED_URI + 'default/playlists'
+    return self.Post(playlist_entry, playlist_post_uri,
+                     converter=gdata.youtube.YouTubePlaylistEntryFromString)
+
+  def DeletePlaylist(self, playlist_uri):
+    """Delete a playlist from the currently authenticated users playlists
+
+    Needs authentication
+
+    Arguments:
+        playlist_uri: The uri of the playlist to delete
+
+    Returns:
+        True if successfully deleted
+    """
+    return self.Delete(playlist_uri)
+
+  def AddPlaylistVideoEntryToPlaylist(self, playlist_uri, video_id, 
+                                      custom_video_title=None,
+                                      custom_video_description=None):
+    """Add a video entry to a playlist, optionally providing a custom title
+    and description
+
+    Needs authentication
+
+    Arguments:
+        playlist_uri: Uri of playlist to add this video to.
+        video_id: Id of the video entry to add
+        custom_video_title: (optional) Custom title for the video
+        custom_video_description: (optional) Custom video description
+
+    Returns:
+        A YouTubePlaylistVideoEntry if successfully posted
+    """
+
+    playlist_video_entry = gdata.youtube.YouTubePlaylistVideoEntry(
+        atom_id=atom.Id(text=video_id))
+    if custom_video_title:
+      playlist_video_entry.title = atom.Title(text=custom_video_title)
+    if custom_video_description:
+      playlist_video_entry.description = gdata.youtube.Description(
+          text=custom_video_description)
+    return self.Post(playlist_video_entry, playlist_uri,
+                    converter=gdata.youtube.YouTubePlaylistVideoEntryFromString)
+
+  def UpdatePlaylistVideoEntryMetaData(self, playlist_uri, playlist_entry_id,
+                                       new_video_title,
+                                       new_video_description,
+                                       new_video_position):
+    """Update the meta data for a YouTubePlaylistVideoEntry
+
+    Needs authentication
+
+    Arguments:
+        playlist_uri: Uri of the playlist that contains the entry to be updated
+        playlist_entry_id: Id of the entry to be updated
+        new_video_title: New title for the video entry
+        new_video_description: New description for the video entry
+        new_video_position: New position for the video
+
+    Returns:
+        A YouTubePlaylistVideoEntry if the update was successful
+    """
+    playlist_video_entry = gdata.youtube.YouTubePlaylistVideoEntry(
+        title=atom.Title(text=new_video_title),
+        description=gdata.youtube.Description(text=new_video_description),
+        position=gdata.youtube.Position(text=str(new_video_position)))
+
+    playlist_put_uri = playlist_uri + '/' + playlist_entry_id
+
+    return self.Put(playlist_video_entry, playlist_put_uri, 
+                    converter=gdata.youtube.YouTubePlaylistVideoEntryFromString)
+
+
+  def AddSubscriptionToChannel(self, username):
+    """Add a new channel subscription to the currently authenticated users 
+    account
+
+    Needs authentication
+
+    Arguments:
+        username: The username of the channel to subscribe to.
+
+    Returns:
+        A new YouTubeSubscriptionEntry if successfully posted
+    """
+    subscription_category = atom.Category(
+        scheme='http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat',
+        term='channel')
+    subscription_username = gdata.youtube.Username(text=username)
+
+    subscription_entry = gdata.youtube.YouTubeSubscriptionEntry(
+        category=subscription_category,
+        username=subscription_username)
+
+    post_uri = YOUTUBE_USER_FEED_URI + 'default/subscriptions'
+    return self.Post(subscription_entry, post_uri,
+                     converter=gdata.youtube.YouTubeSubscriptionEntryFromString)
+
+  def AddSubscriptionToFavorites(self, username):
+    """Add a new subscription to a users favorites to the currently
+    authenticated user's account
+
+    Needs authentication
+
+    Arguments:
+        username: The username of the users favorite feed to subscribe to
+
+    Returns:
+        A new YouTubeSubscriptionEntry if successful
+    """
+    subscription_category = atom.Category(
+        scheme='http://gdata.youtube.com/schemas/2007/subscriptiontypes.cat',
+        term='favorites')
+    subscription_username = gdata.youtube.Username(text=username)
+
+    subscription_entry = gdata.youtube.YouTubeSubscriptionEntry(
+        category=subscription_category,
+        username=subscription_username)
+
+    post_uri = YOUTUBE_USER_FEED_URI + 'default/subscriptions'
+    return self.Post(subscription_entry, post_uri,
+                     converter=gdata.youtube.YouTubeSubscriptionEntryFromString)
+
+  def DeleteSubscription(self, subscription_uri):
+    """Delete a subscription from the currently authenticated user's account
+
+    Needs authentication
+
+    Arguments:
+        subscription_uri: The uri of a subscription
+
+    Returns:
+        True if successfully deleted
+    """
+    return self.Delete(subscription_uri)
+
+  def AddContact(self, contact_username, my_username='default'):
+    """Add a new contact to the currently authenticated user's contact feed.
+
+    Needs authentication
+
+    Arguments:
+        contact_username: The username of the contact that you wish to add
+        my_username: (optional) The username of the contact feed
+
+    Returns:
+        A YouTubeContactEntry if added successfully
+    """
+    contact_category = atom.Category(
+        scheme = 'http://gdata.youtube.com/schemas/2007/contact.cat',
+        term = 'Friends')
+    contact_username = gdata.youtube.Username(text=contact_username)
+    contact_entry = gdata.youtube.YouTubeContactEntry(
+        category=contact_category,
+        username=contact_username)
+    contact_post_uri = YOUTUBE_USER_FEED_URI + my_username + '/contacts'
+    return self.Post(contact_entry, contact_post_uri,
+                     converter=gdata.youtube.YouTubeContactEntryFromString)
+
+  def UpdateContact(self, contact_username, new_contact_status, 
+                    new_contact_category, my_username='default'):
+    """Update a contact, providing a new status and a new category
+
+    Needs authentication
+
+    Arguments:
+        contact_username: The username of the contact to be updated
+        new_contact_status: New status, either 'accepted' or 'rejected'
+        new_contact_category: New category for the contact, either 'Friends' or
+            'Family'
+        my_username: (optional) Username of the user whose contact feed we are 
+            modifying. Defaults to the currently authenticated user
+
+    Returns:
+        A YouTubeContactEntry if updated succesfully
+    """
+    if new_contact_status not in YOUTUBE_CONTACT_STATUS:
+      raise YouTubeError('New contact status must be one of ' +
+                         ' '.join(YOUTUBE_CONTACT_STATUS))
+    if new_contact_category not in YOUTUBE_CONTACT_CATEGORY:
+      raise YouTubeError('New contact category must be one of ' +
+                         ' '.join(YOUTUBE_CONTACT_CATEGORY))
+
+    contact_category = atom.Category(
+        scheme='http://gdata.youtube.com/schemas/2007/contact.cat',
+        term=new_contact_category)
+    contact_status = gdata.youtube.Status(text=new_contact_status)
+    contact_entry = gdata.youtube.YouTubeContactEntry(
+        category=contact_category,
+        status=contact_status)
+    contact_put_uri = (YOUTUBE_USER_FEED_URI + my_username + '/contacts/' +
+                       contact_id)
+    return self.Put(contact_entry, contact_put_uri,
+                    converter=gdata.youtube.YouTubeContactEntryFromString)
+
+  def DeleteContact(self, contact_username, my_username='default'):
+    """Delete a contact from a users contact feed
+
+    Needs authentication
+
+    Arguments:
+        contact_username: Username of the contact to be deleted
+        my_username: (optional) Username of the users contact feed that is to 
+            be modified. Defaults to the currently authenticated user
+
+    Returns:
+        True if the contact was deleted successfully
+    """
+    contact_edit_uri = (YOUTUBE_USER_FEED_URI + my_username +
+                        '/contacts/' + contact_username)
+    return self.Delete(contact_edit_uri)
+
+
+  def _GetDeveloperKey(self):
+    """Getter for Developer Key property"""
+    if '_developer_key' in self.keys():
+      return self._developer_key
+    else:
+      return None
+
+  def _SetDeveloperKey(self, developer_key):
+    """Setter for Developer Key property"""
+    self._developer_key = developer_key
+    self.additional_headers['X-GData-Key'] = 'key=' + developer_key
+
+  developer_key = property(_GetDeveloperKey, _SetDeveloperKey,
+                           doc="""The Developer Key property""")
+
+  def _GetClientId(self):
+    """Getter for Client Id property"""
+    if '_client_id' in self.keys():
+      return self._client_id
+    else:
+      return None
+
+  def _SetClientId(self, client_id):
+    """Setter for Client Id property"""
+    self._client_id = client_id
+    self.additional_headers['X-Gdata-Client'] = client_id
+
+  client_id = property(_GetClientId, _SetClientId,
+                         doc="""The ClientId property""")
+
+  def Query(self, uri):
+    """Performs a query and returns a resulting feed or entry.
+
+    Args:
+      feed: string The feed which is to be queried
+
+    Returns:
+      On success, a tuple in the form
+      (boolean succeeded=True, ElementTree._Element result)
+      On failure, a tuple in the form
+      (boolean succeeded=False, {'status': HTTP status code from server, 
+                                 'reason': HTTP reason from the server, 
+                                 'body': HTTP body of the server's response})
+    """
+
+    result = self.Get(uri)
+    return result
+
+  def YouTubeQuery(self, query):
+    result = self.Query(query.ToUri())
+    if isinstance(query, YouTubeVideoQuery):
+      return gdata.youtube.YouTubeVideoFeedFromString(result.ToString())
+    elif isinstance(query, YouTubeUserQuery):
+      return gdata.youtube.YouTubeUserFeedFromString(result.ToString())
+    elif isinstance(query, YouTubePlaylistQuery):
+      return gdata.youtube.YouTubePlaylistFeedFromString(result.ToString())
+    else:
+      return result
+
+class YouTubeVideoQuery(gdata.service.Query):
+
+  def __init__(self, video_id=None, feed_type=None, text_query=None,
+               params=None, categories=None):
+
+    if feed_type in YOUTUBE_STANDARDFEEDS:
+      feed = 'http://%s/feeds/standardfeeds/%s' % (YOUTUBE_SERVER, feed_type)
+    elif feed_type is 'responses' or feed_type is 'comments' and video_id:
+      feed = 'http://%s/feeds/videos/%s/%s' % (YOUTUBE_SERVER, video_id,
+                                               feed_type)
+    else:
+      feed = 'http://%s/feeds/videos' % (YOUTUBE_SERVER)
+
+    gdata.service.Query.__init__(self, feed, text_query=text_query,
+                                 params=params, categories=categories)
+
+  def _GetStartMin(self):
+    if 'start-min' in self.keys():
+      return self['start-min']
+    else:
+      return None
+
+  def _SetStartMin(self, val):
+    self['start-min'] = val
+
+  start_min = property(_GetStartMin, _SetStartMin,
+                       doc="""The start-min query parameter""")
+
+  def _GetStartMax(self):
+    if 'start-max' in self.keys():
+      return self['start-max']
+    else:
+      return None
+
+  def _SetStartMax(self, val):
+    self['start-max'] = val
+
+  start_max = property(_GetStartMax, _SetStartMax,
+                       doc="""The start-max query parameter""")
+
+  def _GetVideoQuery(self):
+    if 'vq' in self.keys():
+      return self['vq']
+    else:
+      return None
+
+  def _SetVideoQuery(self, val):
+    self['vq'] = val
+
+  vq = property(_GetVideoQuery, _SetVideoQuery,
+                doc="""The video query (vq) query parameter""")
+
+  def _GetOrderBy(self):
+    if 'orderby' in self.keys():
+      return self['orderby']
+    else:
+      return None
+
+  def _SetOrderBy(self, val):
+    if val not in YOUTUBE_QUERY_VALID_ORDERBY_PARAMETERS:
+      raise YouTubeError('OrderBy must be one of: %s ' %
+                         ' '.join(YOUTUBE_QUERY_VALID_ORDERBY_PARAMETERS))
+    self['orderby'] = val
+
+  orderby = property(_GetOrderBy, _SetOrderBy,
+                     doc="""The orderby query parameter""")
+
+  def _GetTime(self):
+    if 'time' in self.keys():
+      return self['time']
+    else:
+      return None
+
+  def _SetTime(self, val):
+    if val not in YOUTUBE_QUERY_VALID_TIME_PARAMETERS:
+      raise YouTubeError('Time must be one of: %s ' % 
+                         ' '.join(YOUTUBE_QUERY_VALID_TIME_PARAMETERS))
+    self['time'] = val
+
+  time = property(_GetTime, _SetTime,
+                  doc="""The time query parameter""")
+
+  def _GetFormat(self):
+    if 'format' in self.keys():
+      return self['format']
+    else:
+      return None
+
+  def _SetFormat(self, val):
+    if val not in YOUTUBE_QUERY_VALID_FORMAT_PARAMETERS:
+      raise YouTubeError('Format must be one of: %s ' % 
+                         ' '.join(YOUTUBE_QUERY_VALID_FORMAT_PARAMETERS))
+    self['format'] = val
+
+  format = property(_GetFormat, _SetFormat,
+                    doc="""The format query parameter""")
+
+  def _GetRacy(self):
+    if 'racy' in self.keys():
+      return self['racy']
+    else:
+      return None
+
+  def _SetRacy(self, val):
+    if val not in YOUTUBE_QUERY_VALID_RACY_PARAMETERS:
+      raise YouTubeError('Racy must be one of: %s ' % 
+                         ' '.join(YOUTUBE_QUERY_VALID_RACY_PARAMETERS))
+    self['racy'] = val
+
+  racy = property(_GetRacy, _SetRacy, 
+                  doc="""The racy query parameter""")
+
+class YouTubeUserQuery(YouTubeVideoQuery):
+
+  def __init__(self, username=None, feed_type=None, subscription_id=None,
+               text_query=None, params=None, categories=None):
+
+    uploads_favorites_playlists = ('uploads', 'favorites', 'playlists')
+
+    if feed_type is 'subscriptions' and subscription_id and username:
+      feed = "http://%s/feeds/users/%s/%s/%s"; % (
+          YOUTUBE_SERVER, username, feed_type, subscription_id)
+    elif feed_type is 'subscriptions' and not subscription_id and username:
+      feed = "http://%s/feeds/users/%s/%s"; % (
+          YOUTUBE_SERVER, username, feed_type)
+    elif feed_type in uploads_favorites_playlists:
+      feed = "http://%s/feeds/users/%s/%s"; % (
+          YOUTUBE_SERVER, username, feed_type)
+    else:
+      feed = "http://%s/feeds/users"; % (YOUTUBE_SERVER)
+
+    YouTubeVideoQuery.__init__(self, feed, text_query=text_query,
+                               params=params, categories=categories)
+
+
+class YouTubePlaylistQuery(YouTubeVideoQuery):
+
+  def __init__(self, playlist_id, text_query=None, params=None,
+               categories=None):
+    if playlist_id:
+      feed = "http://%s/feeds/playlists/%s"; % (YOUTUBE_SERVER, playlist_id)
+    else:
+      feed = "http://%s/feeds/playlists"; % (YOUTUBE_SERVER)
+
+    YouTubeVideoQuery.__init__(self, feed, text_query=text_query,
+                               params=params, categories=categories)

Modified: trunk/conduit/modules/GoogleModule/youtube-config.glade
==============================================================================
--- trunk/conduit/modules/GoogleModule/youtube-config.glade	(original)
+++ trunk/conduit/modules/GoogleModule/youtube-config.glade	Sat Jun  7 07:56:04 2008
@@ -1,423 +1,215 @@
-<?xml version="1.0" standalone="no"?> <!--*- mode: xml -*-->
-<!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd";>
-
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--*- mode: xml -*-->
 <glade-interface>
-
-<widget class="GtkDialog" id="YouTubeSourceConfigDialog">
-  <property name="border_width">5</property>
-  <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-  <property name="title" translatable="yes">YouTube Source</property>
-  <property name="type">GTK_WINDOW_TOPLEVEL</property>
-  <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
-  <property name="modal">False</property>
-  <property name="resizable">False</property>
-  <property name="destroy_with_parent">False</property>
-  <property name="decorated">True</property>
-  <property name="skip_taskbar_hint">False</property>
-  <property name="skip_pager_hint">False</property>
-  <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
-  <property name="gravity">GDK_GRAVITY_NORTH_WEST</property>
-  <property name="focus_on_map">True</property>
-  <property name="urgency_hint">False</property>
-  <property name="has_separator">True</property>
-
-  <child internal-child="vbox">
-    <widget class="GtkVBox" id="dialog-vbox1">
-      <property name="visible">True</property>
-      <property name="homogeneous">False</property>
-      <property name="spacing">2</property>
-
-      <child internal-child="action_area">
-	<widget class="GtkHButtonBox" id="dialog-action_area1">
-	  <property name="visible">True</property>
-	  <property name="layout_style">GTK_BUTTONBOX_END</property>
-
-	  <child>
-	    <placeholder/>
-	  </child>
-	  
-	  <child>
-	    <widget class="GtkButton" id="button1">
-	      <property name="visible">True</property>
-	      <property name="can_focus">True</property>
-	      <property name="label">gtk-cancel</property>
-	      <property name="use_stock">True</property>
-	      <property name="relief">GTK_RELIEF_NORMAL</property>
-	      <property name="focus_on_click">True</property>
-	      <property name="response_id">-6</property>
-	    </widget>
-	  </child>
-
-	  <child>
-	    <widget class="GtkButton" id="button2">
-	      <property name="visible">True</property>
-	      <property name="can_focus">True</property>
-	      <property name="label">gtk-ok</property>
-	      <property name="use_stock">True</property>
-	      <property name="relief">GTK_RELIEF_NORMAL</property>
-	      <property name="focus_on_click">True</property>
-	      <property name="response_id">-5</property>
-	    </widget>
-	  </child>
-
-	</widget>
-	<packing>
-	  <property name="padding">0</property>
-	  <property name="expand">False</property>
-	  <property name="fill">True</property>
-	  <property name="pack_type">GTK_PACK_END</property>
-	</packing>
-      </child>
-
-      <child>
-	<widget class="GtkTable" id="table1">
-	  <property name="visible">True</property>
-	  <property name="n_rows">2</property>
-	  <property name="n_columns">2</property>
-	  <property name="homogeneous">False</property>
-	  <property name="row_spacing">5</property>
-	  <property name="column_spacing">5</property>
-
-	  <child>
-	    <widget class="GtkSpinButton" id="maxdownloads">
-	      <property name="visible">True</property>
-	      <property name="can_focus">True</property>
-	      <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-	      <property name="climb_rate">1</property>
-	      <property name="digits">0</property>
-	      <property name="numeric">False</property>
-	      <property name="update_policy">GTK_UPDATE_ALWAYS</property>
-	      <property name="snap_to_ticks">False</property>
-	      <property name="wrap">False</property>
-	      <property name="adjustment">0 0 100 1 10 10</property>
-	    </widget>
-	    <packing>
-	      <property name="left_attach">1</property>
-	      <property name="right_attach">2</property>
-	      <property name="top_attach">1</property>
-	      <property name="bottom_attach">2</property>
-	    </packing>
-	  </child>
-
-	  <child>
-	    <widget class="GtkLabel" id="label3">
-	      <property name="visible">True</property>
-	      <property name="label" translatable="yes">Max retrieved videos (0 is unlimited):</property>
-	      <property name="use_underline">False</property>
-	      <property name="use_markup">False</property>
-	      <property name="justify">GTK_JUSTIFY_LEFT</property>
-	      <property name="wrap">False</property>
-	      <property name="selectable">False</property>
-	      <property name="xalign">0.5</property>
-	      <property name="yalign">0.5</property>
-	      <property name="xpad">0</property>
-	      <property name="ypad">0</property>
-	      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
-	      <property name="width_chars">-1</property>
-	      <property name="single_line_mode">False</property>
-	      <property name="angle">0</property>
-	    </widget>
-	    <packing>
-	      <property name="left_attach">0</property>
-	      <property name="right_attach">1</property>
-	      <property name="top_attach">1</property>
-	      <property name="bottom_attach">2</property>
-	    </packing>
-	  </child>
-
-	  <child>
-	    <widget class="GtkFrame" id="frame1">
-	      <property name="visible">True</property>
-	      <property name="label_xalign">0</property>
-	      <property name="label_yalign">0.5</property>
-	      <property name="shadow_type">GTK_SHADOW_NONE</property>
-
-	      <child>
-		<widget class="GtkAlignment" id="alignment1">
-		  <property name="visible">True</property>
-		  <property name="xalign">0.5</property>
-		  <property name="yalign">0.5</property>
-		  <property name="xscale">1</property>
-		  <property name="yscale">1</property>
-		  <property name="top_padding">0</property>
-		  <property name="bottom_padding">0</property>
-		  <property name="left_padding">12</property>
-		  <property name="right_padding">0</property>
-
-		  <child>
-		    <widget class="GtkVBox" id="vbox1">
-		      <property name="visible">True</property>
-		      <property name="homogeneous">False</property>
-		      <property name="spacing">5</property>
-
-		      <child>
-			<widget class="GtkRadioButton" id="mostviewed">
-			  <property name="visible">True</property>
-			  <property name="can_focus">True</property>
-			  <property name="label" translatable="yes">Most viewed</property>
-			  <property name="use_underline">True</property>
-			  <property name="relief">GTK_RELIEF_NORMAL</property>
-			  <property name="focus_on_click">True</property>
-			  <property name="active">True</property>
-			  <property name="inconsistent">False</property>
-			  <property name="draw_indicator">True</property>
-			</widget>
-			<packing>
-			  <property name="padding">0</property>
-			  <property name="expand">True</property>
-			  <property name="fill">True</property>
-			</packing>
-		      </child>
-
-		      <child>
-			<widget class="GtkRadioButton" id="toprated">
-			  <property name="visible">True</property>
-			  <property name="can_focus">True</property>
-			  <property name="label" translatable="yes">Top rated</property>
-			  <property name="use_underline">True</property>
-			  <property name="relief">GTK_RELIEF_NORMAL</property>
-			  <property name="focus_on_click">True</property>
-			  <property name="active">False</property>
-			  <property name="inconsistent">False</property>
-			  <property name="draw_indicator">True</property>
-			  <property name="group">mostviewed</property>
-			</widget>
-			<packing>
-			  <property name="padding">0</property>
-			  <property name="expand">True</property>
-			  <property name="fill">True</property>
-			</packing>
-		      </child>
-
-		      <child>
-			<widget class="GtkVBox" id="vbox3">
-			  <property name="visible">True</property>
-			  <property name="homogeneous">False</property>
-			  <property name="spacing">5</property>
-
-			  <child>
-			    <widget class="GtkRadioButton" id="byuser">
-			      <property name="visible">True</property>
-			      <property name="can_focus">True</property>
-			      <property name="label" translatable="yes">By user</property>
-			      <property name="use_underline">True</property>
-			      <property name="relief">GTK_RELIEF_NORMAL</property>
-			      <property name="focus_on_click">True</property>
-			      <property name="active">False</property>
-			      <property name="inconsistent">False</property>
-			      <property name="draw_indicator">True</property>
-			      <property name="group">mostviewed</property>
-			    </widget>
-			    <packing>
-			      <property name="padding">0</property>
-			      <property name="expand">True</property>
-			      <property name="fill">True</property>
-			    </packing>
-			  </child>
-
-			  <child>
-			    <widget class="GtkFrame" id="frame">
-			      <property name="visible">True</property>
-			      <property name="sensitive">False</property>
-			      <property name="label_xalign">0</property>
-			      <property name="label_yalign">0.5</property>
-			      <property name="shadow_type">GTK_SHADOW_NONE</property>
-
-			      <child>
-				<widget class="GtkAlignment" id="alignment2">
-				  <property name="visible">True</property>
-				  <property name="xalign">0.5</property>
-				  <property name="yalign">0.5</property>
-				  <property name="xscale">1</property>
-				  <property name="yscale">1</property>
-				  <property name="top_padding">0</property>
-				  <property name="bottom_padding">0</property>
-				  <property name="left_padding">12</property>
-				  <property name="right_padding">0</property>
-
-				  <child>
-				    <widget class="GtkTable" id="table2">
-				      <property name="visible">True</property>
-				      <property name="n_rows">2</property>
-				      <property name="n_columns">2</property>
-				      <property name="homogeneous">False</property>
-				      <property name="row_spacing">5</property>
-				      <property name="column_spacing">5</property>
-
-				      <child>
-					<widget class="GtkHBox" id="hbox2">
-					  <property name="visible">True</property>
-					  <property name="homogeneous">False</property>
-					  <property name="spacing">0</property>
-
-					  <child>
-					    <widget class="GtkRadioButton" id="uploadedby">
-					      <property name="visible">True</property>
-					      <property name="can_focus">True</property>
-					      <property name="label" translatable="yes">Uploaded by</property>
-					      <property name="use_underline">True</property>
-					      <property name="relief">GTK_RELIEF_NORMAL</property>
-					      <property name="focus_on_click">True</property>
-					      <property name="active">True</property>
-					      <property name="inconsistent">False</property>
-					      <property name="draw_indicator">True</property>
-					    </widget>
-					    <packing>
-					      <property name="padding">0</property>
-					      <property name="expand">True</property>
-					      <property name="fill">True</property>
-					    </packing>
-					  </child>
-
-					  <child>
-					    <widget class="GtkRadioButton" id="favoritesof">
-					      <property name="visible">True</property>
-					      <property name="can_focus">True</property>
-					      <property name="label" translatable="yes">Favorites of</property>
-					      <property name="use_underline">True</property>
-					      <property name="relief">GTK_RELIEF_NORMAL</property>
-					      <property name="focus_on_click">True</property>
-					      <property name="active">False</property>
-					      <property name="inconsistent">False</property>
-					      <property name="draw_indicator">True</property>
-					      <property name="group">uploadedby</property>
-					    </widget>
-					    <packing>
-					      <property name="padding">0</property>
-					      <property name="expand">True</property>
-					      <property name="fill">True</property>
-					    </packing>
-					  </child>
-					</widget>
-					<packing>
-					  <property name="left_attach">1</property>
-					  <property name="right_attach">2</property>
-					  <property name="top_attach">0</property>
-					  <property name="bottom_attach">1</property>
-					</packing>
-				      </child>
-
-				      <child>
-					<widget class="GtkHBox" id="hbox1">
-					  <property name="visible">True</property>
-					  <property name="homogeneous">False</property>
-					  <property name="spacing">0</property>
-
-					  <child>
-					    <widget class="GtkLabel" id="label4">
-					      <property name="visible">True</property>
-					      <property name="label" translatable="yes">User: </property>
-					      <property name="use_underline">False</property>
-					      <property name="use_markup">False</property>
-					      <property name="justify">GTK_JUSTIFY_LEFT</property>
-					      <property name="wrap">False</property>
-					      <property name="selectable">False</property>
-					      <property name="xalign">0.5</property>
-					      <property name="yalign">0.5</property>
-					      <property name="xpad">0</property>
-					      <property name="ypad">0</property>
-					      <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
-					      <property name="width_chars">-1</property>
-					      <property name="single_line_mode">False</property>
-					      <property name="angle">0</property>
-					    </widget>
-					    <packing>
-					      <property name="padding">0</property>
-					      <property name="expand">False</property>
-					      <property name="fill">False</property>
-					    </packing>
-					  </child>
-
-					  <child>
-					    <widget class="GtkEntry" id="user">
-					      <property name="visible">True</property>
-					      <property name="can_focus">True</property>
-					      <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-					      <property name="editable">True</property>
-					      <property name="visibility">True</property>
-					      <property name="max_length">0</property>
-					      <property name="text" translatable="yes"></property>
-					      <property name="has_frame">True</property>
-					      <property name="invisible_char">â</property>
-					      <property name="activates_default">False</property>
-					    </widget>
-					    <packing>
-					      <property name="padding">0</property>
-					      <property name="expand">True</property>
-					      <property name="fill">True</property>
-					    </packing>
-					  </child>
-					</widget>
-					<packing>
-					  <property name="left_attach">1</property>
-					  <property name="right_attach">2</property>
-					  <property name="top_attach">1</property>
-					  <property name="bottom_attach">2</property>
-					  <property name="x_options"></property>
-					</packing>
-				      </child>
-				    </widget>
-				  </child>
-				</widget>
-			      </child>
-			    </widget>
-			    <packing>
-			      <property name="padding">0</property>
-			      <property name="expand">True</property>
-			      <property name="fill">True</property>
-			    </packing>
-			  </child>
-			</widget>
-			<packing>
-			  <property name="padding">0</property>
-			  <property name="expand">True</property>
-			  <property name="fill">True</property>
-			</packing>
-		      </child>
-		    </widget>
-		  </child>
-		</widget>
-	      </child>
-
-	      <child>
-		<widget class="GtkLabel" id="label1">
-		  <property name="visible">True</property>
-		  <property name="label" translatable="yes">&lt;b&gt;Donwload Videos&lt;/b&gt;</property>
-		  <property name="use_underline">False</property>
-		  <property name="use_markup">True</property>
-		  <property name="justify">GTK_JUSTIFY_LEFT</property>
-		  <property name="wrap">False</property>
-		  <property name="selectable">False</property>
-		  <property name="xalign">0.5</property>
-		  <property name="yalign">0.5</property>
-		  <property name="xpad">0</property>
-		  <property name="ypad">0</property>
-		  <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
-		  <property name="width_chars">-1</property>
-		  <property name="single_line_mode">False</property>
-		  <property name="angle">0</property>
-		</widget>
-		<packing>
-		  <property name="type">label_item</property>
-		</packing>
-	      </child>
-	    </widget>
-	    <packing>
-	      <property name="left_attach">0</property>
-	      <property name="right_attach">2</property>
-	      <property name="top_attach">0</property>
-	      <property name="bottom_attach">1</property>
-	    </packing>
-	  </child>
-	</widget>
-	<packing>
-	  <property name="padding">0</property>
-	  <property name="expand">True</property>
-	  <property name="fill">True</property>
-	</packing>
-      </child>
-    </widget>
-  </child>
-</widget>
-
+  <widget class="GtkDialog" id="YouTubeTwoWayConfigDialog">
+    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+    <property name="border_width">5</property>
+    <property name="title" translatable="yes">YouTube Source</property>
+    <property name="resizable">False</property>
+    <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="dialog-vbox1">
+        <property name="visible">True</property>
+        <property name="spacing">2</property>
+        <child>
+          <widget class="GtkVBox" id="vbox1">
+            <property name="visible">True</property>
+            <child>
+              <widget class="GtkLabel" id="label2">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">&lt;b&gt;Account Details&lt;/b&gt;</property>
+                <property name="use_markup">True</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label5">
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">Username:</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkEntry" id="username">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+              </widget>
+              <packing>
+                <property name="position">2</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label6">
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">Password:</property>
+                <property name="ellipsize">PANGO_ELLIPSIZE_START</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">3</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkEntry" id="password">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="visibility">False</property>
+              </widget>
+              <packing>
+                <property name="position">4</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="label" translatable="yes">&lt;b&gt;Download Videos&lt;/b&gt;</property>
+                <property name="use_markup">True</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">5</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkVBox" id="vbox2">
+                <property name="visible">True</property>
+                <property name="spacing">5</property>
+                <child>
+                  <widget class="GtkRadioButton" id="mostviewed">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="label" translatable="yes">Most viewed</property>
+                    <property name="use_underline">True</property>
+                    <property name="response_id">0</property>
+                    <property name="active">True</property>
+                    <property name="draw_indicator">True</property>
+                  </widget>
+                </child>
+                <child>
+                  <widget class="GtkRadioButton" id="toprated">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="label" translatable="yes">Top rated</property>
+                    <property name="use_underline">True</property>
+                    <property name="response_id">0</property>
+                    <property name="draw_indicator">True</property>
+                    <property name="group">mostviewed</property>
+                  </widget>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkRadioButton" id="uploadedby">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="label" translatable="yes">Uploaded by above user</property>
+                    <property name="use_underline">True</property>
+                    <property name="response_id">0</property>
+                    <property name="draw_indicator">True</property>
+                    <property name="group">mostviewed</property>
+                  </widget>
+                  <packing>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <widget class="GtkRadioButton" id="favoritesof">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="label" translatable="yes">Favorites of above user</property>
+                    <property name="response_id">0</property>
+                    <property name="active">True</property>
+                    <property name="draw_indicator">True</property>
+                    <property name="group">mostviewed</property>
+                  </widget>
+                  <packing>
+                    <property name="position">3</property>
+                  </packing>
+                </child>
+              </widget>
+              <packing>
+                <property name="position">6</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label3">
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">Max retrieved videos (0 is unlimited):</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">7</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkSpinButton" id="maxdownloads">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="adjustment">0 0 100 1 10 10</property>
+              </widget>
+              <packing>
+                <property name="position">8</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="dialog-action_area1">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <widget class="GtkButton" id="button1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="label">gtk-cancel</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-6</property>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkButton" id="button2">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="label">gtk-ok</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-5</property>
+              </widget>
+              <packing>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
 </glade-interface>

Modified: trunk/configure.ac
==============================================================================
--- trunk/configure.ac	(original)
+++ trunk/configure.ac	Sat Jun  7 07:56:04 2008
@@ -135,6 +135,7 @@
 conduit/modules/GoogleModule/gdata/Makefile
 conduit/modules/GoogleModule/gdata/apps/Makefile
 conduit/modules/GoogleModule/gdata/base/Makefile
+conduit/modules/GoogleModule/gdata/blogger/Makefile
 conduit/modules/GoogleModule/gdata/calendar/Makefile
 conduit/modules/GoogleModule/gdata/codesearch/Makefile
 conduit/modules/GoogleModule/gdata/contacts/Makefile
@@ -144,6 +145,7 @@
 conduit/modules/GoogleModule/gdata/media/Makefile
 conduit/modules/GoogleModule/gdata/photos/Makefile
 conduit/modules/GoogleModule/gdata/spreadsheet/Makefile
+conduit/modules/GoogleModule/gdata/youtube/Makefile
 conduit/modules/ShutterflyModule/Makefile
 conduit/modules/ShutterflyModule/shutterfly/Makefile
 conduit/modules/RhythmboxModule/Makefile



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]