From a5b5256793945b3c81f911410768d703721fbe30 Mon Sep 17 00:00:00 2001 From: Deon George Date: Mon, 16 Sep 2024 23:17:51 +1000 Subject: [PATCH] Major refactor of video processing. --- app/Jobs/CatalogScan.php | 2 +- app/Media/QuickTime.php | 5 + app/Media/QuickTime/Atoms/moov/meta.php | 5 + app/Media/Unknown.php | 11 - app/Models/Video.php | 245 +++++++----------- .../2024_09_16_221134_video_attributes.php | 46 ++++ .../views/components/pagination/nav.blade.php | 13 + .../components/video/thumbnail.blade.php | 3 + resources/views/video/view.blade.php | 159 +++++------- 9 files changed, 230 insertions(+), 259 deletions(-) create mode 100644 database/migrations/2024_09_16_221134_video_attributes.php create mode 100644 resources/views/components/pagination/nav.blade.php create mode 100644 resources/views/components/video/thumbnail.blade.php diff --git a/app/Jobs/CatalogScan.php b/app/Jobs/CatalogScan.php index 50f51c5..86b6306 100644 --- a/app/Jobs/CatalogScan.php +++ b/app/Jobs/CatalogScan.php @@ -65,7 +65,7 @@ class CatalogScan implements ShouldQueue, ShouldBeUnique $x = $this->o->myduplicates()->get(); if (count($x)) { foreach ($x as $this->oo) { - // And that photo is not marked as a duplicate + // And that catalog item is not marked as a duplicate if (! $this->oo->duplicate) { $this->o->duplicate = TRUE; Log::alert(sprintf('Image [%s] marked as a duplicate',$this->o->filename)); diff --git a/app/Media/QuickTime.php b/app/Media/QuickTime.php index f5fb177..1db100b 100644 --- a/app/Media/QuickTime.php +++ b/app/Media/QuickTime.php @@ -64,6 +64,11 @@ class QuickTime extends Base { return $atom->{$key}; + case 'make': + case 'model': + case 'software': + return $this->find_atoms(moov\meta::class,1)->{$key}; + case 'time_scale': $atom = $this->find_atoms(moov\mvhd::class,1); diff --git a/app/Media/QuickTime/Atoms/moov/meta.php b/app/Media/QuickTime/Atoms/moov/meta.php index 9f7f0e3..32b654a 100644 --- a/app/Media/QuickTime/Atoms/moov/meta.php +++ b/app/Media/QuickTime/Atoms/moov/meta.php @@ -40,6 +40,11 @@ class meta extends SubAtom case 'gps_lon': return Arr::get($this->gps,'lon'); + case 'make': + case 'model': + case 'software': + return Arr::get($this->cache,'mdta.com.apple.quicktime.'.$key); + default: return parent::__get($key); } diff --git a/app/Media/Unknown.php b/app/Media/Unknown.php index c3220f6..e39967e 100644 --- a/app/Media/Unknown.php +++ b/app/Media/Unknown.php @@ -6,17 +6,6 @@ namespace App\Media; * This represents a media type that we dont know how to parse */ class Unknown extends Base { - private const LOGKEY = 'MF?'; - - private string $type; - - public function __construct(string $filename,string $type) - { - parent::__construct($filename); - - $this->type = $type; - } - public function __get(string $key): mixed { return NULL; diff --git a/app/Models/Video.php b/app/Models/Video.php index 52d8692..14f0b69 100644 --- a/app/Models/Video.php +++ b/app/Models/Video.php @@ -2,190 +2,119 @@ namespace App\Models; -use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Log; + +use App\Jobs\CatalogMove; +use App\Media\{Base,Factory}; class Video extends Abstracted\Catalog { - // ID3 Object - private $_o = NULL; + public const config = 'video'; - public function getIDLinkAttribute() + protected $casts = [ + 'created'=>'datetime:Y-m-d H:i:s', + ]; + + // Media Object + private ?Base $_o; + protected array $init = [ + 'creation_date', + 'gps', + 'heightwidth', + 'signature', + 'software', + ]; + + public static function boot() { - return $this->HTMLLinkAttribute($this->id,'v/info'); + parent::boot(); + + // Any video saved, queue it to be moved. + self::saved(function($item) { + if ($item->scanned && (! $item->duplicate) && (! $item->remove) && ($x=$item->shouldMove()) === TRUE) { + Log::info(sprintf('%s: Need to Move [%s]',__METHOD__,$item->id.'|'.serialize($x))); + + CatalogMove::dispatch($item) + ->onQueue('move'); + } + }); } - public function getHtmlImageURL() + public function __get($key): mixed { - return sprintf('',url('/v/view/'.$this->id)); - } + if ($key === 'o') { + if (isset($this->_o)) + return $this->_o; - /** - * Return an GetID3 object or attribute - * - */ - protected function o($attr=NULL) - { - if (! $this->filename OR ! file_exists($this->file_path()) OR ! is_readable($this->file_path())) - return FALSE; + if ((! file_exists($this->file_name(FALSE))) || (! is_readable($this->file_name(FALSE)))) + return $this->_o = NULL; - if (is_null($this->_o)) - { - /* - * @todo There is a bug with getID3, where the second iteration yields different results - * this is problematic when video:scanall is run from a queue and there is more than 1 job. - * It also appears that only 1.9.18 gets date/gps data, a later version doesnt get gps data. - */ - $this->_o = new \getID3; - $this->_o->analyze($this->file_path()); - $this->_o->getHashdata('sha1'); + if (! isset($this->_o)) { + $this->_o = Factory::create($this->file_name(FALSE)); + + return $this->_o; + } } - return is_null($attr) ? $this->_o : (array_key_exists($attr,$this->_o->info) ? $this->_o->info[$attr] : NULL); + return parent::__get($key); } - public function property(string $property) + /* METHODS */ + + public function custom_init() { - if (! $this->o()) - return NULL; + $this->audio_channels = $this->getObjectOriginal('audio_channels'); + $this->audio_codec = $this->getObjectOriginal('audio_codec'); + $this->audio_samplerate = $this->getObjectOriginal('audio_samplerate'); + $this->gps_altitude = $this->getObjectOriginal('gps_altitude'); + $this->length = round($this->getObjectOriginal('length'),2); + $this->type = $this->getObjectOriginal('type'); + $this->video_codec = $this->getObjectOriginal('video_codec'); + $this->video_framerate = $this->getObjectOriginal('video_framerate'); + /* + $x = new \getID3; + $x->analyze($this->file_name(FALSE)); + dump(['id3'=>$x]); + */ + } + + public function getObjectOriginal(string $property): mixed + { switch ($property) { - case 'creationdate': - // Try creation_Data - $x = Arr::get($this->_o->info,'quicktime.comments.creation_date.0'); + case 'file_signature': + return md5_file($this->file_name(FALSE)); - // Try creation_Data - if (is_null($x)) - $x = Arr::get($this->_o->info,'quicktime.comments.creationdate.0'); + case 'length': + return $this->o?->duration; - return $x; - - case 'make': return Arr::get($this->_o->info,'quicktime.comments.make.0'); - case 'model': return Arr::get($this->_o->info,'quicktime.comments.model.0'); - case 'software': return Arr::get($this->_o->info,'quicktime.comments.software.0'); - case 'signature': return Arr::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; - case 'height': return Arr::get($this->_o->info,'video.resolution_y'); - case 'width': return Arr::get($this->_o->info,'video.resolution_x'); - case 'length': return Arr::get($this->_o->info,'playtime_seconds'); - case 'type': return Arr::get($this->_o->info,'video.dataformat'); - case 'codec': return Arr::get($this->_o->info,'audio.codec'); - case 'audiochannels': return Arr::get($this->_o->info,'audio.channels'); - case 'samplerate': return Arr::get($this->_o->info,'audio.sample_rate'); - case 'channelmode': return Arr::get($this->_o->info,'audio.channelmode'); - case 'gps_lat': return Arr::get($this->_o->info,'quicktime.comments.gps_latitude.0'); - case 'gps_lon': return Arr::get($this->_o->info,'quicktime.comments.gps_longitude.0'); - case 'gps_altitude': return Arr::get($this->_o->info,'quicktime.comments.gps_altitude.0'); - case 'identifier': - if ($x=Arr::get($this->_o->info,'quicktime.comments')) { - return Arr::get(Arr::flatten(Arr::only($x,'content.identifier')),0); - } - break; + case 'audio_channels': + case 'audio_codec': + case 'audio_samplerate': + case 'creation_date': + case 'gps_altitude': + case 'gps_lat': + case 'gps_lon': + case 'height': + case 'make': + case 'model': + case 'signature': + case 'software': + case 'type': + case 'width': + case 'video_codec': + case 'video_framerate': + return $this->o?->{$property}; default: - return NULL; + throw new \Exception('To implement: '.$property); } } - public function properties() - { - return $this->o() ? $this->_o->info : []; - } - /** - * Navigate through ID3 data to find the value. + * Return the extension of the video */ - private function subatomsearch($atom,array $paths,$key,array $data=[]) { - if (! $data AND is_null($data = Arr::get($this->_o->info,$atom))) { - // Didnt find it. - return NULL; - } - - foreach ($paths as $path) { - $found = FALSE; - - foreach ($data as $array) { - if ($array['name'] === $path) { - $found = TRUE; - - if ($path != last($paths)) - $data = $array['subatoms']; - else - $data = $array; - - break; - } - } - - if (! $found) - break; - } - - return isset($data[$key]) ? $data[$key] : NULL; - } - - public function setDateCreated() - { - $this->created = $this->property('creationdate') ? strtotime($this->property('creationdate')) : NULL; - } - - public function setLocation() - { - $this->gps_lat = $this->property('gps_lat'); - $this->gps_lon = $this->property('gps_lon'); - $this->gps_altitude = $this->property('gps_altitude'); - } - - public function setMakeModel() - { - $ma = NULL; - - if ($this->property('make')) - $ma = Make::firstOrCreate([ - 'name'=>$this->property('make'), - ]); - - $mo = Model::firstOrCreate([ - 'name'=>$this->property('model') ?: NULL, - 'make_id'=>$ma ? $ma->id : NULL, - ]); - - $so = Software::firstOrCreate([ - 'name'=>$this->property('software') ?: NULL, - 'model_id'=>$mo->id, - ]); - - $this->software_id = $so->id; - - $this->type = $this->property('type'); - $this->length = round($this->property('length'),2); - $this->codec = $this->property('codec'); - $this->audiochannels = $this->property('audiochannels'); - $this->channelmode = $this->property('channelmode'); - $this->samplerate = $this->property('samplerate'); - } - - public function setSignature() - { - parent::setSignature(); - - $this->identifier = $this->property('identifier'); - } - - public function setSubSecTime() - { - // NOOP - } - - public function setThumbnail() - { - // NOOP - } - - /** - * Return the extension of the image - */ - public function type($mime=FALSE) + public function type($mime=FALSE): string { return strtolower($mime ? File::mime_by_ext(pathinfo($this->filename,PATHINFO_EXTENSION)) : pathinfo($this->filename,PATHINFO_EXTENSION)); } diff --git a/database/migrations/2024_09_16_221134_video_attributes.php b/database/migrations/2024_09_16_221134_video_attributes.php new file mode 100644 index 0000000..dab46b6 --- /dev/null +++ b/database/migrations/2024_09_16_221134_video_attributes.php @@ -0,0 +1,46 @@ +string('video_codec')->nullable(); + $table->float('video_framerate')->nullable(); + + $table->dropForeign(['model_id']); + $table->dropColumn(['identifier','channelmode','model_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::statement('ALTER TABLE videos RENAME COLUMN audio_codec TO codec'); + DB::statement('ALTER TABLE videos RENAME COLUMN audio_samplerate TO samplerate'); + DB::statement('ALTER TABLE videos RENAME COLUMN audio_channels TO audiochannels'); + + Schema::table('videos', function (Blueprint $table) { + $table->dropColumn(['video_codec','video_framerate']); + + $table->string('identifier')->nullable(); + $table->string('channelmode',16)->nullable(); + + $table->bigInteger('model_id')->unsigned()->nullable(); + $table->foreign('model_id')->references('id')->on('models'); + }); + } +}; diff --git a/resources/views/components/pagination/nav.blade.php b/resources/views/components/pagination/nav.blade.php new file mode 100644 index 0000000..1db9966 --- /dev/null +++ b/resources/views/components/pagination/nav.blade.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/resources/views/components/video/thumbnail.blade.php b/resources/views/components/video/thumbnail.blade.php new file mode 100644 index 0000000..4f50c0f --- /dev/null +++ b/resources/views/components/video/thumbnail.blade.php @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/views/video/view.blade.php b/resources/views/video/view.blade.php index 07d98b7..2c236b6 100644 --- a/resources/views/video/view.blade.php +++ b/resources/views/video/view.blade.php @@ -1,115 +1,96 @@ @extends('adminlte::layouts.app') @section('htmlheader_title') - Video - {{ $o->id }} + Video - {{ $vo->id }} @endsection @section('contentheader_title') - Video #{{ $o->id }} + Video #{{ $vo->id }} @endsection @section('contentheader_description') - @if(! $o->scanned)@endif - @if($o->duplicate)@endif - @if($o->ignore_duplicate)@endif - @if($o->remove)@endif @endsection @section('page_title') - #{{ $o->id }} + #{{ $vo->id }} @endsection @section('main-content')
- {!! $o->getHtmlImageURL() !!} + - - - +
-
-
Signature
{{ $o->signature(TRUE) }}
-
Filename
{{ $o->filename }}
+
+
+
+ @if(! $vo->scanned)@endif + @if($vo->duplicate)@endif + @if($vo->ignore_duplicate)@endif + @if($vo->remove)@endif +
+
+
Signature
{{ $vo->signature(TRUE) }}
+
Filename
{{ $vo->filename }}
- @if ($o->shouldMove()) -
NEW Filename
{{ $o->file_name() }}
- @endif + @if ($vo->shouldMove()) +
NEW Filename
{{ $vo->file_name() }}
+ @endif + +
Size
{{ number_format($vo->file_size) }}
+
Date Taken
{{ $vo->created?->format('Y-m-d H:i:s') }}
+ + @if($vo->scanned) +
Dimensions
{{ $vo->dimensions }}
+
Length
{{ $vo->length }}
+
Type
{{ $vo->type }}
+
+
Audio Codec
{{ $vo->audio_codec }}
+
Audio Channels
{{ $vo->audio_channels }}
+
Sample Rate
{{ $vo->audio_samplerate }}
+
+
Video Codec
{{ $vo->video_codec }}
+
Video Framerate
{{ $vo->video_framerate }}
+ +
Camera
{{ $vo->device }}
+
+
Location
+
+ @if(! $vo->gps) + UNKNOWN + @else + + @endif +
+ + @if(($x=$vo->myduplicates()->get())->count()) +
Duplicates
+
+ @foreach($x as $oo) + @if(! $loop->first)| @endif + {!! $oo->id_link !!} + @endforeach +
+ @endif + @endif +
+ + @if ($vo->remove) +
+ -
Size
{{ $o->file_size() }}
-
Dimensions
{{ $o->dimensions }}
-
Length
{{ $o->length }}
-
Type
{{ $o->type }}
-
Codec
{{ $o->codec }}
-
Audio Channels
{{ $o->audiochannels }}
-
Channels Mode
{{ $o->channelmode }}
-
Sample Rate
{{ $o->samplerate }}
-
-
Date Taken
{{ $o->date_taken() }}
-
Camera
{{ $o->device() }}
-
Identifier
{{ $o->identifier }}
-
-
Location
-
- @if($o->gps() == 'UNKNOWN') - UNKNOWN @else -
+ + + @endif -
- - @if(($x=$o->myduplicates()->get())->count()) -
Duplicates
-
- @foreach($x as $oo) - @if(! $loop->first)| @endif - {!! $oo->id_link !!} - @endforeach -
- @endif + @csrf +
+
- - @if ($o->remove) -
- - - @else - - - - @endif - {{ csrf_field() }} -
-@endsection - -@section('page-scripts') - @if($o->gps() !== 'UNKNOWN') - @js('//maps.google.com/maps/api/js?sensor=false') - - @endif -@append \ No newline at end of file +@endsection \ No newline at end of file