#!/usr/bin/env -S perl -w # http://kobyla.info/soft/mpv/ # for use with mpp wrapper script with the special input.conf # catches the list control commands and builds DEL and ADD lists for further processing # $Id: mpp-input,v 1.44 2025/04/13 23:34:23 pdc Exp $ our $TMPDIR=$ENV{'TMPDIR'} || "/tmp"; our $list_out="$TMPDIR/mpp.$ENV{USER}"; # 1-time lists (last run) our $list_stash="/var/tmp/mpp.$ENV{USER}.stash"; # cumulative lists ######## our $BUF_SIZE=1024*512; # input buffer our $prefetch_block = 1<<20; our $prefetch_max = 400<<20; our $prefetch_done=0; use IO::Select; our $s = IO::Select->new(); $s->add(\*STDIN); binmode *STDIN; $|=1; my $mode='default'; # or del_seen our $seen_quick=0; # SEEN_QUICK SEEN_FULL our $path='BADPATH'; our $last_path='BADPATH'; our $last_idx=-1; our $playlist=undef; my %list; my $D=0; my $debug_pos=0; my $write_lists=1; my $write2; my $base_list=''; sub out1 { #print STDERR "out1\n"; return unless keys %list; $write2=scalar (keys %{$list{ADD}}) + scalar (keys %{$list{DEL}}); print "write2=$write2\n"; for my $l ('ADD','DEL','SEEN') { next unless $write2 || $l eq 'SEEN'; unless(open(O,'>',"$list_out.$l") ) { print STDERR "!!! cant create output list '$list_out'\n"; next }; print O "#cd $ENV{PWD}\n"; if($mode=~/default/i || $l!~/DEL/) { for my $K (sort keys %{$list{$l}}) { next if $l eq 'DEL' && $list{$l}->{$K}==2; # skip auto-marked events print O "$K\n" if($list{$l}->{$K}); } } elsif($mode=~/del_seen/i) { # DEL_SEEN mode my %del_seen=(); if($mode=~/^del_seen[3-9]/i) { # delete all seen for my $K (keys %{$list{SEEN}}) { $del_seen{$K}=1 unless (defined $list{ADD}->{$K}); # not touching events after clr/add } } for my $K (keys %{$list{DEL}}) { # delete definetely marked $del_seen{$K}=1 if($list{$l}->{$K}); } for my $K (sort keys %del_seen) { print O "$K\n" if($del_seen{$K}); } } close O; } } sub print_pad { printf "\r%-99s", shift; print @_ if scalar @_; return; =no my $p0=@_[0]; #printf "\r%-99s", @_[0]; #return; #my $f="\r%-99s"; #my $n=scalar @_; my $s=''; my $a; while(defined ($a=shift)) { $s.=$a; } #printf "\r%-99s", $s; print "\r", length $p0, ": $p0\n"; print "\r", length $s, ": $s\n"; #printf "\r\%d \%-99s", length $s, $s; #print "\r", length $s, ' ', $s; printf "\r\%d \%-99s", length $s, $p0."\r"; #printf "\r%d %-99s", length $p0, $p0; =cut } sub print_padnl { my $s=''; my $a; while(defined ($a=shift)) { $s.=$a; } printf "\r%-99s\n", $s; } use POSIX ":sys_wait_h"; # for nonblocking read my %children; $SIG{CHLD} = sub { # don't change $! and $? outside handler local ($!, $?); while ( (my $pid = waitpid(-1, WNOHANG)) > 0 ) { $reap=1; delete $children{$pid}; print "\r\nreap pid $pid\n" if $D>0; #cleanup_child($pid, $?); } }; sub try_prefetch { if($path ne $last_path) { my $idx=$PLAYLIST_IDX{$path}; print_padnl "Playlist $idx/",scalar @PLAYLIST; $last_path=$path; my $next_idx; $next_idx=+1 if($idx>$last_idx); $next_idx=-1 if($idx<$last_idx); $last_idx=$idx; $idx+=$next_idx; return if $idx<0; my $next=$PLAYLIST[$idx]; if(defined $next) { print_padnl "Next: $next size ", -s $next; my $pid; for $pid (keys %children) { print_padnl "Killing prefetch pid $pid"; kill 'KILL', $pid; }; $pid=fork(); unless(defined $pid) { print STDERR "Cant fork! $! \n"; return; } if($pid==0) { my $time_start=time; %list=(); $base_list=''; print_padnl "PREFETCH $$"; my $fh; my $buf; my $files=0; while($prefetch_done<$prefetch_max) { exit 0 if $idx<0 || $idx>=scalar(@PLAYLIST); if( open($fh,"<",$PLAYLIST[$idx]) ) { print_padnl "Prefetching $PLAYLIST[$idx]" if $D>0; while($prefetch_done<$prefetch_max) { my $n=sysread($fh, $buf, $prefetch_block); unless(defined $n) { print_padnl "Error reading $PLAYLIST[$idx]: $!"; exit $?; } last if $n==0; $prefetch_done+=$n; } close $fh; $files++; $idx+=$next_idx; if($files==1) { my $el=time-$time_start; $el=1 unless $el; print_padnl sprintf("Prefetched %0.1f MB in %d seconds. %0.2f MB/s", $prefetch_done/(1<<20), $el, $prefetch_done/(1<<20)/$el ); } }; }; my $el=time-$time_start; $el=1 unless $el; print_padnl sprintf("Prefetched $files files. %0.1f MB in %d seconds. %0.2f MB/s", $prefetch_done/(1<<20), $el, $prefetch_done/(1<<20)/$el ); #exec qw%dd bs=1m of=/dev/null%, "if=$next"; exit 0; } else { $children{$pid}=1; } } } } sub process_lists($) { my $write_lists=shift; for my $l ('DEL','ADD','SEEN') { next unless $write2 || $l eq 'SEEN'; my $n=0; for my $f (keys %{$list{$l}}) { next if $mode=~/default/i && $l eq 'DEL' && $list{$l}->{$f}==2; # skip auto-marked events $n++ if $list{$l}->{$f}; }; #print "base_list $base_list\n"; print STDERR "Total $l entries: $n\n" if $n || $l eq $base_list; if($write_lists){ next if $mode!~/default/i && $l eq 'DEL'; # dont append global list after del_seen system qq%( echo; cat $list_out.$l; ) >> $list_stash.$l%; } } } sub check_lists() { my $s=''; my $add_or_del=undef; for my $l ('DEL','ADD','SEEN') { my $a=$list{$l}->{$path}; next unless defined $a; if($a) { $s.="$l "; next if $l eq 'SEEN'; $add_or_del|=1; next; }; next if $l eq 'SEEN'; $add_or_del|=0; } $s="CLR $s" if defined $add_or_del && !$add_or_del; return $s; } sub hms { # return seconds from hh:mm:ss my $str=shift; return undef unless defined $str; if($str=~/(\d+):(\d+):(\d+)/) { return ($1*60+$2)*60+$3; } return undef; } our %vidtime; our $pathtime=undef; sub get_shottime() { print "get_shottime($path)\n"; $pathtime=$vidtime{$path} ; unless(defined $pathtime) { my @times=(stat($path))[8,9]; $pathtime=\@times; } if(defined $pathtime) { $vidtime{$path}=$pathtime; if($D>1) { use Data::Dumper; $Data::Dumper::Sortkeys=1; print Data::Dumper->Dump([$pathtime], ['pathtime']) ; print Data::Dumper->Dump([\%vidtime], ['vidtime']) ; } my $offset=hms($curpos)-hms($total_len); my $shottime=$$pathtime[1]+$offset; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($shottime); $year += 1900; my $local=localtime($shottime); if ($path=~m%^(?:/\S+/)?(\d+)/\1-video.mp4$%) { #print STDERR "zm record $1 - $_\n"; $local.="\n".sprintf("%02d:%02d:%02d $1 \n",$hour,$min,$sec); #$list{$base_list}->{$1}=1; } print "pathtime: $$pathtime[1] offset: $offset shottime: $shottime = $local\n"; return $shottime; } return undef; } $SIG{INT}=sub { exit }; my $a; while ( defined($a=shift @ARGV) ) { #for my $a (@ARGV) { if($a =~ /^(ADD|DEL|SEEN)([Ss])?$/) { $base_list=$1; $write_lists=0; $write2=1; my $base_name=defined($2) ? $list_stash : $list_out; open I, '<', "$base_name.$base_list" or die "cant open $a list '$base_name.$base_list'\n"; while() { chomp; if($base_list eq 'DEL') { if (m%^(?:/\S+/)?(\d+)/\1-video.mp4$%) { print STDERR "zm record $1 - $_\n"; print "rm -rf \"$1\"\n"; $list{$base_list}->{$1}=1; } elsif (m%\.(mp4|avi|webm|mpg|jpg|mov)$%i) { print "rm -f \"$_\"\n"; $list{$base_list}->{$_}=1; } elsif (m%^#cd (/\S+)%i) { print "cd \"$1\"\n"; } elsif (m%^\s*$|^#%) { next } else { print STDERR "refusing to mark '$_' for delete\n"; }} if($base_list =~ m'ADD|SEEN') { if (m%^(?:/\S+/)?(\d+)/\1-video.mp4$%) { print STDERR "zm record $1 - $_\n"; print "$1\n"; $list{$base_list}->{$1}=1; } elsif (m%\.(mp4|avi|webm|mpg|jpg|mov)$%i) { print "$_\n"; $list{$base_list}->{$_}=1; } elsif (m%^#cd (/\S+)%i) { print "cd \"$1\"\n"; } elsif (m%^\s*$|^#%) { next } else { print STDERR "unknown record '$_' to add\n"; }} } exit 0; } elsif($a=~/^-delseen/) { $mode='del_seen'; } elsif($a=~/^-i/) { $path=shift; print STDERR "Got filename hint: $path\n"; } elsif($a=~/^-playlist/) { $playlist=shift; print STDERR "Got playlist: $playlist\n"; } elsif($a=~/^-/) { print "Usage: mpv ... | $0\t\tCatch list control commands from mpv $0 [ADD|DEL|SEEN] Display list content $0 Use alternative 1-time lists path $0 -i \"filename\" mpv input file hint $0 -playlist \"filename\" mpv playlist file 1-time lists: \t$list_out.ADD $list_out.DEL $list_out.SEEN cumulative lists: \t$list_stash.ADD $list_stash.DEL $list_stash.SEEN "; exit 64 } else{ $list_out=$a; } }; our @PLAYLIST=( $path ); our %PLAYLIST_IDX=( $path => 0 ); if(defined $playlist) { if(-r $playlist) { my $pl; open($pl,"<", $playlist); while(<$pl>) { chomp; if(m%^/%) { $PLAYLIST_IDX{$_}=scalar @PLAYLIST; push @PLAYLIST, $_; } } close $pl; use Data::Dumper; $Data::Dumper::Sortkeys=1; print Data::Dumper->Dump([\@PLAYLIST, \%PLAYLIST_IDX], ['PLAYLIST', 'PLAYLIST_IDX']) ; }} # pipe input processing (mpp helper) our $curpos; our $total_len; our $curpos_pct; our $mark_pos; our $last_pos=''; our $reap=0; my $next_ready=1; my $got_pos=0; my $buf; end1: while(1) { my @READ= $s->can_read($s); if(!scalar @READ) { #my $s=join ', ',$s->handles; print("select empty return: $@ / $! ") unless $reap; $reap=0; exit 5 if $! eq 'Broken pipe'; } foreach my $fh (@READ) { if($fh eq \*STDIN) { my $buf1; my $cnt=sysread($fh,$buf1,$BUF_SIZE); if($cnt) { $buf.=$buf1; #print "$cnt: ", ">>$buf<<"; my @A=split/[\r\n]+/s, $buf; $buf=''; for my $line (@A) { if($line=~/^Playing:\s*(.+)$/) { print "\n\n$line\n"; $path=$1; print STDERR "PATH: $path ", check_lists(), "\n"; $got_pos=0; print STDERR "got_pos:$got_pos curpos:",defined $curpos?$curpos:'undef',"\n"; #$last_pos=defined $curpos ? "$curpos/$total_len/$curpos_pct":""; $last_pos="$curpos/$total_len/$curpos_pct" if defined $curpos; #$last_path=$path; $curpos=undef; $total_len=undef; $curpos_pct=undef; $mark_pos=undef; $pathtime=undef; } elsif($line=~/^LIST_(ADD|DEL|CLR)\s+(.+)$/) { print "$line\n"; my ($op,$p)=($1,$2); if($path ne $p) { print STDERR "$op!!! $path != $p\n"; $path=$p; } print STDERR "$op\t$path\n"; $list{ADD}->{$path}=0; $list{DEL}->{$path}=0; if($op ne 'CLR') { $list{$op}->{$path}=1; } out1(); } elsif($line=~/^DEBUG (\d+)$/) { print "$line\n"; $D=$1; $debug_pos=$D>1; } elsif($line=~/^IGNORE_SEEN/) { # default mode $mode='default'; goto mode1; } elsif($line=~/^DEL_SEEN/) { # delete seen avents unless been cleared/added if($mode=~/^del_seen(\d*)$/) { my $n=$1||1; $n++; $n=9 if $n>9; $mode="del_seen$n"; } else { $mode='del_seen'; } mode1: print "MODE: $mode \n"; process_lists(0); out1(); } elsif($line=~/^SEEN_QUICK/) { print "\n\n$line Mark seen partially seen\n"; $seen_quick=1; } elsif($line=~/^SEEN_FULL/) { print "\n\n$line Mark seen only fully seen (default)\n"; $seen_quick=0; } elsif($line=~/^LIST_SHOW/) { print "\n\n$line mode $mode; ",check_lists(),"\n"; process_lists(0); } elsif($line=~/^FILE .+? = (.+)$/) { print "\n\n$line\n"; my ($path1)=$1; if($path ne $path1) { print STDERR "path not match! $path != $path1\n"; $path=$path1; } print STDERR "CURPOS: $curpos ",check_lists()," FILE: '$path' \n"; $path1=$path; my $zm_event=undef; my $out=$path; $out=~s%^.+/%%; if ($path=~m%^(?:/\S+/)?(\d+)/\1-video.mp4$%) { $zm_event=$1; $out="cut.$zm_event.mp4"; } get_shottime(); if(defined $curpos) { print qq%cd "$ENV{PWD}" ffmpeg -i "$path1" -c:v copy -ss $curpos $out ffmpeg -i "$path1" -c:v copy -to $curpos $out %; print qq%ffmpeg -i "$path1" -c:v copy -ss $mark_pos -to $curpos $out \n% if(defined $mark_pos); } } elsif($line=~m!^MARK += *(\d+:\d+:\d+) !i) { $mark_pos=$1; print "MARK: $mark_pos \n"; } # V: 00:00:09 / 00:00:09 (100%) x4.00 # (Paused) V: 00:00:09 / 00:00:09 (99%) x4.00 DS: 1.495/15 # AV: 00:00:17 / 00:00:20 (83%) x6.00 A-V: 0.000 DS: 1/0 elsif($line=~m!(\(Paused\) *)?A?V: *(\d+:\d+:\d+) */ *(\d+:\d+:\d+) *\((\d+)%\)!i) { $curpos=$2; $total_len=$3; $curpos_pct=$4; my $curpos_str="$curpos/$total_len/$curpos_pct"; my $short_seen=0; if ($curpos_str ne $last_pos) { $next_ready=1; $got_pos=1; try_prefetch(); my $total_sec=hms($total_len); print "total_sec:$total_sec curpos_pct:$curpos_pct \n" if $D>1; $short_seen=1 if($total_sec<60 && $curpos_pct>58); $short_seen=1 if($total_sec<20 && $curpos_pct>58); $short_seen=1 if($total_sec<13 && hms($curpos)>4); $short_seen=1 if($total_sec<10 && hms($curpos)>3); $short_seen=1 if($total_sec<7 && hms($curpos)>2); $short_seen=1 if($total_sec<5 && hms($curpos)>0); }; #my $debug1 = $debug_pos && #print "\r$line nr:$next_ready ss:$short_seen", $debug_pos?"\n":"\r"; #print "\r$line nr:$next_ready ",check_lists(), ' ', $debug_pos?"\n":"\r"; #printf "\r%-99s%s", "$line nr:$next_ready ss:$short_seen ".check_lists(), $debug_pos?"\n":"\r"; print_pad "$line nr:$next_ready ss:$short_seen ".check_lists(), $debug_pos?"\n":"\r"; if ($next_ready && ($curpos_pct>75 || $seen_quick && ($curpos_pct>9) ) || $short_seen) { $next_ready=0; my $old=$list{SEEN}->{$path}; $list{SEEN}->{$path}=$curpos_pct; my $call_out=0; # mark seen if(!defined $old) { print "\r*** SEEN $curpos / $total_len = $curpos_pct% = $path \n"; $call_out=1; #out1(); } # mark for delete if($mode=~/^del_seen/ && !defined $list{DEL}->{$path}) { $list{DEL}->{$path}=2; print "\r*** DEL $curpos / $total_len = $curpos_pct% = $path \n"; $call_out=1; } out1() if $call_out; #} elsif ($curpos_pct<64) { # $next_ready=1; } } elsif($line=~m!^\x1b|^\s*$!i) { print "$line"; } elsif($line=~m!^Screenshot: '(.+)'!i) { my $shotfile=$1; print "$line \n"; my $shottime=get_shottime(); if(defined $shottime) { utime($$pathtime[0],$shottime,$shotfile); } system qq%ls -ld "$shotfile" "$path"%; } elsif($line=~m!^\[(ffmpeg\S*?|file|input|lavf|vaapi|osd)\]|^Behavior of|^File tags:|^ Comment:|^ Title:|^AO:|^VO:|^Audio/Video |^hardware, |^position will not match|^Failed to recognize|^Cannot load |^Invalid video timestamp:|^ \(\+\) (Video|Audio) |^ Subs |^Exiting|^Failed to open!i) { #print "$line\n"; #printf "%-99s\n", $line; print_padnl $line; } else { # catch last #print "$line\n"; #print length($line)," $line\n"; printf "%-2d %-96s\n", length($line), $line; #$buf=$line; #$buf=''; =no use lib '/home/pdc/sg'; use cgi_util; print "... ",sprinthex($line), ">$line<\n"; =cut } } } else { print "zero read from STDIN\n"; last end1; } } } } END { if (keys %list || $base_list ne '') { if($write_lists){ out1(); } process_lists($write_lists); } #exit 0; }