Optimised scanning and importing

This commit is contained in:
Deon George 2018-01-11 23:59:53 +11:00
parent 96fadc5080
commit cfc9bf5d9a
26 changed files with 1542 additions and 759 deletions

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ Homestead.yaml
Homestead.json
.env
.*.swp
/storage/debugbar
/config/debugbar.php

View File

@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class CatalogDump extends Command
{
// Our photo object
private $o = NULL;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'catalog:dump {type : Photo | Video } {id : Photo ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Scan Photo for metadata';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$class = 'App\Model\\'.$this->argument('type');
if (! class_exists($class))
abort(500,sprintf('No class [%s]',$this->argument('type')));
$o = $class::findOrFail($this->argument('id'));
if (! is_readable($o->file_path()))
{
$this->warn(sprintf('Ignoring [%s], it is not readable',$o->file_path()));
exit;
}
print_r($o->properties());
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class CatalogScan extends Command
{
// Our photo object
private $o = NULL;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'catalog:scan {type : Photo | Video } {id : Photo ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Scan Photo for metadata';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$class = 'App\Model\\'.$this->argument('type');
if (! class_exists($class))
abort(500,sprintf('No class [%s]',$this->argument('type')));
$o = $class::findOrFail($this->argument('id'));
if (! is_readable($o->file_path()))
{
$this->warn(sprintf('Ignoring [%s], it is not readable',$o->file_path()));
exit;
}
$o->setDateCreated();
$o->setSubSecTime();
$o->setSignature();
$o->setMakeModel();
$o->setLocation();
$o->setHeightWidth();
$o->setThumbnail();
// If this is a duplicate
$x = $class::whereIN('id',$o->list_duplicate())->get();
if (count($x)) {
$break = FALSE;
foreach ($x as $oo) {
// If this photo siganture matches another.
if ($o->signature == $oo->signature)
{
// And that photo is not marked as a duplicate
if (! $oo->duplicate)
{
$o->duplicate = '1';
$this->warn(sprintf('Image [%s] marked as a duplicate',$o->file_path()));
// If the file signature also matches, we'll mark it for deletion
if ($oo->file_signature AND $o->file_signature == $oo->file_signature)
{
$this->warn(sprintf('Image [%s] marked for deletion',$o->file_path()));
$o->remove = '1';
}
break;
}
}
}
}
$o->scanned = '1';
if ($o->getDirty())
$this->warn(sprintf('Image [%s] metadata changed',$o->file_path()));
$o->save();
}
}

View File

@ -4,6 +4,7 @@ namespace App\Console\Commands;
use Log;
use Illuminate\Console\Command;
use Illuminate\Foundation\Bus\DispatchesJobs;
use App\Model\Person;
use App\Model\PhotoPerson;
use App\Model\Photo;
@ -12,6 +13,7 @@ use App\Model\Tag;
class PhotoImport extends Command
{
use DispatchesJobs;
use \App\Traits\Files;
/**
@ -21,7 +23,7 @@ class PhotoImport extends Command
*/
protected $signature = 'photo:import
{--dir= : Directory to Parse}
{--file= : File to import}
{--file= : File to Import}
{--ignoredupe : Ignore duplicate files}
{--deletedupe : Delete duplicate files}
{--people= : People to reference in photo}
@ -108,90 +110,39 @@ class PhotoImport extends Command
$c++;
$po = Photo::where('filename',$file)->first();
$o = Photo::where('filename',$file)->first();
if (is_null($po))
if (is_null($o))
{
$po = new Photo;
$po->filename = $file;
$o = new Photo;
$o->filename = $file;
}
if (! is_readable($po->file_path()))
if (! is_readable($o->file_path()))
{
$this->warn(sprintf('Ignoring [%s], it is not readable',$po->file_path()));
$this->warn(sprintf('Ignoring [%s], it is not readable',$o->file_path()));
continue;
}
$po->date_taken = strtotime($po->property('exif:DateTime') ? $po->property('exif:DateTime') : $po->property('exif:DateTimeOriginal'));
$po->subsectime = $po->property('exif:SubSecTimeOriginal');
if ($o->exists)
$this->warn(sprintf('%s [%s] already in DB: %s',$o->objectType(),$file,$o->id));
$po->signature = $po->property('signature');
$o->save();
$po->make = $po->property('exif:Make');
$po->model = $po->property('exif:Model');
$po->height = $po->property('height');
$po->width = $po->property('width');
$po->orientation = $po->property('orientation');
$po->gps_lat = Photo::latlon(preg_split('/,\s?/',$po->property('exif:GPSLatitude')),$po->property('exif:GPSLatitudeRef'));
$po->gps_lon = Photo::latlon(preg_split('/,\s?/',$po->property('exif:GPSLongitude')),$po->property('exif:GPSLongitudeRef'));
try {
$po->thumbnail = exif_thumbnail($po->file_path());
} catch (\Exception $e) {
// @todo Couldnt get the thumbnail, so we should create one.
}
// If this is a duplicate
$x = Photo::whereIN('id',$po->list_duplicate())->get();
if (count($x)) {
$skip = FALSE;
foreach ($x as $o) {
// We'll only ignore based on the same signature.
if ($po->signature != $o->signature AND ! $po->exists)
continue;
if ($this->option('ignoredupe'))
{
$skip = TRUE;
$this->warn(sprintf("Ignoring file [%s], it's the same as [%s] with id %s",$po->file_path(),$o->filename,$o->id));
break;
}
elseif ($this->option('deletedupe') AND ($po->filename != $o->filename))
{
$skip = TRUE;
$this->error(sprintf("Deleting file [%s], it's the same as [%s] with id %s and signature [%s]\n",$po->file_path(),$o->filename,$o->id,$po->signature));
unlink($po->file_path());
}
}
if ($skip)
continue;
$po->duplicate = '1';
$this->warn(sprintf('Image [%s] marked as a duplicate',$file));
}
if ($po->exists)
$this->warn(sprintf('Image [%s] already in DB: %s',$file,$po->id));
$po->save();
if ($po->wasRecentlyCreated)
$this->info(sprintf('Image [%s] stored in DB: %s',$file,$po->id));
if ($o->wasRecentlyCreated)
$this->info(sprintf('%s [%s] stored in DB: %s',$o->objectType(),$file,$o->id));
// Record our people and tags
if ($p)
$po->People()->sync($p);
$o->People()->sync($p);
if ($t)
$po->Tags()->sync($t);
$o->Tags()->sync($t);
$this->dispatch((new \App\Jobs\CatalogScan($o))->onQueue('scan'));
}
$bar->finish();
return $this->info(sprintf('Images processed: %s',$c));
return $this->info(sprintf('Photos processed: %s',$c));
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Log;
use App\Model\Photo;
use App\Jobs\CatalogScan;
class PhotoScanAll extends Command
{
use DispatchesJobs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'photo:scanall';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
Photo::NotScanned()->chunk(200,function ($data) {
foreach ($data as $o)
{
if ($o->remove) {
Log::warning(sprintf('Not scanning [%s], marked for removal',$o->id));
continue;
}
$this->dispatch((new CatalogScan($o))->onQueue('scan'));
}
});
}
}

View File

