#!/usr/bin/perl use strict; use warnings; use DBI; use Digest::SHA qw(sha256_hex); use JSON; use String::ShellQuote qw/shell_quote/; my $db_host = $ENV{'DB_HOST'} || 'mariadb'; my $db_name = $ENV{'DB_NAME'} || 'videodetect'; my $db_user = $ENV{'DB_USER'} || 'videodetect'; my $db_pass = $ENV{'DB_PASSWORD'} || 'videodetect'; my $do_scan = (defined $ENV{'DO_SCAN'}) ? $ENV{'DO_SCAN'} : 1; my $do_review = (defined $ENV{'DO_REVIEW'}) ? $ENV{'DO_REVIEW'} : 1; my $do_delete = (defined $ENV{'DO_DELETE'}) ? $ENV{'DO_DELETE'} : 1; $SIG{INT} = sub { die "Interrupted\n"; }; my $dbh = DBI->connect( "DBI:mysql:database=$db_name;host=$db_host;mysql_enable_utf8=1", $db_user, $db_pass, { mysql_auto_reconnect => 1 } ); my @video_extensions = qw(mp4 mkv avi mov flv wmv mpg mpeg webm m4v); my %ignored_extensions; my @dir_queue = ('/data'); # Get NOW() from the database my $sth = $dbh->prepare("SELECT NOW() AS now"); $sth->execute(); my $row = $sth->fetchrow_hashref(); my $now = $row->{now}; $sth->finish(); if($do_scan) { while(my $dir = shift @dir_queue) { opendir(my $dh, $dir) or die "Cannot open directory $dir: $!"; print "$dir\n"; while (my $file = readdir($dh)) { next if ($file eq '.' || $file eq '..'); my $full_path = "$dir/$file"; if (-d $full_path) { unshift @dir_queue, $full_path; } elsif (-f $full_path) { process_file($full_path); } } closedir($dh); } for my $ext (sort { $ignored_extensions{$b} <=> $ignored_extensions{$a} } keys %ignored_extensions) { print "Ignored extension $ext: $ignored_extensions{$ext} times\n"; } } if($do_scan && $do_delete) { # Any entry where last_scan_time < $now is considered deleted or moved, so we can mark them as such my $sth_delete = $dbh->prepare("DELETE FROM videos WHERE last_scan_time < ? OR last_scan_time IS NULL"); $sth_delete->execute($now); } if($do_review) { print "Scanning complete. Creating review tasks for completed AISCAN tasks.\n"; CreateReviewTasks(); } sub process_file { my ($file_path) = @_; if (my ($ext)=$file_path =~ /^.+\.(\S+?)$/) { unless (grep { lc($ext) eq $_ } @video_extensions) { $ignored_extensions{lc($ext)}++; return; # Not a video file } } my $sth = $dbh->prepare("SELECT id,file_size FROM videos WHERE file_path=?"); $sth->execute($file_path); if(my $row = $sth->fetchrow_hashref()) { my $file_size = -s $file_path; if ($file_size != $row->{file_size}) { warn "File size mismatch for $file_path. Updating record."; my $update_sth = $dbh->prepare("UPDATE videos SET file_size=?, last_scan_time=NOW() WHERE id=?"); $update_sth->execute($file_size, $row->{id}); $update_sth->finish(); create_aiscan_task($row->{id}); } else { my $update_sth = $dbh->prepare("UPDATE videos SET last_scan_time=NOW() WHERE id=?"); $update_sth->execute($row->{id}); $update_sth->finish(); } return; # Already exists and size matches } print "NEW:$file_path\n"; my $info = get_video_info($file_path); unless ($info) { warn "Failed to get video info for $file_path. Skipping."; return; } $sth = $dbh->prepare("INSERT INTO videos (file_path, file_size, file_hash, resolution_w, resolution_h, codec, duration, last_scan_time) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())"); $sth->execute( $file_path, -s $file_path, $info->{file_hash}, $info->{resolution_w}, $info->{resolution_h}, $info->{codec}, $info->{duration} ); $sth->finish(); my $video_id = $dbh->last_insert_id(undef, undef, 'videos', undef); create_aiscan_task($video_id); } sub create_aiscan_task { my ($video_id) = @_; create_task($video_id, 'AISCAN'); } sub create_task { my ($video_id, $task_type) = @_; $dbh->do("DELETE FROM tasks WHERE video_id=$video_id AND task_type=?", undef, $task_type); my $sth = $dbh->prepare("INSERT INTO tasks (video_id, task_type, status) VALUES (?, ?, 'PENDING')"); $sth->execute($video_id, $task_type); $sth->finish(); } sub get_video_info { my ($file_path) = @_; # --- Compute SHA-256 hash of the file --- return undef unless -f $file_path && -r $file_path; open(my $fh, '<:raw', $file_path) or do { warn "Cannot open $file_path: $!"; return undef; }; my $hash = sha256_hex($fh); close($fh); # --- Run ffprobe to extract metadata --- my $probe_cmd = 'ffprobe -v quiet -print_format json -show_format -show_streams ' . shell_quote($file_path); my $output = `$probe_cmd`; return undef unless defined $output && length($output); my $json = JSON->new->utf8->canonical(1); my $data = $json->decode($output); # --- Extract codec from video stream (prefer first video stream found) --- my $codec = undef; if (exists $data->{streams} && ref($data->{streams}) eq 'ARRAY') { for my $stream (@{$data->{streams}}) { if ($stream->{codec_type} eq 'video') { $codec = $stream->{codec_name}; last; } } } # --- Extract resolution and duration from format/streams --- my ($resolution_w, $resolution_h, $duration); # Duration from format first, then stream if (exists $data->{format} && exists $data->{format}{duration}) { $duration = $data->{format}{duration} + 0; # force numeric } elsif (exists $data->{streams}[0] && exists $data->{streams}[0]{duration}) { $duration = $data->{streams}[0]{duration} + 0; } # Resolution from video stream first, then format side data if (exists $data->{streams} && ref($data->{streams}) eq 'ARRAY') { for my $stream (@{$data->{streams}}) { next unless $stream->{codec_type} eq 'video'; if ($stream->{width} && $stream->{height}) { $resolution_w = $stream->{width}; $resolution_h = $stream->{height}; last; } } } return { file_hash => $hash, resolution_w => $resolution_w // 0, resolution_h => $resolution_h // 0, codec => $codec // 'unknown', duration => $duration, }; } sub CreateReviewTasks { # Fetch all completed AISCAN tasks and filter in Perl. # This avoids MariaDB's inconsistent JSON functions in SQL WHERE clauses, # which behave differently between CLI client and DBI connections. my $sth = $dbh->prepare(" SELECT id, video_id, results FROM tasks WHERE task_type='AISCAN' AND status='COMPLETED' "); $sth->execute(); while (my $row = $sth->fetchrow_hashref()) { my $results = decode_json($row->{results}); next unless $results && ref($results) eq 'HASH'; next unless $results->{verdict} == 1; next if exists $results->{reviewed}; print "Creating REVIEW task for video_id: $row->{video_id}\n"; create_task($row->{video_id}, 'REVIEW'); # Re-encode the whole JSON blob in Perl and update it. # This avoids MariaDB JSON_SET path parsing issues through DBI connections. $results->{reviewed} = 1; my $updated = encode_json($results); my $sth_update = $dbh->prepare( "UPDATE tasks SET results = ? WHERE id = ?" ); $sth_update->execute($updated, $row->{id}); $sth_update->finish(); } $sth->finish(); }