Initial import.

This commit is contained in:
Jason A. Donenfeld 2011-05-05 07:04:40 -04:00
commit b195f76966
3 changed files with 195 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc
cache/*

158
PhotoAlbum.py Normal file
View File

@ -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)

35
TreeWalker.py Normal file
View File

@ -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