@ -4,13 +4,14 @@ namespace App\Console\Commands;
use Log;
use Illuminate\Console\Command;
use Illuminate\Foundation\Bus\DispatchesJobs;
use App\Model\Video;
use App\Model\Tag;
use App\Model\Person;
class VideoImport extends Command
{
use DispatchesJobs;
use \App\Traits\Files;
/**
@ -108,91 +109,42 @@ class VideoImport extends Command
$c++;
$vo = Video::where('filename',$file)->first();
$o = Video::where('filename',$file)->first();
if (is_null($vo))
if (is_null($o))
{
$vo = new Video;
$vo->filename = $file;
$o = new Video;
$o->filename = $file;
}
if (! is_readable($vo->file_path()))
if (! is_readable($o->file_path()))
{
$this->warn(sprintf('Ignoring [%s], it is not readable',$vo->file_path()));
$this->warn(sprintf('Ignoring [%s], it is not readable',$o->file_path()));
continue;
}
$vo->date_created = strtotime($vo->property('creationdate'));
$vo->signature = $vo->property('signature');
if ($o->exists)
$this->warn(sprintf('%s [%s] already in DB: %s',$o->objectType(),$file,$o->id));
$vo->make = $vo->property('make');
$vo->model = $vo->property('model');
$o->save();
$vo->height = $vo->property('height');
$vo->width = $vo->property('width');
$vo->type = $vo->property('type');
$vo->length = $vo->property('length');
$vo->codec = $vo->property('codec');
$vo->audiochannels = $vo->property('audiochannels');
$vo->channelmode = $vo->property('channelmode');
$vo->samplerate = $vo->property('samplerate');
$vo->gps_lat = $vo->property('gps_lat');
$vo->gps_lon = $vo->property('gps_lon');
$vo->gps_altitude = $vo->property('gps_altitude');
// If this is a duplicate
$x = Video::whereIN('id',$vo->list_duplicate())->get();
if (count($x)) {
$skip = FALSE;
foreach ($x as $o) {
// We'll only ignore based on the same signature.
if ($vo->signature != $o->signature AND ! $vo->exists)
continue;
if ($this->option('ignoredupe'))
{
$skip = TRUE;
$this->warn(sprintf("Ignoring file [%s], it's the same as [%s] with id %s",$vo->file_path(),$o->filename,$o->id));
break;
}
elseif ($this->option('deletedupe') AND ($vo->filename != $o->filename))
{
$skip = TRUE;
$this->error(sprintf("Deleting file [%s], it's the same as [%s] with id %s and signature [%s]\n",$vo->file_path(),$o->filename,$o->id,$vo->signature));
unlink($vo->file_path());
}
}
if ($skip)
continue;
$vo->duplicate = '1';
$this->warn(sprintf('Video [%s] marked as a duplicate',$file));
}
if ($vo->exists)
$this->warn(sprintf('Video [%s] already in DB: %s',$file,$vo->id));
$vo->save();
if ($vo->wasRecentlyCreated)
$this->info(sprintf('Video [%s] stored in DB: %s',$file,$vo->id));
if ($o->wasRecentlyCreated)
$this->info(sprintf('%s [%s] stored in DB: %s',$o->objectType(),$file,$o->id));
// Record our people and tags
if ($p)
$vo->People()->sync($p);
$o->People()->sync($p);
if ($t)
$vo->Tags()->sync($t);
$o->Tags()->sync($t);
$this->dispatch((new \App\Jobs\CatalogScan($o))->onQueue('scan'));
if ($this->option('dumpid3'))
dd($vo->dump());
dd($o->dump());
}
$bar->finish();
return $this->info(sprintf('Video processed: %s',$c));
return $this->info(sprintf('Videos processed: %s',$c));
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Log;
use App\Model\Video;
use App\Jobs\CatalogScan;
class VideoScanAll extends Command
{
use DispatchesJobs;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'video:scanall';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
Video::NotScanned()->chunk(200,function ($data) {
foreach ($data as $o)
{
if ($o->remove) {
Log::warning(sprintf('Not scanning [%s], marked for removal',$o->id));
continue;
}
$this->dispatch((new CatalogScan($o))->onQueue('scan'));
}
});
}
}

View File

@ -13,11 +13,15 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
Commands\CatalogScan::class,
Commands\CatalogDump::class,
Commands\PhotoImport::class,
Commands\PhotoMove::class,
Commands\PhotoScanAll::class,
Commands\PhotoUpdate::class,
Commands\VideoImport::class,
Commands\VideoMove::class,
Commands\VideoScanAll::class,
];
/**

View File

@ -34,19 +34,19 @@ class PhotoController extends Controller
return redirect()->action('PhotoController@info',[$id]);
}
public function deletes()
public function deletes($id=NULL)
{
return view('photo.deletereview',['photos'=>Photo::where('remove',1)->paginate(2)]);
return view('catalog.deletereview',['return'=>url('/p/deletes'),'catalog'=>is_null($id) ? Photo::where('remove',1)->paginate(50) : Photo::where('id',$id)]);
}
public function deletesUpdate(Request $request)
{
foreach ($request->input('photo') as $id)
foreach ($request->input('remove') as $id=>$k)
{
$photo = Photo::findOrFail($id);
$o = Photo::findOrFail($id);
if ($photo->remove AND $request->input('remove.'.$id))
$this->dispatch((new PhotoDelete($photo))->onQueue('delete'));
if ($o->remove AND $request->input('remove.'.$id))
$this->dispatch((new PhotoDelete($o))->onQueue('delete'));
}
return redirect()->action('PhotoController@deletes',$request->input('pagenext') ? '?page='.$request->input('pagenext') : NULL);

View File

@ -35,19 +35,19 @@ class VideoController extends Controller
return redirect()->action('VideoController@info',[$id]);
}
public function deletes()
public function deletes($id=NULL)
{
return view('video.deletereview',['videos'=>Video::where('remove',1)->paginate(2)]);
return view('catalog.deletereview',['return'=>url('v/deletes'),'catalog'=>is_null($id) ? Video::where('remove',1)->paginate(10) : Video::where('id',$id)->paginate(1)]);
}
public function deletesUpdate(Request $request)
{
foreach ($request->input('video') as $id)
foreach ($request->input('remove') as $k=>$id)
{
$video = Video::findOrFail($id);
$o = Video::findOrFail($k);
if ($video->remove AND $request->input('remove.'.$id))
$this->dispatch((new VideoDelete($video))->onQueue('delete'));
if ($o->remove AND $request->input('remove.'.$k))
$this->dispatch((new VideoDelete($o))->onQueue('delete'));
}
return redirect()->action('VideoController@deletes',$request->input('pagenext') ? '?page='.$request->input('pagenext') : NULL);

39
app/Jobs/CatalogScan.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace App\Jobs;
use Log;
use App\Jobs\Job;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Artisan;
class CatalogScan extends Job implements ShouldQueue
{
use InteractsWithQueue, SerializesModels;
// Our object
private $o = NULL;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(\App\Model\Abstracted\Catalog $o)
{
$this->o = $o;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Log::info(sprintf('%s: Scanning [%s|%s]',__METHOD__,$this->o->objecttype(),$this->o->id));
Artisan::call('catalog:scan',['type'=>$this->o->objecttype(),'id'=>$this->o->id]);
}
}

View File

@ -0,0 +1,353 @@
<?php
namespace App\Model\Abstracted;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Schema;
use DB;
abstract class Catalog extends Model
{
public function People()
{
return $this->belongsToMany('App\Model\Person');
}
public function Tags()
{
return $this->belongsToMany('App\Model\Tag');
}
/**
* Multimedia NOT duplicate.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNotDuplicate($query)
{
return $query->where(function($query)
{
$query->where('duplicate','!=',TRUE)
->orWhere('duplicate','=',NULL);
});
}
/**
* Multimedia NOT pending removal.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNotRemove($query)
{
return $query->where(function($query)
{
$query->where('remove','!=',TRUE)
->orWhere('remove','=',NULL);
});
}
/**
* Multimedia NOT scanned.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNotScanned($query)
{
return $query->where(function($query)
{
$query->where('scanned','!=',TRUE)
->orWhere('scanned','=',NULL);
});
}
abstract public function setDateCreated();
abstract public function setLocation();
abstract public function setMakeModel();
abstract public function setSignature();
abstract public function setSubSecTime();
abstract public function setThumbnail();
abstract public function view();
/**
* Date the multimedia was created
*/
public function date_taken()
{
return $this->date_created ? date('Y-m-d H:i:s',$this->date_created) : 'UNKNOWN';
}
/**
* Return the date of the file
*/
public function file_date($type,$format=FALSE)
{
if (! is_readable($this->file_path()))
return NULL;
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;
}
return $format ? date('d-m-Y H:i:s',$t) : $t;
}
/**
* Determine the new name for the image
*/
public function file_path($short=FALSE,$new=FALSE)
{
$file = $this->filename;
if ($new)
$file = sprintf('%s.%s',((is_null($this->date_created) OR ! $this->date_created)
? sprintf('UNKNOWN/%07s',$this->file_path_id())
: date('Y/m/d-His',$this->date_created)),$this->type());
return (($short OR preg_match('/^\//',$file)) ? '' : config('video.dir').DIRECTORY_SEPARATOR).$file;
}
/**
* Calculate a file path ID based on the id of the file
*/
public function file_path_id($sep=3,$depth=9)
{
return trim(chunk_split(sprintf("%0{$depth}s",$this->id),$sep,'/'),'/');
}
/**
* Display the file signature
*/
public function file_signature($short=FALSE)
{
return $short ? static::signaturetrim($this->file_signature) : $this->file_signature;
}
/**
* Return the photo size
*/
public function file_size()
{
return (! is_readable($this->file_path())) ? NULL : filesize($this->file_path());
}
public function getFileSizeAttribute()
{
return (! is_readable($this->file_path())) ? NULL : filesize($this->file_path());
}
public function getDateCreatedTextAttribute()
{
if ($this->date_created)
return date('d-m-Y H:i:s',$this->date_created);
}
public function getDuplicateTextAttribute()
{
return $this->TextTrueFalse($this->duplicate);
}
public function getFlagTextAttribute()
{
return $this->TextTrueFalse($this->flag);
}
/**
* Return HTML Checkbox for flagged
*/
public function getFlagCheckboxAttribute()
{
return $this->HTMLCheckbox('flag',$this->id,$this->flag);
}
/**
* Return HTML Checkbox for remove
*/
public function getRemoveCheckboxAttribute()
{
return $this->HTMLCheckbox('remove',$this->id,$this->remove);
}
/**
* Display the GPS coordinates
*/
public function gps()
{
return ($this->gps_lat AND $this->gps_lon) ? sprintf('%s/%s',$this->gps_lat,$this->gps_lon) : 'UNKNOWN';
}
/**
* Get ID Info link
*/
protected function HTMLLinkAttribute($id,$url)
{
return sprintf('<a href="%s" target="%s">%s</a>',url($url.$id),$id,$id);
}
/**
* Return an HTML checkbox
*/
protected function HTMLCheckbox($name,$id,$value)
{
return sprintf('<input type="checkbox" name="%s[%s]" value="1"%s>',$name,$id,$value ? ' checked="checked"' : '');
}
public function isParentWritable($dir)
{
if (file_exists($dir) AND is_writable($dir) AND is_dir($dir))
return TRUE;
elseif ($dir == dirname($dir) OR file_exists($dir))
return FALSE;
else
return ($this->isParentWritable(dirname($dir)));
}
/**
* Determine if a file is moveable
*
* useID boolean Determine if the path is based on the the ID or date
*/
public function moveable()
{
// If the source and target are the same, we dont need to move it
if ($this->file_path() == $this->file_path(FALSE,TRUE))
return FALSE;
// If there is already a file in the target.
// @todo If the target file is to be deleted, we could move this file
if (file_exists($this->file_path(FALSE,TRUE)))
return 1;
// Test if the source is readable
if (! is_readable($this->file_path()))
return 2;
// Test if the dir is writable (so we can remove the file)
if (! $this->isParentWritable(dirname($this->file_path())))
return 3;
// Test if the target dir is writable
// @todo The target dir may not exist yet, so we should check that a parent exists and is writable.
if (! $this->isParentWritable($this->file_path(FALSE,TRUE)))
return 4;
return TRUE;
}
/**
* Get the id of the previous record
*/
public function next()
{
return DB::table($this->getTable())
->where('id','>',$this->id)
->orderby('id','ASC')
->first();
}
/**
* Return my class shortname
*/
public function objectType()
{
return (new \ReflectionClass($this))->getShortName();
}
/**
* Get the id of the previous record
*/
public function previous()
{
return DB::table($this->getTable())
->where('id','<',$this->id)
->orderby('id','DEC')
->first();
}
public function setHeightWidth()
{
$this->height = $this->property('height');
$this->width = $this->property('width');
$this->orientation = $this->property('orientation');
}
/**
* Display the media signature
*/
public function signature($short=FALSE)
{
return $short ? static::signaturetrim($this->signature) : $this->signature;
}
public static function signaturetrim($signature,$chars=6)
{
return sprintf('%s...%s',substr($signature,0,$chars),substr($signature,-1*$chars));
}
/**
* Determine if the image should be moved
*/
public function shouldMove()
{
return ($this->filename != $this->file_path(TRUE,TRUE));
}
protected function TextTrueFalse($value)
{
return $value ? 'TRUE' : 'FALSE';
}
public function list_duplicate($includeme=FALSE)
{
return $this->list_duplicates($includeme)->pluck('id');
}
/**
* Find duplicate images based on some attributes of the current image
*/
public function list_duplicates($includeme=FALSE)
{
$o = static::select();
if ($this->id AND ! $includeme)
$o->where('id','!=',$this->id);
// Ignore photo's pending removal.
if (! $includeme)
$o->where(function($query)
{
$query->where('remove','!=',TRUE)
->orWhere('remove','=',NULL);
});
// Where the signature is the same
$o->where(function($query)
{
$query->where('signature','=',$this->signature);
// Or they have the same time taken with the same camera
if ($this->date_created AND ($this->model OR $this->make))
{
$query->orWhere(function($query)
{
$query->where('date_created','=',$this->date_created ? $this->date_created : NULL);
if (Schema::hasColumn($this->getTable(),'subsectime'))
$query->where('subsectime','=',$this->subsectime ? $this->subsectime : NULL);
if (! is_null($this->model))
$query->where('model','=',$this->model);
if (! is_null($this->make))
$query->where('make','=',$this->make);
});
}
});
return $o->get();
}
}

