diff --git a/scanner/CachePath.py b/scanner/CachePath.py index 89fc20e..d7952f5 100644 --- a/scanner/CachePath.py +++ b/scanner/CachePath.py @@ -1,6 +1,17 @@ import os.path from datetime import datetime +def message(category, text): + if message.level <= 0: + sep = " " + else: + sep = "--" + print "%s %s%s[%s]%s%s" % (datetime.now().isoformat(), max(0, message.level) * " |", sep, category, max(1, (14 - len(category))) * " ", text) +message.level = -1 +def next_level(): + message.level += 1 +def back_level(): + message.level -= 1 def set_cache_path_base(base): trim_base.base = base def untrim_base(path): diff --git a/scanner/PhotoAlbum.py b/scanner/PhotoAlbum.py index 0116ac8..35b2ca1 100644 --- a/scanner/PhotoAlbum.py +++ b/scanner/PhotoAlbum.py @@ -110,6 +110,8 @@ class Photo(object): self.is_valid = True try: mtime = file_mtime(path) + except KeyboardInterrupt: + raise except: self.is_valid = False return @@ -121,16 +123,20 @@ class Photo(object): try: image = Image.open(path) + except KeyboardInterrupt: + raise except: self.is_valid = False return self._metadata(image) - self._thumbnails(image, thumb_path) + self._thumbnails(image, thumb_path, path) def _metadata(self, image): self._attributes["size"] = image.size self._orientation = 1 try: info = image._getexif() + except KeyboardInterrupt: + raise except: return if not info: @@ -144,6 +150,8 @@ class Photo(object): if isinstance(decoded, str) and decoded.startswith("DateTime"): try: value = datetime.strptime(value, '%Y:%m:%d %H:%M:%S') + except KeyboardInterrupt: + raise except: continue exif[decoded] = value @@ -175,6 +183,8 @@ class Photo(object): if "Flash" in exif and exif["Flash"] in self._metadata.flash_dictionary: try: self._attributes["flash"] = self._metadata.flash_dictionary[exif["Flash"]] + except KeyboardInterrupt: + raise except: pass if "ExposureProgram" in exif and exif["ExposureProgram"] < len(self._metadata.exposure_list): @@ -197,19 +207,26 @@ class Photo(object): _metadata.exposure_list = ["Not Defined", "Manual", "Program AE", "Aperture-priority AE", "Shutter speed priority AE", "Creative (Slow speed)", "Action (High speed)", "Portrait", "Landscape", "Bulb"] _metadata.orientation_list = ["Horizontal (normal)", "Mirror horizontal", "Rotate 180", "Mirror vertical", "Mirror horizontal and rotate 270 CW", "Rotate 90 CW", "Mirror horizontal and rotate 90 CW", "Rotate 270 CW"] - def _thumbnail(self, image, thumb_path, size, square=False): + def _thumbnail(self, image, thumb_path, original_path, size, square=False): thumb_path = os.path.join(thumb_path, image_cache(self._path, size, square)) - print "Thumbing %s" % thumb_path + info_string = "%s -> %spx" % (os.path.basename(original_path), str(size)) + if square: + info_string += ", square" + message("thumbing", info_string) if os.path.exists(thumb_path) and file_mtime(thumb_path) >= self._attributes["dateTimeFile"]: return gc.collect() try: image = image.copy() + except KeyboardInterrupt: + raise except: try: image = image.copy() # we try again to work around PIL bug + except KeyboardInterrupt: + raise except: - print "Image is corrupted. %s will not be created." % thumb_path + message("corrupt image", os.path.basename(original_path)) return if square: if image.size[0] > image.size[1]: @@ -227,11 +244,14 @@ class Photo(object): image.thumbnail((size, size), Image.ANTIALIAS) try: image.save(thumb_path, "JPEG") + except KeyboardInterrupt: + os.unlink(thumb_path) + raise except: - print "Could not thumbnail %s" % thumb_path + message("save failure", os.path.basename(thumb_path)) os.unlink(thumb_path) - def _thumbnails(self, image, thumb_path): + def _thumbnails(self, image, thumb_path, original_path): mirror = image if self._orientation == 2: # Vertical Mirror @@ -255,7 +275,7 @@ class Photo(object): # Rotation 90 mirror = image.transpose(Image.ROTATE_90) for size in Photo.thumb_sizes: - self._thumbnail(mirror, thumb_path, size[0], size[1]) + self._thumbnail(mirror, thumb_path, original_path, size[0], size[1]) @property def name(self): return os.path.basename(self._path) @@ -294,6 +314,8 @@ class Photo(object): if key.startswith("dateTime"): try: dictionary[key] = datetime.strptime(dictionary[key], "%a %b %d %H:%M:%S %Y") + except KeyboardInterrupt: + raise except: pass return Photo(path, None, dictionary) diff --git a/scanner/TreeWalker.py b/scanner/TreeWalker.py index e9df64e..4855d20 100644 --- a/scanner/TreeWalker.py +++ b/scanner/TreeWalker.py @@ -3,7 +3,7 @@ import os.path import sys from datetime import datetime from PhotoAlbum import Photo, Album, PhotoAlbumEncoder -from CachePath import json_cache, set_cache_path_base, file_mtime +from CachePath import * import json class TreeWalker: @@ -16,23 +16,28 @@ class TreeWalker: self.walk(self.album_path) self.big_lists() self.remove_stale() + message("complete", "") def walk(self, path): - print "Walking %s" % path + next_level() + message("walking", os.path.basename(path)) cache = os.path.join(self.cache_path, json_cache(path)) cached = False cached_album = None if os.path.exists(cache): - print "Has cache %s" % path try: cached_album = Album.from_cache(cache) if file_mtime(path) <= file_mtime(cache): - print "Album is fully cached" + message("full cache", os.path.basename(path)) cached = True album = cached_album for photo in album.photos: self.all_photos.append(photo) + else: + message("partial cache", os.path.basename(path)) + except KeyboardInterrupt: + raise except: - print "Cache is corrupted. Rescanning album." + message("corrupt cache", os.path.basename(path)) cached_album = None if not cached: album = Album(path) @@ -41,61 +46,70 @@ class TreeWalker: continue try: entry = entry.decode(sys.getfilesystemencoding()) + except KeyboardInterrupt: + raise except: pass 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): + next_level() cache_hit = False if cached_album: cached_photo = cached_album.photo_from_path(entry) if cached_photo and file_mtime(entry) <= cached_photo.attributes["dateTimeFile"]: - print "Photo cache hit %s" % entry + message("cache hit", os.path.basename(entry)) cache_hit = True photo = cached_photo if not cache_hit: - print "No cache, scanning %s" % entry + message("metainfo", os.path.basename(entry)) photo = Photo(entry, self.cache_path) if photo.is_valid: self.all_photos.append(photo) album.add_photo(photo) + else: + message("unreadable", os.path.basename(entry)) + back_level() if not album.empty: - print "Writing cache of %s" % album.cache_path + message("caching", os.path.basename(path)) album.cache(self.cache_path) self.all_albums.append(album) else: - print "Not writing cache of %s because it's empty" % album.cache_path + message("empty", os.path.basename(path)) + back_level() return album def big_lists(self): photo_list = [] self.all_photos.sort() for photo in self.all_photos: photo_list.append(photo.path) - print "Writing all photos list." + message("caching", "all photos path list") fp = open(os.path.join(self.cache_path, "all_photos.json"), 'w') json.dump(photo_list, fp, cls=PhotoAlbumEncoder) fp.close() photo_list.reverse() - print "Writing latest photos list." + message("caching", "latest photos path list") fp = open(os.path.join(self.cache_path, "latest_photos.json"), 'w') json.dump(photo_list[0:27], fp, cls=PhotoAlbumEncoder) fp.close() def remove_stale(self): - print "Building list of all cache entries." + message("cleanup", "building stale list") all_cache_entries = { "all_photos.json": True, "latest_photos.json": True } for album in self.all_albums: all_cache_entries[album.cache_path] = True for photo in self.all_photos: for entry in photo.image_caches: all_cache_entries[entry] = True - print "Searching stale cache entries." + message("cleanup", "searching for stale cache entries") for cache in os.listdir(self.cache_path): try: cache = cache.decode(sys.getfilesystemencoding()) + except KeyboardInterrupt: + raise except: pass if cache not in all_cache_entries: - print "Removing stale cache %s" % cache + message("cleanup", os.path.basename(cache)) os.unlink(os.path.join(self.cache_path, cache)) diff --git a/scanner/main.py b/scanner/main.py index 6134f6f..b15d593 100755 --- a/scanner/main.py +++ b/scanner/main.py @@ -1,13 +1,18 @@ #!/usr/bin/env python from TreeWalker import TreeWalker -from sys import argv +from sys import argv, exit +from CachePath import message def main(): if len(argv) != 3: print "usage: %s ALBUM_PATH CACHE_PATH" % argv[0] return - TreeWalker(argv[1], argv[2]) + try: + TreeWalker(argv[1], argv[2]) + except KeyboardInterrupt: + message("keyboard", "CTRL+C pressed, quitting.") + exit(-97) if __name__ == "__main__": main() \ No newline at end of file