213 lines
7.0 KiB
Perl
Executable File
213 lines
7.0 KiB
Perl
Executable File
#!/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_utf8mb4=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
|
|
FROM tasks
|
|
WHERE task_type='AISCAN'
|
|
AND status='COMPLETED'
|
|
AND JSON_EXTRACT(results, '\$.verdict') = 1
|
|
AND (SELECT 1 FROM tasks t2 WHERE t2.video_id = tasks.video_id AND t2.task_type = 'REVIEW' LIMIT 1) IS NULL
|
|
");
|
|
|
|
$sth->execute();
|
|
while (my $row = $sth->fetchrow_hashref()) {
|
|
print "Creating REVIEW task for video_id: $row->{video_id}\n";
|
|
create_task($row->{video_id}, 'REVIEW');
|
|
}
|
|
$sth->finish();
|
|
} |