View File

@ -3,14 +3,13 @@
namespace App\Model;
use DB;
use Illuminate\Database\Eloquent\Model;
class Photo extends Model
class Photo extends Abstracted\Catalog
{
protected $table = 'photo';
// Imagick Object
private $_io;
private $_o;
// How should the image be rotated, based on the value of orientation
private $_rotate = [
@ -19,68 +18,12 @@ class Photo extends Model
8=>-90,
];
public function People()
{
return $this->belongsToMany('App\Model\Person');
}
public function Tags()
{
return $this->belongsToMany('App\Model\Tag');
}
/**
* Photo's NOT pending removal.
*
* @return \Illuminate\Database\Eloquent\Builder
* Date the photo was taken
*/
public function scopeNotRemove($query)
{
return $query->where(function($query)
{
$query->where('remove','!=',TRUE)
->orWhere('remove','=',NULL);
});
}
/**
* Photo's NOT duplicate.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNotDuplicate($query)
{
return $query->where(function($query)
{
$query->where('duplicate','!=',TRUE)
->orWhere('duplicate','=',NULL);
});
}
public function date_taken()
{
return $this->date_taken ? (date('Y-m-d H:i:s',$this->date_taken).($this->subsectime ? '.'.$this->subsectime : '')) : 'UNKNOWN';
}
/**
* Return the date of the file
*/
public function file_date($type,$format=FALSE)
{
if (! is_readable($this->file_path()))
return NULL;
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;
}
return $format ? date('d-m-Y H:i:s',$t) : $t;
return $this->date_created ? (date('Y-m-d H:i:s',$this->date_created).($this->subsectime ? '.'.$this->subsectime : '')) : 'UNKNOWN';
}
/**
@ -91,35 +34,17 @@ class Photo extends Model
$file = $this->filename;
if ($new)
$file = sprintf('%s.%s',((is_null($this->date_taken) OR ! $this->date_taken)
$file = sprintf('%s.%s',((is_null($this->date_created) OR ! $this->date_created)
? sprintf('UNKNOWN/%07s',$this->file_path_id())
: sprintf('%s_%03s',date('Y/m/d-His',$this->date_taken),$this->subsectime).($this->subsectime ? '' : sprintf('-%05s',$this->id))),$this->type());
: sprintf('%s_%03s',date('Y/m/d-His',$this->date_created),$this->subsectime).
($this->subsectime ? '' : sprintf('-%05s',$this->id))),$this->type());
return (($short OR preg_match('/^\//',$file)) ? '' : config('photo.dir').DIRECTORY_SEPARATOR).$file;
}
/**
* Calculate a file path ID based on the id of the file
*/
public function file_path_id($sep=3,$depth=9)
public function getIDLinkAttribute()
{
return trim(chunk_split(sprintf("%0{$depth}s",$this->id),$sep,'/'),'/');
}
/**
* Return the photo size
*/
public function file_size()
{
return (! is_readable($this->file_path())) ? NULL : filesize($this->file_path());
}
/**
* Display the GPS coordinates
*/
public function gps()
{
return ($this->gps_lat AND $this->gps_lon) ? sprintf('%s/%s',$this->gps_lat,$this->gps_lon) : 'UNKNOWN';
return $this->HTMLLinkAttribute($this->id,url('p/info').'/');
}
/**
@ -127,7 +52,7 @@ class Photo extends Model
*/
public function image()
{
if (is_null($imo = $this->io()))
if (is_null($imo = $this->o()))
return NULL;
if (array_key_exists('exif',$imo->getImageProfiles()))
@ -138,31 +63,6 @@ class Photo extends Model
return $imo->getImageBlob();
}
/**
* Return an Imagick object or attribute
*
*/
protected function io($attr=NULL)
{
if (! file_exists($this->file_path()) OR ! is_readable($this->file_path()))
return NULL;
if (is_null($this->_io))
$this->_io = new \Imagick($this->file_path());
return is_null($attr) ? $this->_io : $this->_io->getImageProperty($attr);
}
public function isParentWritable($dir)
{
if (file_exists($dir) AND is_writable($dir) AND is_dir($dir))
return TRUE;
elseif ($dir == dirname($dir) OR file_exists($dir))
return FALSE;
else
return ($this->isParentWritable(dirname($dir)));
}
/**
* Calculate the GPS coordinates
*/
@ -193,46 +93,18 @@ class Photo extends Model
}
/**
* Determine if a file is moveable
* Return an Imagick object or attribute
*
* useID boolean Determine if the path is based on the the ID or date
*/
public function moveable()
protected function o($attr=NULL)
{
// If the source and target are the same, we dont need to move it
if ($this->file_path() == $this->file_path(FALSE,TRUE))
return FALSE;
if (! file_exists($this->file_path()) OR ! is_readable($this->file_path()))
return NULL;
// If there is already a file in the target.
// @todo If the target file is to be deleted, we could move this file
if (file_exists($this->file_path(FALSE,TRUE)))
return 1;
if (is_null($this->_o))
$this->_o = new \Imagick($this->file_path());
// Test if the source is readable
if (! is_readable($this->file_path()))
return 2;
// Test if the dir is writable (so we can remove the file)
if (! $this->isParentWritable(dirname($this->file_path())))
return 3;
// Test if the target dir is writable
// @todo The target dir may not exist yet, so we should check that a parent exists and is writable.
if (! $this->isParentWritable($this->file_path(FALSE,TRUE)))
return 4;
return TRUE;
}
/**
* Get the id of the previous photo
*/
public function next()
{
$po = DB::table('photo');
$po->where('id','>',$this->id);
$po->orderby('id','ASC');
return $po->first();
return is_null($attr) ? $this->_o : $this->_o->getImageProperty($attr);
}
/**
@ -251,7 +123,6 @@ class Photo extends Model
/**
* Rotate the image
*
*/
private function rotate(\Imagick $imo)
{
@ -261,68 +132,77 @@ class Photo extends Model
return $imo->getImageBlob();
}
/**
* Get the id of the previous photo
*/
public function previous()
{
$po = DB::table('photo');
$po->where('id','<',$this->id);
$po->orderby('id','DEC');
return $po->first();
}
public function property($property)
{
if (! $this->io())
if (! $this->o())
return NULL;
switch ($property)
{
case 'height': return $this->_io->getImageHeight(); break;
case 'orientation': return $this->_io->getImageOrientation(); break;
case 'signature': return $this->_io->getImageSignature(); break;
case 'width': return $this->_io->getImageWidth(); break;
case 'creationdate': return strtotime($this->property('exif:DateTimeOriginal') ? $this->property('exif:DateTimeOriginal') : $this->property('exif:DateTime')); break;
case 'height': return $this->_o->getImageHeight(); break;
case 'orientation': return $this->_o->getImageOrientation(); break;
case 'signature': return $this->_o->getImageSignature(); break;
case 'width': return $this->_o->getImageWidth(); break;
default:
return $this->_io->getImageProperty($property);
return $this->_o->getImageProperty($property);
}
}
public function properties()
{
return $this->io() ? $this->_io->getImageProperties() : [];
return $this->o() ? $this->_o->getImageProperties() : [];
}
/**
* Display the photo signature
*/
public function signature($short=FALSE)
public function setDateCreated()
{
return $short ? static::signaturetrim($this->signature) : $this->signature;
if ($this->property('creationdate'))
$this->date_created = $this->property('creationdate');
}
public static function signaturetrim($signature,$chars=6)
public function setLocation()
{
return sprintf('%s...%s',substr($signature,0,$chars),substr($signature,-1*$chars));
$this->gps_lat = static::latlon(preg_split('/,\s?/',$this->property('exif:GPSLatitude')),$this->property('exif:GPSLatitudeRef'));
$this->gps_lon = static::latlon(preg_split('/,\s?/',$this->property('exif:GPSLongitude')),$this->property('exif:GPSLongitudeRef'));
#$this->gps_altitude = $this->property('gps_altitude');
}
/**
* Determine if the image should be moved
*/
public function shouldMove()
public function setMakeModel()
{
return ($this->filename != $this->file_path(TRUE,TRUE));
$this->make = $this->property('exif:Make') ? $this->property('exif:Make') : NULL;
$this->model = $this->property('exif:Model') ? $this->property('exif:Model') : NULL;
$this->software = $this->property('exif:Software') ? $this->property('exif:Software') : NULL;
}
public function setSignature()
{
$this->signature = $this->property('signature');
$this->file_signature = md5_file($this->file_path());
#$this->identifier = $this->property('identifier');
}
public function setSubSecTime()
{
$this->subsectime = $this->property('exif:SubSecTimeOriginal');
}
public function setThumbnail()
{
try {
$this->thumbnail = exif_thumbnail($this->file_path());
} catch (\Exception $e) {
// @todo Couldnt get the thumbnail, so we should create one.
}
}
/**
* Return the image's thumbnail
*
*/
public function thumbnail($rotate=TRUE)
{
if (! $this->thumbnail)
{
return $this->io()->thumbnailimage(200,200,true,true) ? $this->_io->getImageBlob() : NULL;
return $this->o()->thumbnailimage(200,200,true,true) ? $this->_o->getImageBlob() : NULL;
}
if (! $rotate OR ! array_key_exists($this->orientation,$this->_rotate) OR ! extension_loaded('imagick'))
@ -336,53 +216,15 @@ class Photo extends Model
/**
* Return the extension of the image
* @todo mime-by-ext?
*/
public function type($mime=FALSE)
{
return strtolower($mime ? 'image/jpeg' : pathinfo($this->filename,PATHINFO_EXTENSION));
}
/**
* Find duplicate images based on some attributes of the current image
*/
public function list_duplicate($includeme=FALSE)
public function view()
{
$po = DB::table('photo');
if ($this->id AND ! $includeme)
$po->where('id','!=',$this->id);
// Ignore photo's pending removal.
if (! $includeme)
$po->where(function($query)
{
$query->where('remove','!=',TRUE)
->orWhere('remove','=',NULL);
});
// Where the signature is the same
$po->where(function($query)
{
$query->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))
{
$query->orWhere(function($query)
{
$query->where('date_taken','=',$this->date_taken ? $this->date_taken : NULL);
$query->where('subsectime','=',$this->subsectime ? $this->subsectime : NULL);
if (! is_null($this->model))
$query->where('model','=',$this->model);
if (! is_null($this->make))
$query->where('make','=',$this->make);
});
}
});
return $po->pluck('id');
return sprintf('<img height="240" src="%s"></img>',url('/p/thumbnail/'.$this->id));
}
}

