def __init__(self, cl_args, path, album_path = None): path = googlecl.safe_decode(path) self.path = path self.timestamp = None if album_path: path = os.path.join(album_path, path) if 'stat' not in cl_args.origin: cl_args.origin.append('stat') for origin in cl_args.origin: if origin == 'stat': try: self.timestamp = int(os.stat(path).st_mtime) break except Exception: pass elif origin == 'exif': metadata = pyexiv2.ImageMetadata(path) try: metadata.read() if 'Exif.Image.DateTime' in metadata: self.timestamp = calendar.timegm(metadata['Exif.Image.DateTime'].value.timetuple()) break except Exception: pass else: for m in re.finditer(r'\d', self.path): try: self.timestamp = calendar.timegm(dateutil.parser.parse(m.string[m.start():], fuzzy = True, dayfirst = True).timetuple()) break except ValueError: pass if self.timestamp: break
def fillFromDisk(self): if self.filled_from_disk: return for path in self.cl_args.paths: for root, dirs, files in os.walk(path): supported_files = sorted([f for f in files if mimetypes.guess_type(f)[0] in self.supported_types]) if len(supported_files) == 0: continue if root == path: album_title = os.path.basename(os.path.normpath(root)) elif len(self.cl_args.paths) > 1 or googlecl.safe_decode(os.path.basename(os.path.normpath(path))) in self: album_title = os.path.join(os.path.basename(os.path.normpath(path)), os.path.relpath(root, path)) else: album_title = os.path.relpath(root, path) num_albums = (len(supported_files) + self.cl_args.max_photos - 1) / self.cl_args.max_photos full_album_title = album_title for i in xrange(0, num_albums): if num_albums > 1: self.LOG.debug(u'Splicing album "{} ({})" with photos from "{}" to "{}"'.format(album_title, i + 1, supported_files[i * self.cl_args.max_photos], supported_files[min(i * self.cl_args.max_photos + self.cl_args.max_photos - 1, len(supported_files) - 1)])) full_album_title = album_title + ' (%s)' % (i + 1) album = Album(self.cl_args, full_album_title, disk = AlbumDiskEntry(self.cl_args, root)) album.fillFromDisk(supported_files[i * self.cl_args.max_photos:i * self.cl_args.max_photos + self.cl_args.max_photos]) if album.title in self: self[album.title].combine(album) else: self[album.title] = album self.filled_from_disk = True
def __init__(self, cal_entry=None, user=None, name=None): """Parse a CalendarEntry into "user" and human-readable names, or take them directly.""" if cal_entry: # Non-primary calendar feeds look like this: # http:blah/.../feeds/JUNK%40group.calendar.google.com/private/full # So grab the part after /feeds/ and unquote it. self.user = urllib.unquote(cal_entry.content.src.split("/")[-3]) self.name = safe_decode(cal_entry.title.text) else: self.user = user self.name = name
def __init__(self, cal_entry=None, user=None, name=None): """Parse a CalendarEntry into "user" and human-readable names, or take them directly.""" if cal_entry: # Non-primary calendar feeds look like this: # http:blah/.../feeds/JUNK%40group.calendar.google.com/private/full # So grab the part after /feeds/ and unquote it. self.user = urllib.unquote(cal_entry.content.src.split('/')[-3]) self.name = safe_decode(cal_entry.title.text) else: self.user = user self.name = name
def __init__(self, album, title = None, disk = None, picasa = None, raw = False): self.album = album self.disk = disk self.picasa = picasa self.raw = raw if not title: if disk: self.title = os.path.splitext(disk.path)[0] elif picasa: self.title = picasa.title.text else: raise InvalidArguments("No title for photo given and no valid entry found") else: self.title = title self.title = googlecl.safe_decode(self.title)
def __init__(self, cl_args, title = None, disk = None, picasa = None): self.client = None self.cl_args = cl_args self.disk = disk self.picasa = picasa if not title: if disk: self.title = disk.path elif picasa: self.title = picasa.title.text else: raise InvalidArguments("No title for photo given and no valid entry found") else: self.title = title self.title = googlecl.safe_decode(self.title) self.filled_from_disk = False self.filled_from_picasa = False
def add_item_to_dict(self, title, album): if not album.picasa: # Skip if there is no online entry. return album_dict = dict() album_dict['title'] = title album_dict['published'] = album.picasa.published.text #album_dict['thumbnail'] = album.picasa.media.thumbnail.url album_dict['photos'] = [] for photo_name in sorted(album.keys(), key=lambda x: album[x].order_id): photo = album[photo_name] photo_dict = {'name': photo_name} if photo.picasa and photo.picasa.summary.text: photo_dict['summary'] = googlecl.safe_decode(photo.picasa.summary.text) album_dict['photos'].append(photo_dict) self.dict_for_dump[title] = album_dict self.LOG.error("%s: %s", title, album)
def __init__(self, cl_args, path): self.path = googlecl.safe_decode(path) self.timestamp = None if 'stat' not in cl_args.origin: cl_args.origin.append('stat') for origin in cl_args.origin: if origin == 'stat': try: self.timestamp = int(os.stat(path).st_mtime) break except Exception: pass elif origin == 'filename': for m in re.finditer(r'\d', self.path): try: self.timestamp = calendar.timegm(dateutil.parser.parse(m.string[m.start():], fuzzy = True, dayfirst = True).timetuple()) break except ValueError: pass if self.timestamp: break
def delete_recurring_events(self, events, start_date, end_date, cal_user, prompt): """Delete recurring events from a calendar. Keyword arguments: events: List of non-expanded calendar events to delete. start_date: Date specifying the start of events (inclusive). end_date: Date specifying the end of events (inclusive). None for no end date. cal_user: "******" of the calendar to delete events from. prompt: True if we should prompt before deleting events, False otherwise. """ # option_list is a list of tuples, (prompt_string, deletion_instruction) # prompt_string gets displayed to the user, # deletion_instruction is a special value that will let the program know # what to do. # 'ALL' -- delete all events in the series. # 'NONE' -- don't delete anything. # 'TWIXT' -- delete events between start_date and end_date. # 'ON' -- delete events on the single date given. # 'ONAFTER' -- delete events on and after the date given. deletion_choice = "ALL" option_list = [("All events in this series", deletion_choice)] if start_date and end_date: deletion_choice = "TWIXT" option_list.append(("Instances between %s and %s" % (start_date, end_date), deletion_choice)) elif start_date or end_date: delete_date = start_date or end_date option_list.append(("Instances on %s" % delete_date, "ON")) option_list.append(("All events on and after %s" % delete_date, "ONAFTER")) deletion_choice = "ON" option_list.append(("Do not delete", "NONE")) prompt_str = "" for i, option in enumerate(option_list): prompt_str += str(i) + ") " + option[0] + "\n" # Condense events so that the user isn't prompted for the same event # multiple times. This is assuming that recurring events have been # expanded. events = googlecl.calendar.condense_recurring_events(events) for event in events: if prompt: delete_selection = -1 while delete_selection < 0 or delete_selection > len(option_list) - 1: msg = 'Delete "%s"?\n%s' % (safe_decode(event.title.text), prompt_str) try: delete_selection = int(raw_input(safe_encode(msg))) except ValueError: continue deletion_choice = option_list[delete_selection][1] # deletion_choice has either been picked by the prompt, or is the default # value. The default value is determined by the date info passed in, # and should be the "least destructive" option. if deletion_choice == "ALL": self._delete_original_event(event, cal_user) elif deletion_choice == "TWIXT": self._batch_delete_recur(event, cal_user, start_date=start_date, end_date=end_date) elif deletion_choice == "ON": self._batch_delete_recur(event, cal_user, start_date=delete_date, end_date=delete_date) elif deletion_choice == "ONAFTER": self._batch_delete_recur(event, cal_user, start_date=delete_date) elif deletion_choice != "NONE": raise CalendarError("Got unexpected batch deletion command!")
def delete_recurring_events(self, events, start_date, end_date, cal_user, prompt): """Delete recurring events from a calendar. Keyword arguments: events: List of non-expanded calendar events to delete. start_date: Date specifying the start of events (inclusive). end_date: Date specifying the end of events (inclusive). None for no end date. cal_user: "******" of the calendar to delete events from. prompt: True if we should prompt before deleting events, False otherwise. """ # option_list is a list of tuples, (prompt_string, deletion_instruction) # prompt_string gets displayed to the user, # deletion_instruction is a special value that will let the program know # what to do. # 'ALL' -- delete all events in the series. # 'NONE' -- don't delete anything. # 'TWIXT' -- delete events between start_date and end_date. # 'ON' -- delete events on the single date given. # 'ONAFTER' -- delete events on and after the date given. deletion_choice = 'ALL' option_list = [('All events in this series', deletion_choice)] if start_date and end_date: deletion_choice = 'TWIXT' option_list.append(('Instances between %s and %s' % (start_date, end_date), deletion_choice)) elif start_date or end_date: delete_date = (start_date or end_date) option_list.append(('Instances on %s' % delete_date, 'ON')) option_list.append(('All events on and after %s' % delete_date, 'ONAFTER')) deletion_choice = 'ON' option_list.append(('Do not delete', 'NONE')) prompt_str = '' for i, option in enumerate(option_list): prompt_str += str(i) + ') ' + option[0] + '\n' # Condense events so that the user isn't prompted for the same event # multiple times. This is assuming that recurring events have been # expanded. events = googlecl.calendar.condense_recurring_events(events) for event in events: if prompt: delete_selection = -1 while delete_selection < 0 or delete_selection > len(option_list) - 1: msg = 'Delete "%s"?\n%s' %\ (safe_decode(event.title.text), prompt_str) try: delete_selection = int(raw_input(safe_encode(msg))) except ValueError: continue deletion_choice = option_list[delete_selection][1] # deletion_choice has either been picked by the prompt, or is the default # value. The default value is determined by the date info passed in, # and should be the "least destructive" option. if deletion_choice == 'ALL': self._delete_original_event(event, cal_user) elif deletion_choice == 'TWIXT': self._batch_delete_recur(event, cal_user, start_date=start_date, end_date=end_date) elif deletion_choice == 'ON': self._batch_delete_recur(event, cal_user, start_date=delete_date, end_date=delete_date) elif deletion_choice == 'ONAFTER': self._batch_delete_recur(event, cal_user, start_date=delete_date) elif deletion_choice != 'NONE': raise CalendarError('Got unexpected batch deletion command!')
def upload(self): if self.isInPicasa(): if self.album.cl_args.force_update and self.album.cl_args.force_update == 'metadata': self.picasa.timestamp = gdata.photos.Timestamp(text = str(long(self.disk.timestamp) * 1000)) try: self.picasa = self.album.client.UpdatePhotoMetadata(self.picasa) except GooglePhotosException as e: self.LOG.error(u'Error updating metadata for photo "{}": '.format(self.title) + str(e)) finally: return else: metadata = self.picasa metadata.timestamp = gdata.photos.Timestamp(text = str(long(self.disk.timestamp) * 1000)) metadata.title = atom.Title(text = self.title) else: metadata = gdata.photos.PhotoEntry() metadata.title = atom.Title(text = self.title) metadata.timestamp = gdata.photos.Timestamp(text = str(long(self.disk.timestamp) * 1000)) mimetype = mimetypes.guess_type(self.path)[0] transforms = self.album.cl_args.transform[:] if self.album.cl_args.transform else None if transforms: original = pyexiv2.ImageMetadata(googlecl.safe_decode(self.path)) try: original.read() except Exception as e: self.LOG.error(u'Error reading file "{}": '.format(self.disk.path) + str(e)) return if 'raw' in transforms and not self.isRaw(): transforms.remove('raw') if 'resize' in transforms and not (original.dimensions[0] > self.album.cl_args.max_size[0] or original.dimensions[1] > self.album.cl_args.max_size[1]): transforms.remove('resize') if 'rotate' in transforms and ('Exif.Image.Orientation' not in original or original['Exif.Image.Orientation'].value == 1): transforms.remove('rotate') if 'raw' in transforms: if len(original.previews) == 0: self.LOG.error(u'Error getting valid preview from raw file "{}"'.format(self.disk.path)) return try: preview = next(x for x in original.previews if (x.dimensions[0] >= self.album.cl_args.max_size[0] or x.dimensions[1] >= self.album.cl_args.max_size[1]) and x.mime_type in AlbumList.standard_types) except StopIteration: preview = metadata.previews[-1] mimetype = preview.mime_type if mimetype not in AlbumList.standard_types: self.LOG.error(u'Error getting valid preview from raw file "{}"'.format(self.disk.path)) return photo = cStringIO.StringIO(preview.data) else: photo = cStringIO.StringIO(original.buffer) # if 'resize' in transforms or 'rotate' in transforms and mimetype != 'image/jpeg': if 'resize' in transforms or 'rotate' in transforms: image = Image.open(photo) if 'resize' in transforms: image.thumbnail(self.album.cl_args.max_size, Image.ANTIALIAS) if 'rotate' in transforms: for t in self.transforms.get(original['Exif.Image.Orientation'].value, ()): image = image.transpose(t) original['Exif.Image.Orientation'] = 1 photo = cStringIO.StringIO() # TODO: save in the same format and size approx image.save(photo, 'JPEG', quality = 95) mimetype = 'image/jpeg' photo.seek(0) # if 'rotate' in transforms and 'resize' not in transforms and mimetype == 'image/jpeg': # # TODO: lossless jpeg rotate # pass if not self.album.cl_args.strip_exif: modified = pyexiv2.ImageMetadata.from_buffer(photo.getvalue()) modified.read() original.copy(modified) modified.write() photo = cStringIO.StringIO(modified.buffer) else: if self.album.cl_args.strip_exif: original = pyexiv2.ImageMetadata.from_buffer(file(self.path).read()) original.read() for k in original.exif_keys + original.iptc_keys + original.xmp_keys: del original[k] del original.comment original.write() photo = cStringIO.StringIO(original.buffer) else: photo = self.path try: if self.isInPicasa(): metadata = self.album.client.UpdatePhotoMetadata(metadata) self.picasa = self.album.client.UpdatePhotoBlob(metadata, photo, mimetype) else: self.picasa = self.album.client.InsertPhoto(self.album.picasa, metadata, photo, mimetype) except GooglePhotosException as e: self.LOG.error(u'Error uploading file "{}": '.format(self.disk.path) + str(e))