diff --git a/application/bootstrap.php b/application/bootstrap.php index 7bb3dfb..2b6e1b9 100644 --- a/application/bootstrap.php +++ b/application/bootstrap.php @@ -1,7 +1,9 @@ request->post()) + foreach ($this->request->post('process') as $pid) { + if (! Arr::get($this->request->post('remove'),$pid,FALSE)) + continue; + + $po = ORM::factory('Photo',$pid); + + // If the photo is not marked as remove, or flagged, dont do it. + if (! $po->loaded() OR $po->flag OR ! $po->remove) + continue; + + + if (! is_writable(dirname($po->file_path()))) + $output .= sprintf('Dont have write permissions on %s',dirname($po->file_path())); + + $output .= sprintf('Removing %s (%s)',$po->id,$po->file_path()); + + } + + $p = ORM::factory('Photo'); + + // Review a specific photo, or the next one marked remove + if ($x=$this->request->param('id')) + $p->where('id','=',$x); + + else + $p->where('remove','=',TRUE) + ->where_open() + ->where('flag','!=',TRUE) + ->or_where('flag','is',NULL) + ->where_close(); + + $output .= Form::open(sprintf('%s/%s',strtolower($this->request->controller()),$this->request->action())); + + foreach ($p->find_all() as $po) { + $dp = $po->list_duplicate()->find_all(); + + $output .= Form::hidden('process[]',$po->id); + foreach ($dp as $dpo) + $output .= Form::hidden('process[]',$dpo->id); + + $output .= ''; + + 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%'))), + 'Signature'=>array('key'=>'signature'), + 'Date Taken'=>array('key'=>'date_taken()'), + 'File Modified'=>array('key'=>'date_file("m",TRUE)'), + 'File Created'=>array('key'=>'date_file("c",TRUE)'), + 'Filename'=>array('key'=>'file_path(TRUE,FALSE)'), + 'Proposed Name'=>array('key'=>'path()'), + 'Width'=>array('key'=>'width'), + 'Height'=>array('key'=>'height'), + 'Orientation'=>array('key'=>'orientation'), + 'Orientate'=>array('key'=>'rotation()'), + 'Make'=>array('key'=>'make'), + 'Model'=>array('key'=>'model'), + ) as $k=>$v) + $output .= $this->table_duplicate_details($dp,$po,$v['key'],$k,Arr::get($v,'value','%VALUE%')); + + foreach (array( + 'Delete'=>array('key'=>'id','value'=>'remove'), + ) as $k=>$v) + $output .= $this->table_duplicate_checkbox($dp,$po,$v['key'],$k,Arr::get($v,'value','%VALUE%')); + + $output .= '
'; + + break; + } + + $output .= '
'; + $output .= '
'; + $output .= ''; + $output .= ''; + $output .= '
'; + $output .= '
'; + + $output .= Form::close(); + + Block::factory() + ->title('Delete Photo:'.$po->id) + ->title_icon('icon-delete') + ->body($output); + } + public function action_details() { $po = ORM::factory('Photo',$this->request->param('id')); if (! $po->loaded()) @@ -32,7 +122,7 @@ class Controller_Photo extends Controller_TemplateDefault { $po = ORM::factory('Photo',$pid); $po->duplicate = Arr::get($this->request->post('duplicate'),$pid); - $po->delete = Arr::get($this->request->post('delete'),$pid); + $po->remove = Arr::get($this->request->post('remove'),$pid); $po->flag = Arr::get($this->request->post('flag'),$pid); $po->save(); @@ -40,20 +130,21 @@ class Controller_Photo extends Controller_TemplateDefault { $p = ORM::factory('Photo'); + // Review a specific photo, or the next one marked duplicate if ($x=$this->request->param('id')) $p->where('id','=',$x); else $p->where('duplicate','=',TRUE) ->where_open() - ->where('delete','!=',TRUE) - ->or_where('delete','is',NULL) + ->where('remove','!=',TRUE) + ->or_where('remove','is',NULL) ->where_close(); $output .= Form::open(sprintf('%s/%s',strtolower($this->request->controller()),$this->request->action())); foreach ($p->find_all() as $po) { - $dp = $po->duplicate_find()->find_all(); + $dp = $po->list_duplicate()->find_all(); // Check that there are still duplicates if ($dp->count() == 0) { @@ -73,20 +164,24 @@ class Controller_Photo extends Controller_TemplateDefault { 'Thumbnail'=>array('key'=>'id','value'=>HTML::anchor('/photo/view/%VALUE%',HTML::image('photo/thumbnail/%VALUE%'))), 'Signature'=>array('key'=>'signature'), 'Date Taken'=>array('key'=>'date_taken()'), - 'Filename'=>array('key'=>'filename'), - 'Proposed Name'=>array('key'=>'path()'), + 'File Modified'=>array('key'=>'date_file("m",TRUE)'), + 'File Created'=>array('key'=>'date_file("c",TRUE)'), + 'Filename'=>array('key'=>'file_path(TRUE,FALSE)'), + 'Proposed Name'=>array('key'=>'file_path(TRUE,TRUE)'), 'Width'=>array('key'=>'width'), 'Height'=>array('key'=>'height'), 'Orientation'=>array('key'=>'orientation'), + 'Orientate'=>array('key'=>'rotation()'), 'Make'=>array('key'=>'make'), 'Model'=>array('key'=>'model'), + 'Exif Diff'=>array('key'=>"propertydiff({$po->id})"), ) as $k=>$v) $output .= $this->table_duplicate_details($dp,$po,$v['key'],$k,Arr::get($v,'value','%VALUE%')); foreach (array( 'Flag'=>array('key'=>'id','value'=>'flag'), 'Duplicate'=>array('key'=>'id','value'=>'duplicate'), - 'Delete'=>array('key'=>'id','value'=>'delete'), + 'Delete'=>array('key'=>'id','value'=>'remove'), ) as $k=>$v) $output .= $this->table_duplicate_checkbox($dp,$po,$v['key'],$k,Arr::get($v,'value','%VALUE%')); @@ -96,7 +191,7 @@ class Controller_Photo extends Controller_TemplateDefault { } $output .= '
'; - $output .= '
'; + $output .= '
'; $output .= ''; $output .= ''; $output .= '
'; @@ -157,24 +252,29 @@ class Controller_Photo extends Controller_TemplateDefault { return $output; } + private function evaluate(Model $o,$param) { + $result = NULL; + + if (preg_match('/\(/',$param) OR preg_match('/-\>/',$param)) + eval("\$result = \$o->$param;"); + else + $result = $o->display($param); + + return $result; + } + private function table_duplicate_details(Database_MySQL_Result $dp,Model_Photo $po,$param,$title='',$content='') { $output = ''; - if (preg_match('/\(/',$param) OR preg_match('/-\>/',$param)) - eval("\$d = \$po->$param;"); - else - $d = $po->display($param); + $v = $this->evaluate($po,$param); $output .= sprintf('%s',$title); - $output .= sprintf('%s',$content ? str_replace('%VALUE%',$d,$content) : $d); + $output .= sprintf('%s',$content ? str_replace('%VALUE%',$v,$content) : $v); foreach ($dp as $dpo) { - if (preg_match('/\(/',$param) OR preg_match('/-\>/',$param)) - eval("\$d = \$dpo->$param;"); - else - $d = $dpo->display($param); + $d = $this->evaluate($dpo,$param); - $output .= sprintf('%s',$content ? str_replace('%VALUE%',$d,$content) : $d); + $output .= sprintf('%s',($d==$v ? 'success' : 'warning'),$content ? str_replace('%VALUE%',$d,$content) : $d); } $output .= ''; diff --git a/application/classes/File.php b/application/classes/File.php new file mode 100644 index 0000000..1d329fb --- /dev/null +++ b/application/classes/File.php @@ -0,0 +1,24 @@ + diff --git a/application/classes/Model/Photo.php b/application/classes/Model/Photo.php index 90b86d6..8205f28 100644 --- a/application/classes/Model/Photo.php +++ b/application/classes/Model/Photo.php @@ -10,7 +10,7 @@ * @license http://dev.leenooks.net/license.html */ class Model_Photo extends ORM { - private $path = '/mnt/net/qnap/Multimedia/Photos'; + private $_path = '/mnt/net/qnap/Multimedia/Photos'; private $_io = NULL; protected $_has_many = array( @@ -33,44 +33,27 @@ class Model_Photo extends ORM { ), ); - public function duplicate_find() { - $po = ORM::factory($this->_object_name); - - if ($this->loaded()) - $po->where('id','!=',$this->id); - - $po->where_open(); - $po->where('delete','!=',TRUE); - $po->or_where('delete','is',NULL); - $po->where_close(); - - $po->where_open(); - $po->where('signature','=',$this->signature); - - if ($this->date_taken AND ($this->model OR $this->make)) { - $po->or_where_open(); - $po->where('date_taken','=',$this->date_taken); - $po->where('subsectime','=',$this->subsectime); - - if (! is_null($this->model)) - $po->and_where('model','=',$this->model); - - if (! is_null($this->make)) - $po->and_where('make','=',$this->make); - $po->where_close(); + public function date_file($type,$format=FALSE) { + switch ($type) { + case 'a': $t = fileatime($this->file_path()); + break; + case 'c': $t = filectime($this->file_path()); + break; + case 'm': $t = filemtime($this->file_path()); + break; } - $po->where_close(); - - return $po; + return $format ? Site::Datetime($t) : $t; } public function date_taken() { return $this->display('date_taken').($this->subsectime ? '.'.$this->subsectime : ''); } - public function duplicate_get() { - return $this->duplicate_find()->find_all(); + public function file_path($short=FALSE,$new=FALSE) { + $file = $new ? sprintf('%s_%03s.%s',date('Y/m/d-His',$this->date_taken),$this->subsectime,$this->type()) : $this->filename; + + return (($short OR preg_match('/^\//',$file)) ? '' : $this->_path.DIRECTORY_SEPARATOR).$file; } public function gps(array $coordinate,$hemisphere) { @@ -100,7 +83,9 @@ class Model_Photo extends ORM { public function image() { $imo = $this->io(); - $imo->removeImageProfile('exif'); + if (array_key_exists('exif',$imo->getImageProfiles())) + $imo->removeImageProfile('exif'); + $this->rotate($imo); return $imo->getImageBlob(); @@ -114,29 +99,45 @@ class Model_Photo extends ORM { public function io($attr=NULL) { if (is_nulL($this->_io)) - $this->_io = new Imagick($this->path.'/'.$this->filename); + $this->_io = new Imagick($this->_path.DIRECTORY_SEPARATOR.$this->filename); return is_null($attr) ? $this->_io : $this->_io->getImageProperty($attr); } - public function path() { - $path = ''; - $ao = $this->album->where('primary','=',TRUE)->find(); + public function move($path='') { + if (! $path) + $path = $this->file_path(FALSE,TRUE); - if ($ao->loaded()) { - $path .= $ao->path.'/'; + $po = ORM::factory('Photo',$path); - if ($ao->subpath_age) { - $po = $this->people->where('primary','=',TRUE)->find(); - $path .= $po->age($this->date_taken).'/'; - } + // If the file already exists, we'll ignore the move. + if ($po->loaded() OR file_exists($path) OR ! File::ParentDirExist(dirname($path),TRUE)) + return FALSE; + + if (rename($this->file_path(FALSE,FALSE),$path)) { + $this->filename = preg_replace(":^{$this->_path}/:",'',$path); + + // If the DB update failed, move it back. + if (! $this->save() AND ! rename($path,$this->file_path())) + throw new Kohana_Exception('Error: Unable to move file, ID: :id, OLD: :oldname, NEW: :newname', + array(':id'=>$this->id,':oldname'=>$this->file_path(),':newname'=>$this->file_path())); + + return TRUE; } - - $path .= sprintf('%s_%03s.%s',date('Ymd-His',$this->date_taken),$this->subsectime,$this->type()); - - return $path; } + public function propertydiff($id) { + if ($id == $this->id) + return; + + $po = ORM::factory($this->_object_name,$id); + if (! $po->loaded()) + return; + + $result = array_diff_assoc($this->info(),$po->info()); + + return join('|',array_keys($result)); + } private function rotate(Imagick $imo) { switch ($this->orientation) { case 3: $imo->rotateImage(new ImagickPixel('none'),180); @@ -148,15 +149,28 @@ class Model_Photo extends ORM { } } - public static function Signaturetrim($signature) { + public function rotation() { + switch ($this->orientation) { + case 1: return 'None!'; + case 3: return 'Upside Down'; + case 6: return 'Rotate Right'; + case 8: return 'Rotate Left'; + default: + return 'unknown?'; + } + } + + public static function SignatureTrim($signature) { return sprintf('%s...%s',substr($signature,0,6),substr($signature,-6)); } - public function thumbnail() { + public function thumbnail($rotate=TRUE) { $imo = new Imagick(); $imo->readImageBlob($this->thumbnail); - $this->rotate($imo); + + if ($rotate) + $this->rotate($imo); return $imo->getImageBlob(); } @@ -164,5 +178,41 @@ class Model_Photo extends ORM { public function type($mime=FALSE) { return strtolower($mime ? File::mime_by_ext(pathinfo($this->filename,PATHINFO_EXTENSION)) : pathinfo($this->filename,PATHINFO_EXTENSION)); } + + public function list_duplicate() { + $po = ORM::factory($this->_object_name); + + if ($this->loaded()) + $po->where('id','!=',$this->id); + + // Ignore photo's pending removal. + $po->where_open(); + $po->where('remove','!=',TRUE); + $po->or_where('remove','is',NULL); + $po->where_close(); + + // Where the signature is the same + $po->where_open(); + $po->where('signature','=',$this->signature); + + // Or they have the same time taken with the same camera + if ($this->date_taken AND ($this->model OR $this->make)) { + $po->or_where_open(); + $po->where('date_taken','=',$this->date_taken); + $po->where('subsectime','=',$this->subsectime); + + if (! is_null($this->model)) + $po->and_where('model','=',$this->model); + + if (! is_null($this->make)) + $po->and_where('make','=',$this->make); + + $po->where_close(); + } + + $po->where_close(); + + return $po; + } } ?> diff --git a/application/classes/Task/Photo/Import.php b/application/classes/Task/Photo/Import.php index 2facb6c..2a4c2e9 100644 --- a/application/classes/Task/Photo/Import.php +++ b/application/classes/Task/Photo/Import.php @@ -54,7 +54,7 @@ class Task_Photo_Import extends Minion_Task { break; } - if ($po->duplicate_get()->count()) + if ($po->list_duplicate()->find_all()->count()) $po->duplicate = '1'; if (! $po->changed()) @@ -68,6 +68,7 @@ class Task_Photo_Import extends Minion_Task { return sprintf("Image [%s] processed in DB: %s\n",$params['file'],$po->id); } + // Force the return of a string or NULL private function dbcol($val,$noval=NULL) { return $val ? (string)$val : $noval; } diff --git a/application/classes/Task/Photo/Move.php b/application/classes/Task/Photo/Move.php new file mode 100644 index 0000000..2c0cce4 --- /dev/null +++ b/application/classes/Task/Photo/Move.php @@ -0,0 +1,54 @@ +NULL, // Photo File to Move + 'batch'=>NULL, // Number of photos to move in a batch + ); + + protected function _execute(array $params) { + if ($params['file']) { + $po = ORM::factory('Photo',array('filename'=>$params['file'])); + + } else { + $p = ORM::factory('Photo') + ->where('date_taken','is not',NULL) + ->where_open() + ->where('remove','!=',TRUE) + ->or_where('remove','is',NULL) + ->where_close() + ->where_open() + ->where('duplicate','!=',TRUE) + ->or_where('duplicate','is',NULL) + ->where_close(); + } + + $c = 0; + foreach ($p->find_all() as $po) { + if ($po->file_path() == $po->file_path(FALSE,TRUE)) + continue; + + if ($po->move()) + printf("Photo [%s] moved to %s.\n",$po->id,$po->file_path()); + else + printf("Photo [%s] NOT moved to %s.\n",$po->id,$po->file_path(FALSE,TRUE)); + + $c++; + + if (! is_null($params['batch']) AND $c >= $params['batch']) + break; + } + + return sprintf("Images processed [%s]\n",$c); + } +} +?> diff --git a/application/config/database.php b/application/config/database.php index 5b249f5..c57afc1 100644 --- a/application/config/database.php +++ b/application/config/database.php @@ -20,7 +20,7 @@ return array */ 'hostname' => 'mysql.leenooks.vpn', 'database' => 'weblnphoto', - 'username' => 'ln-webphoto', + 'username' => 'ln-photo', 'password' => 'Ph0T0!', 'persistent' => TRUE, ),