# service.py

#

Copyright (C) 2010 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. Service details and instances for the Picasa service.

#

    from __future__ import with_statement

    __author__ = 'tom.h.miller@gmail.com (Tom Miller)'
    import logging
    import os
    import urllib
    import time

    import gdata.photos
    from gdata.photos.service import PhotosService, GooglePhotosException

    import googlecl
    import googlecl.base
    import googlecl.service
    import googlecl.picasa
    import googlecl.calendar.date

#

Shortening the names of these guys.

    safe_encode = googlecl.safe_encode
    safe_decode = googlecl.safe_decode

    LOG = logging.getLogger(googlecl.picasa.LOGGER_NAME)
    SUPPORTED_VIDEO_TYPES = {'wmv': 'video/x-ms-wmv',
                             'avi': 'video/avi',
                             '3gp': 'video/3gpp',
                             'mov': 'video/quicktime',
                             'qt': 'video/quicktime',
                             'mp4': 'video/mp4',
                             'mpa': 'video/mpeg',
                             'mpe': 'video/mpeg',
                             'mpeg': 'video/mpeg',
                             'mpg': 'video/mpeg',
                             'mpv2': 'video/mpeg',
                             'mpeg4': 'video/mpeg4',}

#

XXX gdata.photos.service contains a very strange check against (outdated) allowed MIME types. This is a hack to allow videos to be uploaded. We're creating a list of the allowed video types stripped of the initial 'video/', eliminating duplicates via set(), then converting to tuple() since that's what gdata.photos.service uses.

    gdata.photos.service.SUPPORTED_UPLOAD_TYPES += \
       tuple(set([vtype.split('/')[1] for vtype in SUPPORTED_VIDEO_TYPES.values()]))
    DOWNLOAD_VIDEO_TYPES = {'swf': 'application/x-shockwave-flash',
                            'mp4': 'video/mpeg4',}

#

Extends gdata.photos.service.PhotosService for the command line.

    class PhotosServiceCL(PhotosService, googlecl.service.BaseServiceCL):

#

This class adds some features focused on using Picasa via an installed app with a command line interface.

#

Constructor.

      def __init__(self, config):

#

        PhotosService.__init__(self)
        googlecl.service.BaseServiceCL.__init__(self,
                                                googlecl.picasa.SECTION_HEADER,
                                                config)

#

Build a list of entries of either photos or albums.

      def build_entry_list(self, user='default', titles=None, query=None,
                           force_photos=False, photo_title=None):

#

If no title is specified, entries will be of photos matching the query. If no query is specified, entries will be of albums matching the title. If both title and query are specified, entries will be of photos matching the query that are also in albums matching the title.

Keyword arguments: user: Username of the owner of the albums / photos (Default 'default'). titles: list Titles of the albums (Default None). query: Query for photos, url-encoded (Default None). force_photos: If true, returns photo entries, even if album entries would typically be returned. The entries will be for all photos in each album. photo_title: Title of the photo(s) to return. Default None for all photos.

Returns: A list of entries, as specified above.

        album_entry = []
        if titles[0] or not(titles[0] or query):
          album_entry = self.GetAlbum(user=user, titles=titles)
        if photo_title or query or force_photos:
          uri = '/data/feed/api/user/' + user
          if query and not album_entry:
            entries = self.GetEntries(uri + '?kind=photo&q=' + query, photo_title)
          else:
            entries = []
            uri += '/albumid/%s?kind=photo'
            if query:
              uri += '&q=' + query
            for album in album_entry:
              photo_entries = self.GetEntries(uri % album.gphoto_id.text,
                                              photo_title)
              entries.extend(photo_entries)
        else:
          entries = album_entry

        return entries

#

Create photo album

      def create_album(self, title, summary, access, date):

#

Args: title: Title of the album. summary: Summary or description of the album. access: Access level string. See the picasa package init file for valid values. date: Date on the album, as a string. If eveluates to False, uses today.

