|
|
@@ -0,0 +1,114 @@
|
|
|
+(in-package :cl-user)
|
|
|
+(defpackage #:timeliner.photos
|
|
|
+ (:use :cl #:timeliner.utils :cl-mongo :zpb-exif)
|
|
|
+ (:export #:*photos-path*
|
|
|
+ #:*photos-ignore*
|
|
|
+ #:on-cron))
|
|
|
+(in-package #:timeliner.photos)
|
|
|
+
|
|
|
+(defvar *photos-path* nil "Root path to search photos")
|
|
|
+(defvar *photos-types* '("JPG" "PNG") "Allowed types of photos")
|
|
|
+(defvar *photos-ignore* nil "Ignored filenames")
|
|
|
+
|
|
|
+(defun make-photo-doc (photo)
|
|
|
+ (let* ((ts (aget :created-at photo)))
|
|
|
+ (kv
|
|
|
+ (kv :ts ts)
|
|
|
+ (kv :type "photo")
|
|
|
+ (kv :title (format nil "Photo taken: ~A" (aget :name photo)))
|
|
|
+ (kv :photo (kv
|
|
|
+ (kv "path" (aget :path photo))
|
|
|
+ (kv "modified" (aget :modified photo))
|
|
|
+ (kv "length" (aget :length photo))
|
|
|
+ (kv "dimensions" (aget :dim photo))))
|
|
|
+ (kv :loc (timeliner.locations:point->doc
|
|
|
+ (or (aget :location photo)
|
|
|
+ (timeliner.locations:find-location-at (ts->ms ts))))))))
|
|
|
+
|
|
|
+(defun degrees-to-rational (deg ref)
|
|
|
+ (when deg
|
|
|
+ (let ((rational
|
|
|
+ (+ (elt deg 0)
|
|
|
+ (/ (elt deg 1) 60)
|
|
|
+ (/ (elt deg 2) 3600))))
|
|
|
+ (if (member ref '("N" "E") :test #'equal)
|
|
|
+ rational
|
|
|
+ (- rational)))))
|
|
|
+
|
|
|
+(defun exif-to-point (exif)
|
|
|
+ (when exif
|
|
|
+ (let ((lat (degrees-to-rational (exif-value :GPSLatitude exif)
|
|
|
+ (exif-value :GPSLatitudeRef exif)))
|
|
|
+ (lon (degrees-to-rational (exif-value :GPSLongitude exif)
|
|
|
+ (exif-value :GPSLongitudeRef exif))))
|
|
|
+ (and lat lon (geo:point-deg lat lon)))))
|
|
|
+
|
|
|
+(defun exif-to-taken (exif)
|
|
|
+ (when exif
|
|
|
+ (or (parsed-exif-value :DateTimeOriginal exif)
|
|
|
+ (parsed-exif-value :DateTime exif))))
|
|
|
+
|
|
|
+(defun exif-to-dim (exif)
|
|
|
+ (when exif
|
|
|
+ (let ((width (and exif (or (exif-value :PixelXDimension exif)
|
|
|
+ (exif-value :ImageWidth exif))))
|
|
|
+ (height (and exif (or (exif-value :PixelYDimension exif)
|
|
|
+ (exif-value :ImageHeight exif)))))
|
|
|
+ (and width height (list width height)))))
|
|
|
+
|
|
|
+(defun load-photo-info (path modified)
|
|
|
+ (with-open-file (in path :element-type '(unsigned-byte 8))
|
|
|
+ (let ((length (file-length in))
|
|
|
+ (exif (ignore-errors (make-exif in))))
|
|
|
+ (list
|
|
|
+ (cons :name (pathname-name path))
|
|
|
+ (cons :path (namestring path))
|
|
|
+ (cons :modified modified)
|
|
|
+ (cons :length length)
|
|
|
+ (cons :created-at (local-time:universal-to-timestamp
|
|
|
+ (or (exif-to-taken exif) modified)))
|
|
|
+ (cons :dim (exif-to-dim exif))
|
|
|
+ (cons :location (exif-to-point exif))))))
|
|
|
+
|
|
|
+(defun load-existing-photos ()
|
|
|
+ (let ((photos (make-hash-table :test #'equal)))
|
|
|
+ (mongo-iter
|
|
|
+ #'(lambda (d)
|
|
|
+ (setf (gethash (get-element "photo.path" d) photos)
|
|
|
+ (list (get-element "photo.modified" d)
|
|
|
+ (doc-id d))))
|
|
|
+ "events" ($ "type" "photo") :selector ($ ($ "photo.path" 1)
|
|
|
+ ($ "photo.modified" 1)))
|
|
|
+ photos))
|
|
|
+
|
|
|
+(defun import-photo-events ()
|
|
|
+ (let ((existing (load-existing-photos)))
|
|
|
+ (labels ((test-path (p)
|
|
|
+ (if (directory-pathname-p p)
|
|
|
+ (not (member (car (last (pathname-directory p)))
|
|
|
+ *photos-ignore*
|
|
|
+ :test #'equal))
|
|
|
+ (member (pathname-type p :case :common)
|
|
|
+ *photos-types* :test #'equal)))
|
|
|
+ (handle (p)
|
|
|
+ (let* ((name (namestring p))
|
|
|
+ (modified (file-write-date p))
|
|
|
+ (prev (gethash name existing)))
|
|
|
+ (if (and prev (= modified (first prev)))
|
|
|
+ (remhash name existing) ;; so that existing would only contains deleted files
|
|
|
+ (handler-case
|
|
|
+ (let ((info (load-photo-info p modified)))
|
|
|
+ (db.insert "events" (make-photo-doc info))
|
|
|
+ (log:info "Added" info))
|
|
|
+ (error (e) (log:error "Error adding photo" name e)))))))
|
|
|
+ (walk-directory *photos-path* #'handle :test #'test-path)
|
|
|
+ ;; Remove events for missing files
|
|
|
+ (loop for name being the hash-keys in existing
|
|
|
+ do (log:info "Deleting missing" name)
|
|
|
+ (db.delete "events" ($ "_id" (cl-mongo::make-bson-oid
|
|
|
+ :oid (second (gethash name existing)))))))))
|
|
|
+
|
|
|
+(defun on-cron ()
|
|
|
+ (handler-case
|
|
|
+ (import-photo-events)
|
|
|
+ (error (e) (log:error e))))
|