View File

@ -3,178 +3,19 @@
namespace App\Model;
use DB;
use Illuminate\Database\Eloquent\Model;
class Video extends Model
class Video extends Abstracted\Catalog
{
protected $table = 'videos';
// ID3 Object
private $_o = NULL;
public function People()
public function getIDLinkAttribute()
{
return $this->belongsToMany('App\Model\Person');
}
public function Tags()
{
return $this->belongsToMany('App\Model\Tag');
return $this->HTMLLinkAttribute($this->id,url('v/info').'/');
}
/**
* Photo's NOT pending removal.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNotRemove($query)
{
return $query->where(function($query)
{
$query->where('remove','!=',TRUE)
->orWhere('remove','=',NULL);
});
}
/**
* Photo's NOT duplicate.
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeNotDuplicate($query)
{
return $query->where(function($query)
{
$query->where('duplicate','!=',TRUE)
->orWhere('duplicate','=',NULL);
});
}
public function date_taken()
{
return $this->date_created ? date('Y-m-d H:i:s',$this->date_created) : 'UNKNOWN';
}
public function dump()
{
return $this->_o->info;
}
/**
* Return the date of the file
*/
public function file_date($type,$format=FALSE)
{
if (! is_readable($this->file_path()))
return NULL;
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;
}
return $format ? date('d-m-Y H:i:s',$t) : $t;
}
/**
* Determine the new name for the image
*/
public function file_path($short=FALSE,$new=FALSE)
{
$file = $this->filename;
if ($new)
$file = sprintf('%s.%s',((is_null($this->date_created) OR ! $this->date_created)
? sprintf('UNKNOWN/%07s',$this->file_path_id())
: date('Y/m/d-His',$this->date_created)),$this->type());
return (($short OR preg_match('/^\//',$file)) ? '' : config('video.dir').DIRECTORY_SEPARATOR).$file;
}
/**
* Calculate a file path ID based on the id of the file
*/
public function file_path_id($sep=3,$depth=9)
{
return trim(chunk_split(sprintf("%0{$depth}s",$this->id),$sep,'/'),'/');
}
/**
* Return the photo size
*/
public function file_size()
{
return (! is_readable($this->file_path())) ? NULL : filesize($this->file_path());
}
/**
* Display the GPS coordinates
*/
public function gps()
{
return ($this->gps_lat AND $this->gps_lon) ? sprintf('%s/%s',$this->gps_lat,$this->gps_lon) : 'UNKNOWN';
}
public function isParentWritable($dir)
{
if (file_exists($dir) AND is_writable($dir) AND is_dir($dir))
return TRUE;
elseif ($dir == dirname($dir) OR file_exists($dir))
return FALSE;
else
return ($this->isParentWritable(dirname($dir)));
}
/**
* Determine if a file is moveable
*
* useID boolean Determine if the path is based on the the ID or date
*/
public function moveable()
{
// If the source and target are the same, we dont need to move it
if ($this->file_path() == $this->file_path(FALSE,TRUE))
return FALSE;
// If there is already a file in the target.
// @todo If the target file is to be deleted, we could move this file
if (file_exists($this->file_path(FALSE,TRUE)))
return 1;
// Test if the source is readable
if (! is_readable($this->file_path()))
return 2;
// Test if the dir is writable (so we can remove the file)
if (! $this->isParentWritable(dirname($this->file_path())))
return 3;
// Test if the target dir is writable
// @todo The target dir may not exist yet, so we should check that a parent exists and is writable.
if (! $this->isParentWritable($this->file_path(FALSE,TRUE)))
return 4;
return TRUE;
}
/**
* Get the id of the previous photo
*/
public function next()
{
$po = DB::table('videos');
$po->where('id','>',$this->id);
$po->orderby('id','ASC');
return $po->first();
}
/**
* Return an Imagick object or attribute
* Return an GetID3 object or attribute
*
*/
protected function o($attr=NULL)
@ -192,17 +33,6 @@ class Video extends Model
return is_null($attr) ? $this->_o : (array_key_exists($attr,$this->_o->info) ? $this->_o->info[$attr] : NULL);
}
/**
* Get the id of the previous photo
*/
public function previous()
{
$po = DB::table('videos');
$po->where('id','<',$this->id);
$po->orderby('id','DEC');
return $po->first();
}
public function property($property)
{
if (! $this->o())
@ -222,6 +52,7 @@ class Video extends Model
case 'make': return array_get($this->_o->info,'quicktime.comments.make.0');
case 'model': return array_get($this->_o->info,'quicktime.comments.model.0');
case 'software': return array_get($this->_o->info,'quicktime.comments.software.0');
case 'signature': return array_get($this->_o->info,'sha1_data');
#case 'height': return $this->subatomsearch('quicktime.moov.subatoms',['trak','tkhd'],'height'); break;
#case 'width': return $this->subatomsearch('quicktime.moov.subatoms',['trak','tkhd'],'width'); break;
@ -236,13 +67,22 @@ class Video extends Model
case 'gps_lat': return array_get($this->_o->info,'quicktime.comments.gps_latitude.0');
case 'gps_lon': return array_get($this->_o->info,'quicktime.comments.gps_longitude.0');
case 'gps_altitude': return array_get($this->_o->info,'quicktime.comments.gps_altitude.0');
case 'identifier': if ($x=array_get($this->_o->info,'quicktime.comments')) {
return array_get(array_flatten(array_only($x,'content.identifier')),0);
}
break;
default:
return NULL;
}
}
public function properties()
{
return $this->o() ? $this->_o->info : [];
}
/**
Navigate through ID3 data to find the value.
* Navigate through ID3 data to find the value.
*/
private function subatomsearch($atom,array $paths,$key,array $data=[]) {
if (! $data AND is_null($data = array_get($this->_o->info,$atom)))
@ -274,30 +114,52 @@ class Video extends Model
return isset($data[$key]) ? $data[$key] : NULL;
}
public function properties()
public function setDateCreated()
{
return $this->o() ? $this->_io->info : [];
if ($this->property('creationdate'))
$this->date_created = $this->property('creationdate');
}
/**
* Display the photo signature
*/
public function signature($short=FALSE)
public function setDateCreatedAttribute($value)
{
return $short ? static::signaturetrim($this->signature) : $this->signature;
$this->attributes['date_created'] = strtotime($value);
}
public static function signaturetrim($signature,$chars=6)
public function setLocation()
{
return sprintf('%s...%s',substr($signature,0,$chars),substr($signature,-1*$chars));
$this->gps_lat = $this->property('gps_lat');
$this->gps_lon = $this->property('gps_lon');
$this->gps_altitude = $this->property('gps_altitude');
}
/**
* Determine if the image should be moved
*/
public function shouldMove()
public function setMakeModel()
{
return ($this->filename != $this->file_path(TRUE,TRUE));
$this->make = $this->property('make');
$this->model = $this->property('model');
$this->software = $this->property('software');
$this->type = $this->property('type');
$this->length = $this->property('length');
$this->codec = $this->property('codec');
$this->audiochannels = $this->property('audiochannels');
$this->channelmode = $this->property('channelmode');
$this->samplerate = $this->property('samplerate');
}
public function setSignature()
{
$this->signature = $this->property('signature');
$this->file_signature = md5_file($this->file_path());
$this->identifier = $this->property('identifier');
}
public function setSubSecTime()
{
// NOOP
}
public function setThumbnail()
{
// NOOP
}
/**
@ -312,47 +174,4 @@ class Video extends Model
{
return sprintf('<video width="320" height="240" src="%s" controls></video>',url('/v/view/'.$this->id));
}
/**
* Find duplicate images based on some attributes of the current image
*/
public function list_duplicate($includeme=FALSE)
{
$po = DB::table('videos');
if ($this->id AND ! $includeme)
$po->where('id','!=',$this->id);
// Ignore photo's pending removal.
if (! $includeme)
$po->where(function($query)
{
$query->where('remove','!=',TRUE)
->orWhere('remove','=',NULL);
});
// Where the signature is the same
$po->where(function($query)
{
$query->where('signature','=',$this->signature);
// Or they have the same time taken with the same camera
if ($this->date_created AND ($this->model OR $this->make))
{
$query->orWhere(function($query)
{
$query->where('date_created','=',$this->date_created ? $this->date_created : NULL);
if (! is_null($this->model))
$query->where('model','=',$this->model);
if (! is_null($this->make))
$query->where('make','=',$this->make);
});
}
});
return $po->pluck('id');
}
}