Returns: AlbumEntry of newly created album.

        if date:
          parser = googlecl.calendar.date.DateParser()
          date = parser.determine_day(date, shift_dates=False)
          if date:
            timestamp = time.mktime(date.timetuple())
            timestamp_ms = '%i' % int((timestamp * 1000))
          else:
            LOG.error('Could not parse date %s. (Picasa only takes day info)' %
                      date)
            timestamp_ms = ''
        else:
          timestamp_ms = ''

        access = googlecl.picasa._map_access_string(access)
        return self.InsertAlbum(title=title, summary=summary,
                                access=access,
                                timestamp=timestamp_ms)

      CreateAlbum = create_album

#

Download an album to the local host.

      def download_album(self, base_path, user, video_format='mp4', titles=None,
                         photo_title=None):

#

Keyword arguments: base_path: Path on the filesystem to copy albums to. Each album will be stored in base_path/. If base_path does not exist, it and each non-existent parent directory will be created. user: User whose albums are being retrieved. (Default 'default') titles: list or string Title(s) that the album(s) should have. Default None, for all albums.

Get download link and extension for photo or video.

        def _get_download_info(photo_or_video, video_format):

#

video_format must be in DOWNLOAD_VIDEO_TYPES.

Returns: (url, extension)

          wanted_content = None
          for content in photo_or_video.media.content:
            if content.medium == 'image' and not wanted_content:
              wanted_content = content
            elif content.type == DOWNLOAD_VIDEO_TYPES[video_format]:
              wanted_content = content
          if not wanted_content:
            LOG.error('Did not find desired medium!')
            LOG.debug('photo_or_video.media:\n' + photo_or_video.media)
            return None
          elif wanted_content.medium == 'image':
            url = googlecl.picasa.make_download_url(photo_or_video.content.src)
            mimetype = photo_or_video.content.type
            extension = mimetype.split('/')[1]
          else:
            url = wanted_content.url
            extension = video_format
          return (url, extension)

#

End _get_download_info

        if not user:
          user = 'default'
        entries = self.GetAlbum(user=user, titles=titles)
        if video_format not in DOWNLOAD_VIDEO_TYPES.keys():
          LOG.error('Unsupported video format: ' + video_format)
          LOG.info('Try one of the following video formats: ' +
                   str(DOWNLOAD_VIDEO_TYPES.keys())[1:-1])
          video_format = 'mp4'
          LOG.info('Downloading videos as ' + video_format)

        for album in entries:
          album_path = os.path.join(base_path, safe_decode(album.title.text))
          album_concat = 1
          if os.path.exists(album_path):
            base_album_path = album_path
            while os.path.exists(album_path):
              album_path = base_album_path + '-%i' % album_concat
              album_concat += 1
          os.makedirs(album_path)

          uri = ('/data/feed/api/user/%s/albumid/%s?kind=photo' %
                 (user, album.gphoto_id.text))
          photo_entries = self.GetEntries(uri, photo_title)

          for photo_or_video in photo_entries:

#

TODO: Test on Windows (upload from one OS, download from another)

            photo_or_video_name = safe_decode(photo_or_video.title.text)
            photo_or_video_name = photo_or_video_name.split(os.extsep)[0]
            url, extension = _get_download_info(photo_or_video, video_format)
            path = os.path.join(album_path,
                                photo_or_video_name + os.extsep + extension)

#

Check for a file extension, add it if it does not exist.

            if os.path.exists(path):
              base_path = path
              photo_concat = 1
              while os.path.exists(path):
                path = base_path + '-%i' % photo_concat
                photo_concat += 1
            LOG.info(safe_encode('Downloading %s to %s' %
                                 (safe_decode(photo_or_video.title.text), path)))
            urllib.urlretrieve(url, path)

      DownloadAlbum = download_album

#

Get albums from a user feed.

      def get_album(self, user='default', titles=None):

#

Keyword arguments: user: The user whose albums are being retrieved. (Default 'default') titles: list or string Title(s) that the album(s) should have. Default None, for all albums.

Returns: List of albums that match parameters, or [] if none do.

        uri = '/data/feed/api/user/' + user + '?kind=album'
        return self.GetEntries(uri, titles)

      GetAlbum = get_album

