From b195f769661d4178c964d7f9ac6abb8ed4bad101 Mon Sep 17 00:00:00 2001 From: "Jason A. Donenfeld" Date: Thu, 5 May 2011 07:04:40 -0400 Subject: Initial import. --- .gitignore | 2 + PhotoAlbum.py | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TreeWalker.py | 35 +++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 .gitignore create mode 100644 PhotoAlbum.py create mode 100644 TreeWalker.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2173ff7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +cache/* diff --git a/PhotoAlbum.py b/PhotoAlbum.py new file mode 100644 index 0000000..3e52086 --- /dev/null +++ b/PhotoAlbum.py @@ -0,0 +1,158 @@ +from datetime import datetime +import json +import os.path +from PIL import Image +from PIL.ExifTags import TAGS + +def set_cache_path_base(base): + trim_base.base = base +def trim_base(path): + if path.startswith(trim_base.base): + path = path[len(trim_base.base):] + if path.startswith('/'): + path = path[1:] + return path +def untrim_base(path): + return os.path.join(trim_base.base, path) +def cache_base(path): + path = trim_base(path).replace('/', '-').replace(' ', '_') + if len(path) == 0: + path = "root" + return path +def json_cache(path): + return cache_base(path) + ".json" +def image_cache(path, suffix): + return cache_base(path) + "_" + suffix + ".jpg" + +class Album(object): + def __init__(self, path): + self._path = trim_base(path) + self._photos = list() + self._albums = list() + self._photos_sorted = True + self._albums_sorted = True + @property + def path(self): + return self._path + def __str__(self): + return self.path + @property + def cache_path(self): + return json_cache(self.path) + @property + def date(self): + self._sort() + if len(self._photos) == 0 and len(self._albums) == 0: + return datetime.min + elif len(self._photos) == 0: + return self._albums[-1].date + elif len(self._albums) == 0: + return self._photos[-1].date + return max(self._photos[-1].date, self._albums[-1].date) + def __cmp__(self, other): + return cmp(self.date, other.date) + def add_photo(self, photo): + self._photos.append(photo) + self._photos_sorted = False + def add_album(self, album): + self._albums.append(album) + self._photos_sorted = False + def _sort(self): + if not self._photos_sorted: + self._photos.sort() + self._photos_sorted = True + if not self._albums_sorted: + self._albums.sort() + self._albums_sorted = True + def cache(self, base_dir): + self._sort() + fp = open(os.path.join(base_dir, self.cache_path), 'w') + json.dump(self, fp, cls=PhotoAlbumEncoder) + fp.close() + @staticmethod + def from_cache(path): + fp = open(path, "r") + dictionary = json.load(fp) + fp.close() + return Album.from_dict(dictionary) + @staticmethod + def from_dict(dictionary, cripple=True): + album = Album(dictionary["path"]) + for photo in dictionary["photos"]: + album.add_photo(Photo.from_dict(photo, album.path)) + if not cripple: + for subalbum in dictionary["albums"]: + album.add_album(Album.from_dict(subalbum), cripple) + album._sort() + return album + def to_dict(self, cripple=True): + self._sort() + if cripple: + subalbums = [ { "path": sub.path, "date": sub.date } for sub in self._albums ] + else: + subalbums = self._albums + return { "path": self.path, "date": self.date, "albums": subalbums, "photos": self._photos } + +class Photo(object): + def __init__(self, path, attributes=None): + self._path = trim_base(path) + self.is_valid = True + if attributes is not None: + self._attributes = attributes + return + else: + self._attributes = {} + try: + i = Image.open(path) + except: + self.is_valid = False + return + try: + info = i._getexif() + except: + info = None + if info: + for tag, value in info.items(): + decoded = TAGS.get(tag, tag) + if not isinstance(decoded, int) and decoded not in ['JPEGThumbnail', 'TIFFThumbnail', 'Filename', 'FileSource', 'MakerNote', 'UserComment', 'ImageDescription', 'ComponentsConfiguration']: + if isinstance(value, str): + value = value.strip() + self._attributes[decoded] = value + @property + def name(self): + return os.path.basename(self._path) + def __str__(self): + return self.name + @property + def cache_paths(self): + return [image_cache(self.path, size) for size in [100, 640, 1024]] + @property + def date(self): + if "DateTime" in self._attributes: + return datetime.strptime(self._attributes["DateTime"], '%Y:%m:%d %H:%M:%S') + else: + return datetime.fromtimestamp(os.path.getmtime(untrim_base(self._path))) + def __cmp__(self, other): + return cmp(self.date, other.date) + @property + def attributes(self): + return self._attributes + @staticmethod + def from_dict(dictionary, basepath): + del dictionary["date"] + path = os.path.join(basepath, dictionary["name"]) + del dictionary["name"] + return Photo(path, dictionary) + def to_dict(self): + photo = { "name": self.name, "date": self.date } + photo.update(self.attributes) + return photo + +class PhotoAlbumEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime): + return obj.isoformat() + if isinstance(obj, Album) or isinstance(obj, Photo): + return obj.to_dict() + return json.JSONEncoder.default(self, obj) + \ No newline at end of file diff --git a/TreeWalker.py b/TreeWalker.py new file mode 100644 index 0000000..e894944 --- /dev/null +++ b/TreeWalker.py @@ -0,0 +1,35 @@ +import os +import os.path +from PhotoAlbum import Photo, Album, json_cache, set_cache_path_base + +class TreeWalker: + def __init__(self, album_path, cache_path): + self.album_path = album_path + self.cache_path = cache_path + set_cache_path_base(self.album_path) + self.all_albums = list() + self.all_photos = list() + self.walk(album_path) + self.remove_stale() + def walk(self, path): + cache = os.path.join(self.cache_path, json_cache(path)) + cached = False + if os.path.exists(cache) and os.path.getmtime(path) <= os.path.getmtime(cache): + album = Album.from_cache(cache) + cached = True + else: + album = Album(path) + for entry in os.listdir(path): + entry = os.path.join(path, entry) + if os.path.isdir(entry): + album.add_album(self.walk(entry)) + elif not cached and os.path.isfile(entry): + photo = Photo(entry) + if photo.is_valid: + self.all_photos.append(photo) + album.add_photo(photo) + album.cache(self.cache_path) + self.all_albums.append(album) + return album + def remove_stale(self): + pass \ No newline at end of file -- cgit v1.2.3-59-g8ed1b