aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--PhotoAlbum.py158
-rw-r--r--TreeWalker.py35
3 files changed, 195 insertions, 0 deletions
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