#

Get a single album.

      def get_single_album(self, user='default', title=None):

#

        uri = '/data/feed/api/user/' + user + '?kind=album'
        return self.GetSingleEntry(uri, title=title)

      GetSingleAlbum = get_single_album

#

Insert photos or videos into an album.

      def insert_media_list(self, album, media_list, tags='', user='default',
                            photo_name=None, caption=None):

#

Keyword arguments: album: The album entry of the album getting the media. media_list: A list of paths, each path a picture or video on the local host. tags: Text of the tags to be added to each item, e.g. 'Islands, Vacation' (Default ''). caption: Caption/summary to give each item. Default None for no caption.

        album_url = ('/data/feed/api/user/%s/albumid/%s' %
                     (user, album.gphoto_id.text))
        keywords = tags
        if caption is None:
          caption = ''
        failures = []
        for path in media_list:
          LOG.info(safe_encode('Loading file ' + path + ' to album ' +
                               safe_decode(album.title.text)))

          ext = googlecl.get_extension_from_path(path)
          if not ext:
            LOG.debug('No extension match on path ' + path)
            content_type = 'image/jpeg'
          else:
            ext = ext.lower()
            try:
              content_type = SUPPORTED_VIDEO_TYPES[ext]
            except KeyError:
              content_type = 'image/' + ext.lower()
          title = photo_name
          if not title:
            title = os.path.split(path)[1]
          try:
            self.InsertPhotoSimple(album_url,
                                   title=title,
                                   summary=caption,
                                   filename_or_handle=path,
                                   keywords=keywords,
                                   content_type=content_type)
          except GooglePhotosException, err:
            LOG.error('Failed to upload %s. (%s: %s)', path,
                                                       err.args[0],
                                                       err.args[1])
            failures.append(file)
          except Exception, err:

#

Don't let a stray error wreck an upload of 1000 photos

            LOG.error(safe_encode('Unexpected error -- ' + unicode(err)))
            failures.append(file)
        if failures:
          LOG.info(str(len(failures)) + ' photos failed to upload')
          LOG.debug(safe_encode('Failed files: ' + unicode(failures)))
        return failures

      InsertMediaList = insert_media_list

#

Check that the token being used is valid.

      def is_token_valid(self, test_uri='/data/feed/api/user/default'):

#

        return googlecl.base.BaseCL.IsTokenValid(self, test_uri)

      IsTokenValid = is_token_valid

#

Add or remove tags on a list of photos.

      def tag_photos(self, photo_entries, tags, caption):

#

Keyword arguments: photo_entries: List of photo entry objects. tags: String representation of tags in a comma separated list. For how tags are generated from the string, see googlecl.base.generate_tag_sets(). Set None to leave the tags as they currently are. caption: New caption for the photo. Set None to leave the caption as it is.

        from gdata.media import Group, Keywords
        from atom import Summary
        if tags is not None:
          remove_set, add_set, replace_tags = googlecl.base.generate_tag_sets(tags)
        for photo in photo_entries:
          if tags is not None:
            if not photo.media:
              photo.media = Group()
            if not photo.media.keywords:
              photo.media.keywords = Keywords()

#

No point removing tags if the photo has no keywords, or we're replacing the keywords.

            if photo.media.keywords.text and remove_set and not replace_tags:
              current_tags = photo.media.keywords.text.replace(', ', ',')
              current_set = set(current_tags.split(','))
              photo.media.keywords.text = ','.join(current_set - remove_set)

            if replace_tags or not photo.media.keywords.text:
              photo.media.keywords.text = ','.join(add_set)
            elif add_set:
              photo.media.keywords.text += ',' + ','.join(add_set)

          if caption is not None:
            if not photo.summary:
              photo.summary = Summary(text=caption, summary_type='text')
            else:
              photo.summary.text = caption

          self.UpdatePhotoMetadata(photo)

      TagPhotos = tag_photos

    SERVICE_CLASS = PhotosServiceCL