View File

@ -23,7 +23,7 @@ class AppServiceProvider extends ServiceProvider
{
// Any photo saved, queue it to be moved.
Photo::saved(function($photo) {
if (! $photo->duplicate AND ($x=$photo->moveable()) === TRUE)
if ($photo->scanned AND ! $photo->duplicate AND ! $photo->remove AND ($x=$photo->moveable()) === TRUE)
{
Log::info(sprintf('%s: Need to Move [%s]',__METHOD__,$photo->id.'|'.serialize($x)));
$this->dispatch((new PhotoMove($photo))->onQueue('move'));
@ -32,7 +32,7 @@ class AppServiceProvider extends ServiceProvider
// Any video saved, queue it to be moved.
Video::saved(function($video) {
if (! $video->duplicate AND ($x=$video->moveable()) === TRUE)
if ($video->scanned AND ! $video->duplicate AND ! $video->remove AND ($x=$video->moveable()) === TRUE)
{
Log::info(sprintf('%s: Need to Move [%s]',__METHOD__,$video->id.'|'.serialize($x)));
$this->dispatch((new VideoMove($video))->onQueue('move'));

View File

@ -7,9 +7,11 @@
"require": {
"php": ">=5.5.9",
"laravel/framework": "5.5.*",
"james-heinrich/getid3": "^1.9"
"james-heinrich/getid3": "^1.9",
"doctrine/dbal": "^2.6"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.1"
},
"autoload": {
"classmap": [

489
composer.lock generated
View File

@ -4,8 +4,363 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "9032a4f9673c0b58a684f3f9dbbf559c",
"content-hash": "a8aeeb055c8c6f1ee724994018b254a1",
"packages": [
{
"name": "doctrine/annotations",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
"reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5",
"reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5",
"shasum": ""
},
"require": {
"doctrine/lexer": "1.*",
"php": "^7.1"
},
"require-dev": {
"doctrine/cache": "1.*",
"phpunit/phpunit": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Docblock Annotations Parser",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"annotations",
"docblock",
"parser"
],
"time": "2017-12-06T07:11:42+00:00"
},
{
"name": "doctrine/cache",
"version": "v1.7.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/b3217d58609e9c8e661cd41357a54d926c4a2a1a",
"reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a",
"shasum": ""
},
"require": {
"php": "~7.1"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"alcaeus/mongo-php-adapter": "^1.1",
"mongodb/mongodb": "^1.1",
"phpunit/phpunit": "^5.7",
"predis/predis": "~1.0"
},
"suggest": {
"alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Caching library offering an object-oriented API for many cache backends",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"cache",
"caching"
],
"time": "2017-08-25T07:02:50+00:00"
},
{
"name": "doctrine/collections",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
"reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
"reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
"shasum": ""
},
"require": {
"php": "^7.1"
},
"require-dev": {
"doctrine/coding-standard": "~0.1@dev",
"phpunit/phpunit": "^5.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3.x-dev"
}
},
"autoload": {
"psr-0": {
"Doctrine\\Common\\Collections\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Collections Abstraction library",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"array",
"collections",
"iterator"
],
"time": "2017-07-22T10:37:32+00:00"
},
{
"name": "doctrine/common",
"version": "v2.8.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/common.git",
"reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/common/zipball/f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
"reference": "f68c297ce6455e8fd794aa8ffaf9fa458f6ade66",
"shasum": ""
},
"require": {
"doctrine/annotations": "1.*",
"doctrine/cache": "1.*",
"doctrine/collections": "1.*",
"doctrine/inflector": "1.*",
"doctrine/lexer": "1.*",
"php": "~7.1"
},
"require-dev": {
"phpunit/phpunit": "^5.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.8.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "lib/Doctrine/Common"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "Common Library for Doctrine projects",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"annotations",
"collections",
"eventmanager",
"persistence",
"spl"
],
"time": "2017-08-31T08:43:38+00:00"
},
{
"name": "doctrine/dbal",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "e3eed9b1facbb0ced3a0995244843a189e7d1b13"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/e3eed9b1facbb0ced3a0995244843a189e7d1b13",
"reference": "e3eed9b1facbb0ced3a0995244843a189e7d1b13",
"shasum": ""
},
"require": {
"doctrine/common": "^2.7.1",
"ext-pdo": "*",
"php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "^5.4.6",
"phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5",
"symfony/console": "2.*||^3.0"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"bin": [
"bin/doctrine-dbal"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.6.x-dev"
}
},
"autoload": {
"psr-0": {
"Doctrine\\DBAL\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
}
],
"description": "Database Abstraction Layer",
"homepage": "http://www.doctrine-project.org",
"keywords": [
"database",
"dbal",
"persistence",
"queryobject"
],
"time": "2017-11-19T13:38:54+00:00"
},
{
"name": "doctrine/inflector",
"version": "v1.2.0",
@ -1898,7 +2253,137 @@
"time": "2016-09-01T10:05:43+00:00"
}
],
"packages-dev": [],
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.1.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "01a859752094e00aa8548832312366753272f8af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/01a859752094e00aa8548832312366753272f8af",
"reference": "01a859752094e00aa8548832312366753272f8af",
"shasum": ""
},
"require": {
"illuminate/routing": "5.5.x",
"illuminate/session": "5.5.x",
"illuminate/support": "5.5.x",
"maximebf/debugbar": "~1.14.0",
"php": ">=7.0",
"symfony/debug": "^3",
"symfony/finder": "^3"
},
"require-dev": {
"illuminate/framework": "5.5.x"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
},
"laravel": {
"providers": [
"Barryvdh\\Debugbar\\ServiceProvider"
],
"aliases": {
"Debugbar": "Barryvdh\\Debugbar\\Facade"
}
}
},
"autoload": {
"psr-4": {
"Barryvdh\\Debugbar\\": "src/"
},
"files": [
"src/helpers.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "PHP Debugbar integration for Laravel",
"keywords": [
"debug",
"debugbar",
"laravel",
"profiler",
"webprofiler"
],
"time": "2017-09-18T13:32:46+00:00"
},
{
"name": "maximebf/debugbar",
"version": "v1.14.1",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
"reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/64251a392344e3d22f3d21c3b7c531ba96eb01d2",
"reference": "64251a392344e3d22f3d21c3b7c531ba96eb01d2",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"psr/log": "^1.0",
"symfony/var-dumper": "^2.6|^3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0|^5.0"
},
"suggest": {
"kriswallsmith/assetic": "The best way to manage assets",
"monolog/monolog": "Log using Monolog",
"predis/predis": "Redis storage"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.14-dev"
}
},
"autoload": {
"psr-4": {
"DebugBar\\": "src/DebugBar/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maxime Bouroumeau-Fuseau",
"email": "maxime.bouroumeau@gmail.com",
"homepage": "http://maximebf.com"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Debug bar in the browser for php application",
"homepage": "https://github.com/maximebf/php-debugbar",
"keywords": [
"debug",
"debugbar"
],
"time": "2017-09-13T12:19:36+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class PhotosRenameFields extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('photo', function(Blueprint $table)
{
$table->renameColumn('date_taken', 'date_created');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('photo', function(Blueprint $table)
{
$table->renameColumn('date_created', 'date_taken');
});
}
}

View File

@ -0,0 +1,51 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class VideoAddScanned extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('photo', function (Blueprint $table) {
$table->boolean('scanned')->nullable();
$table->string('file_signature')->nullable();
$table->string('identifier')->nullable();
$table->string('software')->nullable();
});
Schema::table('videos', function (Blueprint $table) {
$table->boolean('scanned')->nullable();
$table->string('file_signature')->nullable();
$table->string('identifier')->nullable();
$table->string('software')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('photo', function (Blueprint $table) {
$table->dropColumn('scanned');
$table->dropColumn('file_signature');
$table->dropColumn('identifier');
$table->dropColumn('software');
});
Schema::table('videos', function (Blueprint $table) {
$table->dropColumn('scanned');
$table->dropColumn('file_signature');
$table->dropColumn('identifier');
$table->dropColumn('software');
});
}
}

View File

@ -0,0 +1,54 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class PhotoAddIndex extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('photo', function (Blueprint $table) {
$table->index(['signature']);
$table->index(['filename']);
$table->index(['remove']);
$table->index(['duplicate']);
$table->index(['date_created','subsectime','model','make']);
});
Schema::table('videos', function (Blueprint $table) {
$table->index(['signature']);
$table->index(['filename']);
$table->index(['remove']);
$table->index(['duplicate']);
$table->index(['date_created','model','make']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('photo', function (Blueprint $table) {
$table->dropIndex(['signature']);
$table->dropIndex(['filename']);
$table->dropIndex(['remove']);
$table->dropIndex(['duplicate']);
$table->dropIndex(['date_created','subsectime','model','make']);
});
Schema::table('videos', function (Blueprint $table) {
$table->dropIndex(['signature']);
$table->dropIndex(['filename']);
$table->dropIndex(['remove']);
$table->dropIndex(['duplicate']);
$table->dropIndex(['date_created','model','make']);
});
}
}

View File

@ -0,0 +1,80 @@
@extends('layouts.app')
<?php $data = [
'ID'=>['id','idlink'],
'Signature'=>['signature','signature'],
'File Signature'=>['file_signature','file_signature'],
'Date Created'=>['date_created','datecreatedtext'],
'Filename'=>['filename','filename'],
'Filesize'=>['filesize','filesize'],
'Is Duplicate'=>['duplicate','duplicatetext'],
'Flagged'=>['flag','flagtext'],
'Delete'=>['remove','removecheckbox'],
];?>
@section('content')
<div class="container">
<div class="row">
<div class="col-md-11 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
Permanently Delete Items
</div>
<div class="text-center">{{ $catalog->links() }}</div>
<div class="panel-body">
@if ($catalog->count())
<form action="{{ $return }}" method="POST">
<table class="table table-striped table-condensed table-hover">
<tr>
<th>Source</th>
<th>Remove</th>
</tr>
@foreach ($catalog as $o)
<tr>
@php
$d=$o->list_duplicates(TRUE); $src=[];
@endphp
<td>
@if($d->where('duplicate',null)->count() > 1)
More than 1 source?
@elseif($d->count())
@php($src = $d->first())
{!! $src->view() !!}
<table class="table table-striped table-condensed table-hover small">
@foreach($data as $k=>$v)
<tr><th>{{$k}}</th><td>{!! $src->{$v[1]} !!}</td></tr>
@endforeach
</table>
@endif
</td>
@foreach($d as $item)
@if($src AND $item->id == $src->id) @continue @endif
@php($diff=collect($src)->diffAssoc($item))
<td>
{!! $item->view() !!}
<table class="table table-striped table-condensed table-hover small">
@foreach($data as $k=>$v)
<tr class="@if($diff->has($v[0])) danger @endif"><th>{{$k}}</th><td>{!! $item->{$v[1]} !!}</td></tr>
@endforeach
</table>
</td>
@endforeach
</tr>
@endforeach
</table>
<input type="hidden" name="pagenext" value="{{ $catalog->hasMorePages() ? $catalog->currentPage()+1 : NULL }}">
<button class="btn btn-default">Confirm Delete</button>
{{ csrf_field() }}
</form>
@else
NONE!
@endif
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -1,78 +0,0 @@
@extends('layouts.app')
@section('content')
<?php $data = [
'ID'=>'id',
'Thumbnail'=>'thumbnail',
'Signature'=>'signature',
'Date Taken'=>'datetaken',
'File Created'=>'created',
'File Modified'=>'modified',
'Filename'=>'filepath',
'Filesize'=>'filesize',
'Width'=>'width',
'Height'=>'height',
'Orientation'=>'orientation',
'Make'=>'make',
'Model'=>'model',
'Duplicates'=>'duplicates',
'Is Duplicate'=>'duplicate',
'Flagged'=>'flag',
'Delete'=>'remove',
];?>
<div class="container">
<div class="row">
<div class="col-md-11 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
Permanently Delete Photos
</div>
<div class="text-center">{{ $photos->links() }}</div>
<div class="panel-body">
@if ($photos->count())
<form action="{{ url('/p/deletes') }}" method="POST">
<table class="table table-striped table-condensed table-hover">
@foreach ($data as $k=>$v)
<tr>
<th>{{ $k }}</th>
@foreach ($photos as $o)
<?php
switch ($v) :
case 'id': $x=$o->id; $y=sprintf('<input type="hidden" name="photo[]" value="%s"><a href="%s">%s</a>',$o->id,url('/p/info/'.$o->id),$o->id); break;
case 'thumbnail': $x=md5($o->thumbnail); $y=sprintf('<a href="%s"><img src="%s" width="200px"></a>',url('/p/view/'.$o->id),url('/p/thumbnail/'.$o->id)); break;
case 'signature': $x=$y=$o->signature(TRUE); break;
case 'datetaken': $x=$y=$o->date_taken(); break;
case 'created': $x=$y=$o->file_date('c',TRUE); break;
case 'modified': $x=$y=$o->file_date('m',TRUE); break;
case 'filepath': $x=$y=$o->file_path(TRUE); break;
case 'filesize': $x=$y=$o->file_size(); break;
case 'width':
case 'height':
case 'orientation':
case 'make':
case 'model': $y=$o->{$v}; break;
case 'duplicates': $x=$y='';foreach($o->list_duplicate() as $id) $y.=($y ? '|' : '').sprintf('<a href="%s">%s</a>',url('/p/info/'.$id),$id); break;
case 'flag':
case 'remove':
case 'duplicate': $y=sprintf('<input type="checkbox" name="%s[%s]" value="1"%s>',$v,$o->id,$o->{$v} ? ' checked="checked"' : ''); break;
endswitch ?>
<td><?php echo $y; ?></td>
@endforeach {{-- photo --}}
</tr>
@endforeach {{-- data --}}
</table>
<input type="hidden" name="pagenext" value="{{ $photos->hasMorePages() ? $photos->currentPage()+1 : NULL }}">
<button class="btn btn-default">Confirm Delete</button>
{{ csrf_field() }}
</form>
@else
NONE!
@endif
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -33,6 +33,7 @@
<dt>Date Taken</dt><dd>{{ $photo->date_taken() }}<dd>
<dt>Camera</dt><dd>{{ $photo->make }}<dd>
<dt>Model</dt><dd>{{ $photo->model }}<dd>
<dt>Software</dt><dd>{{ $photo->software }}<dd>
<br/>
<dt>Location</dt><dd>
<?php if ($photo->gps() == 'UNKNOWN') : ?>

View File

@ -1,75 +0,0 @@
@extends('layouts.app')
@section('content')
<?php $data = [
'ID'=>'id',
'Signature'=>'signature',
'Date Created'=>'datecreated',
'File Created'=>'created',
'File Modified'=>'modified',
'Filename'=>'filepath',
'Filesize'=>'filesize',
'Width'=>'width',
'Height'=>'height',
'Make'=>'make',
'Model'=>'model',
'Duplicates'=>'duplicates',
'Is Duplicate'=>'duplicate',
'Flagged'=>'flag',
'Delete'=>'remove',
];?>
<div class="container">
<div class="row">
<div class="col-md-11 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
Permanently Delete Photos
</div>
<div class="text-center">{{ $videos->links() }}</div>
<div class="panel-body">
@if ($videos->count())
<form action="{{ url('/v/deletes') }}" method="POST">
<table class="table table-striped table-condensed table-hover">
@foreach ($data as $k=>$v)
<tr>
<th>{{ $k }}</th>
@foreach ($videos as $o)
<?php
switch ($v) :
case 'id': $x=$o->id; $y=sprintf('<input type="hidden" name="video[]" value="%s"><a href="%s">%s</a>',$o->id,url('/v/info/'.$o->id),$o->id); break;
case 'signature': $x=$y=$o->signature(TRUE); break;
case 'datecreated': $x=$y=$o->date_taken(); break;
case 'created': $x=$y=$o->file_date('c',TRUE); break;
case 'modified': $x=$y=$o->file_date('m',TRUE); break;
case 'filepath': $x=$y=$o->file_path(TRUE); break;
case 'filesize': $x=$y=$o->file_size(); break;
case 'width':
case 'height':
case 'orientation':
case 'make':
case 'model': $y=$o->{$v}; break;
case 'duplicates': $x=$y='';foreach($o->list_duplicate() as $id) $y.=($y ? '|' : '').sprintf('<a href="%s">%s</a>',url('/v/info/'.$id),$id); break;
case 'flag':
case 'remove':
case 'duplicate': $y=sprintf('<input type="checkbox" name="%s[%s]" value="1"%s>',$v,$o->id,$o->{$v} ? ' checked="checked"' : ''); break;
endswitch ?>
<td><?php echo $y; ?></td>
@endforeach {{-- video --}}
</tr>
@endforeach {{-- data --}}
</table>
<input type="hidden" name="pagenext" value="{{ $videos->hasMorePages() ? $videos->currentPage()+1 : NULL }}">
<button class="btn btn-default">Confirm Delete</button>
{{ csrf_field() }}
</form>
@else
NONE!
@endif
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -5,6 +5,7 @@
'ID'=>'id',
'Video'=>'video',
'Signature'=>'signature',
'File Signature'=>'file_signature',
'Length'=>'length',
'Date Created'=>'datecreated',
'File Created'=>'created',
@ -58,6 +59,7 @@ case 'id': $x=$id; $y=sprintf('<a href="%s">%s</a>',url('/v/info/'.$o->id),$o->i
case 'video': $x=$o->signature(TRUE);$y=$o->view();; break;
case 'length': $x=$y=$o->length; break;
case 'signature': $x=$y=$o->signature(TRUE); break;
case 'file_signature': $x=$y=$o->file_signature(TRUE); break;
case 'datecreated': $x=$y=$o->date_taken(); break;
case 'created': $x=$y=$o->file_date('c',TRUE); break;
case 'modified': $x=$y=$o->file_date('m',TRUE); break;

View File

@ -6,7 +6,7 @@
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">
Video {{ $video->id }}<?php if ($video->remove) : ?> - <strong>PENDING DELETE</strong><?php endif?>
Video {{ $video->id }}<?php if ($video->remove) : ?> - <strong>PENDING DELETE</strong><?php endif?><?php if ($video->duplicate) : ?> - <strong>DUPLICATE</strong><?php endif?>
</div>
<div class="panel-body">
@ -39,6 +39,8 @@
<dt>Date Taken</dt><dd>{{ $video->date_taken() }}<dd>
<dt>Camera</dt><dd>{{ $video->make }}<dd>
<dt>Model</dt><dd>{{ $video->model }}<dd>
<dt>Software</dt><dd>{{ $video->software }}<dd>
<dt>Identifier</dt><dd>{{ $video->identifier }}<dd>
<br/>
<dt>Location</dt><dd>
<?php if ($video->gps() == 'UNKNOWN') : ?>
@ -60,6 +62,14 @@
</script>
<?php endif ?>
</dd>
@if($x = $video->list_duplicate())
<dt>Duplicates</dt><dd>
@php
$y=''; foreach($video->list_duplicate() as $id) $y.=($y ? '|' : '').sprintf('<a href="%s">%s</a>',url('/v/info/'.$id),$id);
echo($y);
@endphp
</dd>
@endif
</div>
</div>