diff --git a/application/classes/Controller/Photo.php b/application/classes/Controller/Photo.php index a30afd5..7fe3973 100644 --- a/application/classes/Controller/Photo.php +++ b/application/classes/Controller/Photo.php @@ -33,8 +33,7 @@ class Controller_Photo extends Controller_TemplateDefault { $output .= sprintf('Removing %s (%s)',$po->id,$po->file_path()); // Delete it - if (unlink($po->file_path())) - $po->delete(); + $po->delete(); } $p = ORM::factory('Photo'); @@ -167,6 +166,7 @@ class Controller_Photo extends Controller_TemplateDefault { foreach (array( 'ID'=>array('key'=>'id','value'=>HTML::anchor('/photo/details/%VALUE%','%VALUE%')), 'Thumbnail'=>array('key'=>'id','value'=>HTML::anchor('/photo/view/%VALUE%',HTML::image('photo/thumbnail/%VALUE%'))), + 'ThumbSig'=>array('key'=>'thumbnail_sig()'), 'Signature'=>array('key'=>'signature'), 'Date Taken'=>array('key'=>'date_taken()'), 'File Modified'=>array('key'=>'date_file("m",TRUE)'), diff --git a/application/classes/Model/Photo.php b/application/classes/Model/Photo.php index b7d9217..10ebca5 100644 --- a/application/classes/Model/Photo.php +++ b/application/classes/Model/Photo.php @@ -15,7 +15,10 @@ class Model_Photo extends ORM { protected $_has_many = array( 'album'=>array('through'=>'album_photo'), - 'people'=>array('through'=>'people_photo','far_key'=>'people_id'), + 'people'=>array('through'=>'photo_people','far_key'=>'people_id'), + 'tags'=>array('through'=>'photo_tag','far_key'=>'tag_id'), + 'photo_tag'=>array('far_key'=>'id'), + 'photo_people'=>array('far_key'=>'id'), ); /** @@ -65,6 +68,27 @@ class Model_Photo extends ORM { return trim(chunk_split(sprintf("%0{$depth}s",$this->id),$sep,'/'),'/'); } + public function file_path_short($path) { + return preg_replace(":^{$this->_path}".DIRECTORY_SEPARATOR.":",'',$path); + } + + public function delete() { + if (unlink($this->file_path())) { + // Remove any tags + foreach ($this->photo_tag->find_all() as $o) + $o->delete(); + + // Remove any people + foreach ($this->photo_people->find_all() as $o) + $o->delete(); + + return parent::delete(); + } + + // If unlink failed... + return FALSE; + } + public function file_size() { return filesize($this->file_path()); } @@ -121,15 +145,22 @@ class Model_Photo extends ORM { if (! $path) $path = $this->file_path(FALSE,TRUE); - $po = ORM::factory('Photo',$path); + // Check if there is already a phoot here - and if it is pending delete. + $po = ORM::factory('Photo',array('filename'=>$this->file_path_short($path))); - // If the file already exists, we'll ignore the move. - if ($po->loaded() OR file_exists($path) OR ! File::ParentDirExist(dirname($path),TRUE)) + // @todo Move any tags if we are replacing an existing photo. + if ($po->loaded() AND (! $po->remove OR ($po->remove AND ! $po->delete()))) + return FALSE; + + unset($po); + + // If the file already exists, or we cant create the dir structure, we'll ignore the move + if (file_exists($path) OR ! File::ParentDirExist(dirname($path),TRUE)) return FALSE; if (rename($this->file_path(),$path)) { // Convert the path to a relative one. - $this->filename = preg_replace(":^{$this->_path}/:",'',$path); + $this->filename = $this->file_path_short($path); // If the DB update failed, move it back. if (! $this->save() AND ! rename($path,$this->file_path())) @@ -176,11 +207,14 @@ class Model_Photo extends ORM { } } - public static function SignatureTrim($signature) { - return sprintf('%s...%s',substr($signature,0,6),substr($signature,-6)); + public static function SignatureTrim($signature,$chars=6) { + return sprintf('%s...%s',substr($signature,0,$chars),substr($signature,-1*$chars)); } public function thumbnail($rotate=TRUE) { + if (! $this->thumbnail) + return NULL; + $imo = new Imagick(); $imo->readImageBlob($this->thumbnail); @@ -191,6 +225,10 @@ class Model_Photo extends ORM { return $imo->getImageBlob(); } + public function thumbnail_sig() { + return md5($this->thumbnail()).':'.strlen($this->thumbnail()); + } + public function type($mime=FALSE) { return strtolower($mime ? File::mime_by_ext(pathinfo($this->filename,PATHINFO_EXTENSION)) : pathinfo($this->filename,PATHINFO_EXTENSION)); } diff --git a/application/classes/Model/Photo/People.php b/application/classes/Model/Photo/People.php new file mode 100644 index 0000000..273da9c --- /dev/null +++ b/application/classes/Model/Photo/People.php @@ -0,0 +1,14 @@ + diff --git a/application/classes/Model/Photo/Tag.php b/application/classes/Model/Photo/Tag.php new file mode 100644 index 0000000..4e5b5d6 --- /dev/null +++ b/application/classes/Model/Photo/Tag.php @@ -0,0 +1,14 @@ + diff --git a/application/classes/Model/Tags.php b/application/classes/Model/Tags.php new file mode 100644 index 0000000..4f109f3 --- /dev/null +++ b/application/classes/Model/Tags.php @@ -0,0 +1,14 @@ + diff --git a/application/classes/Task/Photo/Import.php b/application/classes/Task/Photo/Import.php index 4578960..4c07036 100644 --- a/application/classes/Task/Photo/Import.php +++ b/application/classes/Task/Photo/Import.php @@ -10,74 +10,177 @@ * @license http://dev.leenooks.net/license.html */ class Task_Photo_Import extends Minion_Task { + private $_log = '/tmp/photo_import.txt'; + private $_accepted = array( 'jpg', ); protected $_options = array( - 'file'=>NULL, // Photo File to Import - 'verbose'=>FALSE, // Photo File to Import + 'dir'=>NULL, // Directory of Photos to Import + 'file'=>NULL, // Photo File to Import + 'tags'=>NULL, + 'people'=>NULL, + 'ignoredupe'=>FALSE, // Skip duplicate photos + 'deletedupe'=>FALSE, // Skip duplicate photos + 'verbose'=>FALSE, // Photo File to Import ); + private function _adddir(&$value,$key,$path='') { + if ($path) + $value = sprintf('%s/%s',$path,$value); + } + protected function _execute(array $params) { - if (is_null($params['file'])) - throw new Kohana_Exception('Missing filename, please use --file='); + $tags = NULL; + $t = $p = array(); - if (preg_match('/@__thumb/',$params['file']) OR preg_match('/\/._/',$params['file'])) - return sprintf("Ignoring file [%s]\n",$params['file']); + if (is_null($params['file']) AND is_null($params['dir']) OR ($params['file'] AND $params['dir'])) + throw new Kohana_Exception('Missing filename, please use --file= OR --dir='); - if (! in_array(strtolower(pathinfo($params['file'],PATHINFO_EXTENSION)),$this->_accepted)) - return sprintf("Ignoring file [%s]\n",$params['file']); + if ($params['dir']) { + $files = array_diff(scandir($params['dir']),array('.','..')); - $po = ORM::factory('Photo',array('filename'=>$params['file'])); + array_walk($files,'static::_adddir',$params['dir']); - if (! $po->loaded()) - $po->filename = realpath($params['file']); + } else + $files = array($params['file']); - $po->date_taken = $this->dbcol(strtotime($po->io('exif:DateTime'))); - $po->signature = $this->dbcol($po->io()->getImageSignature()); - $po->make = $this->dbcol($po->io('exif:Make')); - $po->model = $this->dbcol($po->io('exif:Model')); - $po->height = $this->dbcol($po->io()->getImageheight()); - $po->width = $this->dbcol($po->io()->getImageWidth()); - $po->orientation = $this->dbcol($po->io()->getImageOrientation()); - $po->subsectime = $this->dbcol($po->io('exif:SubSecTimeOriginal')); - - $po->gps_lat = $this->dbcol($po->gps(preg_split('/,\s?/',$po->io()->getImageProperty('exif:GPSLatitude')),$po->io()->getImageProperty('exif:GPSLatitudeRef'))); - $po->gps_lon = $this->dbcol($po->gps(preg_split('/,\s?/',$po->io()->getImageProperty('exif:GPSLongitude')),$po->io()->getImageProperty('exif:GPSLongitudeRef'))); - - try { - $po->thumbnail = $this->dbcol(exif_thumbnail($po->filename)); - } catch (Exception $e) { + // Tags + if ($params['tags']) { + $tags = explode(',',$params['tags']); + $t = ORM::factory('Tags')->where('tag','IN',$tags)->find_all(); } - switch ($params['verbose']) { - case 1: - print_r($po->what_changed()); - break; - - case 2: - print_r($po->io()->getImageProperties()); - break; + // People + if ($params['people']) { + $tags = explode(',',$params['people']); + $p = ORM::factory('People')->where('tag','IN',$tags)->find_all(); } - if ($po->list_duplicate()->find_all()->count()) - $po->duplicate = '1'; + $c = 0; + foreach ($files as $file) { + if ($params['verbose']) + printf("Processing file [%s]\n",$file); - if (! $po->changed()) - return sprintf("Image [%s] already in DB: %s\n",$params['file'],$po->id); + if (preg_match('/@__thumb/',$file) OR preg_match('/\/._/',$file)) { + $this->writelog(sprintf("Ignoring file [%s]\n",$file)); + continue; + } - $po->save(); + if (! in_array(strtolower(pathinfo($file,PATHINFO_EXTENSION)),$this->_accepted)) { + $this->writelog(sprintf("Ignoring file [%s]\n",$file)); + continue; + } - if ($po->saved()) - return sprintf("Image [%s] stored in DB: %s\n",$params['file'],$po->id); + $c++; - return sprintf("Image [%s] processed in DB: %s\n",$params['file'],$po->id); + $po = ORM::factory('Photo',array('filename'=>$file)); + + if (! $po->loaded()) + $po->filename = realpath($file); + + $po->date_taken = $this->dbcol(strtotime($po->io('exif:DateTime'))); + $po->signature = $this->dbcol($po->io()->getImageSignature()); + $po->make = $this->dbcol($po->io('exif:Make')); + $po->model = $this->dbcol($po->io('exif:Model')); + $po->height = $this->dbcol($po->io()->getImageheight()); + $po->width = $this->dbcol($po->io()->getImageWidth()); + $po->orientation = $this->dbcol($po->io()->getImageOrientation()); + $po->subsectime = $this->dbcol($po->io('exif:SubSecTimeOriginal')); + + $po->gps_lat = $this->dbcol($po->gps(preg_split('/,\s?/',$po->io()->getImageProperty('exif:GPSLatitude')),$po->io()->getImageProperty('exif:GPSLatitudeRef'))); + $po->gps_lon = $this->dbcol($po->gps(preg_split('/,\s?/',$po->io()->getImageProperty('exif:GPSLongitude')),$po->io()->getImageProperty('exif:GPSLongitudeRef'))); + + try { + $po->thumbnail = $this->dbcol(exif_thumbnail($po->filename)); + } catch (Exception $e) { + } + + switch ($params['verbose']) { + case 1: + print_r($po->what_changed()); + break; + + case 2: + print_r($po->io()->getImageProperties()); + break; + } + + $x = $po->list_duplicate()->find_all(); + if (count($x)) { + $skip = FALSE; + + foreach ($x as $o) { + # We'll only ignore based on the same signature. + if ($params['ignoredupe'] AND ($po->signature == $o->signature)) { + $skip = TRUE; + $this->writelog(sprintf("Ignore file [%s], it's the same as [%s (%s)]\n",$po->filename,$o->id,$o->file_path())); + break; + + } elseif ($params['deletedupe'] AND ($po->signature == $o->signature) AND ($po->filename != $o->filename)) { + $skip = TRUE; + $this->writelog(sprintf("Delete file [%s], it's the same as [%s (%s)] with signature [%s]\n",$po->filename,$o->id,$o->file_path(),$po->signature)); + unlink($file); + } + } + + unset($x); + + if ($skip) + continue; + else + $po->duplicate = '1'; + } + + if (! $po->changed()) + $this->writelog(sprintf("Image [%s] already in DB: %s\n",$file,$po->id)); + + $po->save(); + + if ($po->saved()) + $this->writelog(sprintf("Image [%s] stored in DB: %s\n",$file,$po->id)); + + // Record our tags + foreach ($t as $o) { + $x = ORM::factory('Photo_Tag')->where('tag_id','=',$o->id)->where('photo_id','=',$po->id)->find(); + $x->tag_id = $o->id; + $x->photo_id = $po->id; + $x->save(); + } + + // Record our people + foreach ($p as $o) { + $x = ORM::factory('Photo_People',array('people_id'=>$o->id,'photo_id'=>$po->id)); + $x->people_id = $o->id; + $x->photo_id = $po->id; + $x->save(); + } + + unset($po); + unset($x); + unset($o); + } + + if ($c > 1) + return sprintf("Images processed: %s\n",$c); } // Force the return of a string or NULL private function dbcol($val,$noval=NULL) { return $val ? (string)$val : $noval; } + + private function writelog($msg) { + if (! $this->_log) + return; + + static $fh = NULL; + + if (is_null($fh)) + $fh = fopen($this->_log,'a+'); + + fwrite($fh,$msg); + } } ?> diff --git a/application/classes/Task/Photo/Move.php b/application/classes/Task/Photo/Move.php index 341cdad..ed1f07a 100644 --- a/application/classes/Task/Photo/Move.php +++ b/application/classes/Task/Photo/Move.php @@ -13,6 +13,8 @@ class Task_Photo_Move extends Minion_Task { protected $_options = array( 'file'=>NULL, // Photo File to Move 'batch'=>NULL, // Number of photos to move in a batch + 'useid'=>TRUE, // If date not in photo use ID + 'verbose'=>FALSE, // Show some debuggig ); protected function _execute(array $params) { @@ -33,9 +35,12 @@ class Task_Photo_Move extends Minion_Task { $c = 0; foreach ($p->find_all() as $po) { - if ($po->file_path() == $po->file_path(FALSE,TRUE)) + if ($po->file_path() == $po->file_path(FALSE,($params['useid'] OR $po->date_taken) ? TRUE : FALSE)) continue; + if ($params['verbose']) + printf("Processing [%s], file [%s] - newpath [%s]\n",$po->id,$po->file_path(),$po->file_path(FALSE,($params['useid'] OR $po->date_taken) ? TRUE : FALSE)); + if ($po->move()) printf("Photo [%s] moved to %s.\n",$po->id,$po->file_path()); else diff --git a/application/classes/Task/Photo/Stat.php b/application/classes/Task/Photo/Stat.php new file mode 100644 index 0000000..2f9f0ff --- /dev/null +++ b/application/classes/Task/Photo/Stat.php @@ -0,0 +1,32 @@ +find_all(); + + foreach ($p as $po) { + if (! file_exists($po->file_path())) { + printf("ID [%s] doesnt exist (%s)?\n",$po->id,$po->file_path()); + continue; + } + + if (($x=$po->io()->getImageSignature()) !== $po->signature) { + printf("ID [%s] signature doesnt match (%s) [%s != %s]?\n",$po->id,$po->file_path(),$po->signature,$x); + continue; + } + } + } +} +?>