#!/usr/bin/perl use strict; use warnings; use DBI; use Digest::SHA qw(sha256_hex); use JSON; 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'} || 'videodetect123'; my $dbh = DBI->connect("DBI:mysql:database=$db_name;host=$db_host", $db_user, $db_pass); my @video_extensions = qw(mp4 mkv avi mov flv wmv mpg mpeg webm); 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(); while(my $dir = shift @dir_queue) { opendir(my $dh, $dir) or die "Cannot open directory $dir: $!"; 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); } # 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); sub process_file { my ($file_path) = @_; # Determine if the file is a video based on mime info or extension. For simplicity, let's check the extension. if (my ($ext)=$file_path =~ /^.+\.(\S+?)$/) { unless (grep { lc($ext) eq $_ } @video_extensions) { 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_review_task($row->{id}); } return; # Already exists and size matches } 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_review_task($video_id); } sub create_review_task { my ($video_id) = @_; $dbh->do("DELETE FROM tasks WHERE video_id=$video_id AND task_type='REVIEW'"); my $sth = $dbh->prepare("INSERT INTO tasks (video_id, task_type, status) VALUES (?, 'REVIEW', 'PENDING')"); $sth->execute($video_id); $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 = qq{ffprobe -v quiet -print_format json -show_format -show_streams '$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, }; }