require epg.class set ::sweeper::cf "/mod/etc/sweeper.conf" set ::sweeper::dryrun 0 set ::sweeper::lastruleresult 0 set ::sweeper::stack {} set ::sweeper::dustbin [system dustbin 1] proc ::sweeper::unknown {cmd args} { log "Unknown sweeper rule clause '$cmd'" 0 return 0 } alias ::sweeper::log ::auto::log ###################################################################### # Utility functions proc ::sweeper::skipdir {e} { if {[string match {\[*} [string trimleft $e]]} { return 1 } if {$e eq $::sweeper::dustbin} { return 1 } return 0 } # Perform an integer comparison. proc ::sweeper::intcomp {ref val} { lassign $val op num if {$num eq ""} { set num $op set op "==" } return [expr $ref $op $num] } # Substring/pattern check proc ::sweeper::strcontains {ref val} { if {[string index $val 0] eq "~"} { return [regexp -nocase -- [string range $val 1 end] $ref] } if {[string first "*" $val] > -1} { return [string match -nocase $val $ref] } return [expr \ [string first [string tolower $val] [string tolower $ref]] \ >= 0] } set ::sweeper::expand_fns { replace {3 {inline fallback}} regsub {3 {inline fallback}} asfilename {1 {inline}} asuniqfilename {1 {inline}} format {2 {inline}} var {1 {inline}} } proc ::sweeper::expand_fb_replace {ts &ret search replace} { set ret [string map [list $search $replace] $ret] } proc ::sweeper::expand_fb_regsub {ts &ret search replace} { if {[catch { regsub -all -- $search $ret $replace ret } msg]} { log "Error. %regsub - $msg" } } proc ::sweeper::expand_replace {ts &ret arg search replace} { set arg [string map [list $search $replace] $arg] return $arg } proc ::sweeper::expand_regsub {ts &ret arg search replace} { if {[catch { regsub -all -- $search $arg $replace arg } msg]} { log "Error. %regsub - $msg" } return $arg } proc ::sweeper::expand_asfilename {ts &ret arg} { return [system filename $arg] } proc ::sweeper::expand_asuniqfilename {ts &ret arg} { set arg [system filename $arg] if {[string index $arg 0] ne "/"} { set path "[$ts dir]/$arg" } else { set path $arg } if {[file isfile "$path.ts"]} { set i 2 while {[file isfile "${path}_$i.ts"]} { incr i } append arg "_$i" } return $arg } proc ::sweeper::expand_format {ts &ret format arg} { if {[catch {set r [format $format $arg]} msg]} { log "Error. %format - $msg" return "!FORMAT-ERROR!" } return $r } proc ::sweeper::expand_var {ts &ret arg} { if {![dict exists $::sweeper::stack $arg]} { log "Error. Variable does not exist." return "" } return $::sweeper::stack($arg) } # Expand a string containing tokens proc ::sweeper::expand {ts str {orig ""}} { if {[string first "%" $str] == -1} { return $str } # First process any extended functions set ret $str set fnstack {} foreach {fn params} $::sweeper::expand_fns { lassign $params numargs flags set ls -1 while {[set s [string first "%$fn" $ret]] >= 0} { if {$s <= $ls} break set ls $s # Fetch the delimiter set chpos $($s + [string length $fn] + 1) set ch [string index $ret $chpos] #log "Found FN $fn @ $s \[delim:$ch@$chpos]" 2 # Extract the arguments set pos $chpos set fnargs {} set e -1 while {[llength $fnargs] < $numargs} { incr pos set le $e set e [string first $ch $ret $pos] if {$e == -1} { # Insufficient arguments. set argcnt [llength $fnargs] if {"fallback" in $flags && "inline" in $flags && [expr $argcnt + 1] == $numargs} { log "%$fn - falling back." 2 set fn "fb_$fn" set flags {} set e $le } else { log "Error. %$fn - $argcnt/$numargs parameters found." } break } lappend fnargs [::sweeper::expand $ts [ string range $ret $pos $($e - 1)] $orig] set pos $e } # s points to the start, e.g. %function # e points to the last delimiter or -1 if insufficient # arguments were found. if {$e == -1} break if {"inline" in $flags} { # For inline functions, replace the call with # the result. set fnret [ ::sweeper::expand_$fn $ts ret {*}$fnargs] set oret $ret set ret [string replace $ret $s $e $fnret] log " $fn\($oret) -> \[$ret]" 2 } else { # otherwise, queue the function up for later # execution. lappend fnstack $fn $fnargs # and remove the call set ret [string replace $ret $s $e] } } } #log "FNSTACK: $fnstack" 2 lassign [$ts genre_info] genre set start [$ts get start] set timestamp [clock format $start -format "%Y%m%d%H%M%S"] set yyyymmdd [string range $timestamp 0 7] set hhmm [string range $timestamp 8 11] set hh [string range $hhmm 0 1] set mm [string range $hhmm 2 3] set end [$ts get end] set etimestamp [clock format $end -format "%Y%m%d%H%M%S"] set eyyyymmdd [string range $etimestamp 0 7] set ehhmm [string range $etimestamp 8 11] set ehh [string range $ehhmm 0 1] set emm [string range $ehhmm 2 3] set map [list \ "%orig" $orig \ "%title" [$ts get title] \ "%genre" $genre \ "%definition" [$ts get definition] \ "%synopsis" [$ts get synopsis] \ "%lcn" [$ts get channel_num] \ "%channel" [$ts get channel_name] \ "%duration" [$ts duration] \ \ "%filename" [$ts get file] \ "%basename" [$ts bfile] \ "%folder" [$ts dir] \ "%bfolder" [relativedir [$ts dir]] \ \ %epname [$ts episode_name] \ %series [$ts get seriesnum] \ %episodes [$ts get episodetot] \ %episode [$ts get episodenum] \ %epdescr [$ts epstr] \ \ "%timestamp" $timestamp \ "%yyyymmdd" $yyyymmdd \ "%hhmm" $hhmm \ "%hh" $hh \ "%mm" $mm \ "%year" [clock format $start -format "%Y"] \ "%month" $(0 + [clock format $start -format "%m"]) \ "%date" $(0 + [clock format $start -format "%d"]) \ "%2digityear" [clock format $start -format "%y"] \ "%2digitmonth" [clock format $start -format "%m"] \ "%2digitdate" [clock format $start -format "%d"] \ "%shortday" [clock format $start -format "%a"] \ "%longday" [clock format $start -format "%A"] \ "%shortmonth" [clock format $start -format "%b"] \ "%longmonth" [clock format $start -format "%B"] \ \ "%etimestamp" $etimestamp \ "%eyyyymmdd" $eyyyymmdd \ "%ehhmm" $ehhmm \ "%ehh" $ehh \ "%emm" $emm \ ] #log "STACK: $::sweeper::stack" 2 foreach {key val} $::sweeper::stack { #log "MAP(%%$key) -> $val" 2 set map(%%$key) $val } #log $map 2 set ret [string map $map $ret] log " Expanded \[$str] -> \[$ret]" 2 # Now call any queued extended functions foreach {fn fnargs} $fnstack { log " - Calling expand_$fn\($fnargs)" 2 ::sweeper::expand_$fn $ts ret {*}$fnargs log " Result: ($ret)" 2 } return $ret } proc ::sweeper::resolvedir {dir} { if {$dir eq ""} { return $::auto::root } if {[string index $dir 0] eq "/"} { return $dir } return "$::auto::root/$dir" } proc ::sweeper::relativedir {dir} { set dir [string map \ [list [file normalize $::auto::root] ""] \ [file normalize $dir]] if {[string index $dir 0] eq "/"} { set dir [string range $dir 1 end] } return $dir } # returns true if the arguments are actually the same file proc ::sweeper::samefile {a b} { if {![file exists $a] || ![file exists $b]} { return 0 } if {[file stat $a] eq [file stat $b]} { return 1 } return 0 } # Move/copy the set of files which make up a recording proc ::sweeper::moveset {ts dst {op rename}} { set file [$ts get file] log "${op}set($file) -> $dst" 0 # Handle alias for rename if {$op eq "move"} { set op "rename" } # Determine whether this is a cross-filesystem move. file stat [$ts get file] sts file stat $dst std set xfs 0 if {$sts(dev) ne $std(dev)} { log " Cross-filesystem - will copy then delete." 0 set xfs 1 } set fset [lsort [$ts fileset]] # Handle cross-filesystem (xfs) moves if {$xfs && $op eq "rename"} { # For cross-filesystem moves, copy the whole file set # and then delete the originals if all the copies were # successful. foreach f $fset { set tail [file tail $f] if {[::sweeper::samefile $f "$dst/$tail"]} { log " Destination is same as source." 0 return } if {[catch {file copy $f "$dst/$tail"} msg]} { log " ....... $f: XFS copy failed, $msg." 0 file delete -force "$dst/$tail" log " .... Leaving originals intact." 0 return } log " ....... $f: OK" 0 } log " Now deleting original files." 0 foreach f $fset { set tail [file tail $f] if {[file exists "$dst/$tail"] && [file size "$dst/$tail"] == [file size $f]} { file tdelete $f log " ....... $f: OK" 0 } else { log " ....... $f: ERROR, sizes differ." 0 return } } if {$op eq "rename"} { set ::sweeper::renames($file) "$dst/[file tail $file]" } return } # Otherwise - copy or local FS move. foreach f $fset { set tail [file tail $f] if {[::sweeper::samefile $f "$dst/$tail"]} { log " Destination is same as source." 0 return } if {$op eq "copy" && [file exists "$dst/$tail"]} { if {[file size "$dst/$tail"] == [file size $f]} { log " ....... $f: Already copied." 2 continue } log "Deleting truncated copy $dst/$tail" 0 file tdelete "$dst/$tail" } log " ....... $f" if {[catch {file $op $f "$dst/[file tail $f]"} msg]} { log "$op failed, $msg." 0 return } } if {$op eq "rename"} { set ::sweeper::renames($file) "$dst/[file tail $file]" } } # Search for a folder with name 'target' under 'root', skipping 'orig' proc ::sweeper::find {root target orig} { foreach e [readdir -nocomplain $root] { regsub -all -- {//} "$root/$e" "/" entry if {![file isdirectory $entry]} continue if {[::sweeper::skipdir $e]} continue if {$entry eq $orig} continue if {$e eq $target} { return $entry } set ret [::sweeper::find $entry $target $orig] if {$ret ne ""} { return $ret } } return "" } # Apply a function to all recordings in a directory. proc ::sweeper::folder_apply {dir callback args} { log "Applying action to recordings in $dir" 2 foreach e [readdir -nocomplain $dir] { if {![string match {*.ts} $e]} continue set entry "$dir/$e" log "+ folder_apply processing $entry" 2 if {[catch {set ts [ts fetch $entry]} msg]} { log "Error reading TS file, $msg" 0 continue } if {$ts == "0"} { log "Invalid TS file." 2 continue } if {[$ts inuse]} { log "Recording in use." 2 continue } $callback $ts {*}$args } } proc ::sweeper::rmdir_if_empty {dir} { if {$dir eq $::auto::root} return if {![system rmdir_if_empty $dir ".series"]} { log "Failed to remove directory" 0 foreach l [system dirblockers $dir] { log "Blocking file: $l" 2 } } } proc ::sweeper::qrecalc {args} { foreach dst $args { if {$dst ni $::sweeper::recalc} { lappend ::sweeper::recalc $dst } } } ###################################################################### # Rule conditions # # Parameters: # ts - instance of the ts class for the recording being processed. # arg - arguments provided for the action. # folder - true if the action is being applied to a folder. # # Return values: # # 0 - condition does not match. # 1 - condition matched. proc ::sweeper::lastrule {ts flag folder} { return $::sweeper::lastruleresult } proc ::sweeper::flag {ts flag folder} { return [$ts flag $flag] } proc ::sweeper::queue {ts q folder} { set queues [split [{queue status} $ts] ,] if {$q eq "any" && [llength $queues]} { return 1 } return $($q in $queues) } proc ::sweeper::lcn {ts num folder} { return [::sweeper::intcomp [$ts get channel_num] $num] } proc ::sweeper::duration {ts dur folder} { return [::sweeper::intcomp [$ts duration] $dur] } proc ::sweeper::hour {ts str folder} { set hour [clock format [$ts get start] -format "%H"] return [::sweeper::intcomp $hour $str] } proc ::sweeper::now {ts str folder} { set now [clock format [clock seconds] -format "%H%M"] return [::sweeper::intcomp $now $str] } proc ::sweeper::schedduration {ts dur folder} { return [::sweeper::intcomp [expr [$ts get scheddur] / 60] $dur] } proc ::sweeper::size {ts size folder} { return [::sweeper::intcomp [$ts size] $size] } proc ::sweeper::age {ts age folder} { set recage $(([clock seconds] - [$ts get end]) / 3600) log " ... Recording age: $recage" 2 return [::sweeper::intcomp $recage $age] } proc ::sweeper::wage {ts age folder} { set recage $(([clock seconds] - [$ts lastmod]) / 3600) log " ... Watched age: $recage" 2 return [::sweeper::intcomp $recage $age] } proc ::sweeper::bookmarks {ts bookmarks folder} { return [::sweeper::intcomp [$ts get bookmarks] $bookmarks] } proc ::sweeper::definition {ts def folder} { return [::sweeper::strcontains [$ts get definition] $def] } proc ::sweeper::filename {ts str folder} { return [::sweeper::strcontains [$ts bfile] $str] } proc ::sweeper::title {ts str folder} { return [::sweeper::strcontains [$ts get title] $str] } proc ::sweeper::synopsis {ts str folder} { return [::sweeper::strcontains [$ts get synopsis] $str] } proc ::sweeper::guidance {ts str folder} { return [::sweeper::strcontains [$ts get guidance] $str] } proc ::sweeper::genre {ts genre folder} { lassign [$ts genre_info] tsgenre if {$tsgenre eq $genre} { return 1 } return 0 } proc ::sweeper::fflag {ts flag folder} { return [file exists "[$ts dir]/.$flag"] } proc ::sweeper::foldername {ts str folder} { return [::sweeper::strcontains [$ts dir] $str] } proc ::sweeper::fileexists {ts str folder} { if {[string index $str 0] ne "/"} { set str "[$ts dir]/[::sweeper::expand $ts $str]" } else { set str [::sweeper::expand $ts $str] } log " FILEEXISTS($str)" 2 set matches [glob -nocomplain \ -directory [file dirname $str] \ -tails [file tail $str]] log " Matches($matches)" 2 return $([llength $matches] > 0) } proc ::sweeper::direxists {ts str folder} { if {[string index $str 0] ne "/"} { set str "[$ts dir]/[::sweeper::expand $ts $str]" } else { set str [::sweeper::expand $ts $str] } log " DIREXISTS($str)" 2 return [file isdirectory $str] } proc ::sweeper::series {ts flag folder} { if {!$folder} { return 0 } set dir [$ts dir] if {![file exists "$dir/.series"]} { log "Not series folder (nofile)." 2 return 0 } # Check if the .series entry is a real one versus one created # by ts resetnew set fd [open "$dir/.series"] set bytes [read $fd] close $fd set sbytes [unpack $bytes -uintle 160 32] if {$sbytes == 0} { log "Not series folder." 2 return 0 } return 1 } proc ::sweeper::varset {ts var folder} { return [dict exists $::sweeper::stack $var] } proc ::sweeper::textmatch {ts str folder} { if {![regexp -- {^([^~]+)~~(.*)$} $str x target pattern]} { log "No pattern in textmatch." 1 return 0 } log "Textmatch ($target) against ($pattern)" 2 return [::sweeper::strcontains [::sweeper::expand $ts $target] \ [::sweeper::expand $ts $pattern]] } proc ::sweeper::intmatch {ts str folder} { if {![regexp -- {^([^~]+)~~(.*)$} $str x target pattern]} { log "No pattern in intmatch." 1 return 0 } if {[llength [split $pattern " "]] != 2} { log "Invalid pattern in numeric comparison." 0 return 0 } log "Intmatch ($target) against ($pattern)" 2 return [::sweeper::intcomp [::sweeper::expand $ts $target] $pattern] } ######################## # Deprecated conditions proc ::sweeper::lock {ts g folder} { if {$g} { if {![$ts flag Locked]} { log "Locked recording." 0 $ts lock } } else { if {[$ts flag Locked]} { log "Unlocked recording." 0 $ts unlock } } # Always matches return 1 } ###################################################################### # Rule actions. # # Parameters: # ts - instance of the ts class for the recording being processed. # cmd - name of action being processed. # arg - arguments provided for the action. # folder - true if the action is being applied to a folder. # # Return values: # # 0 - continue to next rule # 1 - stop processing proc ::sweeper::action_continue {ts cmd arg folder} { return 0 } proc ::sweeper::action_stop {ts cmd arg folder} { return 1 } alias ::sweeper::action_preserve ::sweeper::action_stop proc ::sweeper::action_move {ts cmd arg folder} { set dir [::sweeper::resolvedir [::sweeper::expand $ts $arg]] set create 0 set ocmd $cmd if {[string range $cmd end-5 end] eq "create"} { set create 1 set cmd [string range $cmd 0 end-6] } if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_move $ocmd $arg 0 if {$cmd eq "move"} { ::sweeper::rmdir_if_empty [$ts dir] } return 1 } if {![file isdirectory $dir]} { if {!$create} { log " ... No such directory $dir" 2 return 1 } if {!$::sweeper::dryrun} { system mkdir_p $dir if {![file isdirectory $dir]} { log "Error creating $dir" 1 return 1 } else { log " ... created $dir" 2 } } } log "$cmd [$ts get file] to $arg" 0 if {!$::sweeper::dryrun} { ::sweeper::moveset $ts $dir $cmd ::sweeper::qrecalc $dir [$ts dir] if {$cmd eq "move"} { ::sweeper::rmdir_if_empty [$ts dir] } } return 1 } alias ::sweeper::action_movecreate ::sweeper::action_move alias ::sweeper::action_copy ::sweeper::action_move alias ::sweeper::action_copycreate ::sweeper::action_move proc ::sweeper::action_fileunder {ts cmd arg folder} { if {!$folder} { log "$cmd action can only be applied to folders." return 1 } set dir [::sweeper::resolvedir $arg] if {![file isdirectory $dir]} { log " ... No such directory $dir" 2 return 1 } set folder [$ts dir] set lfolder [file tail $folder] log " - searching for $lfolder under $dir" 2 set target [::sweeper::find $dir $lfolder $folder] log " = $target" 2 if {$target eq "" || ![file isdirectory $target]} { log "Did not find directory." 2 if {$cmd ne "fileundercreate"} { return 1 } set target "$dir/$lfolder" if {$target eq $folder} { log "Skipping merge to self." 2 return 1 } log "Creating $target" 0 if {!$::sweeper::dryrun} { system mkdir_p $target } } if {$::sweeper::dryrun} { return 1 } ::sweeper::folder_apply $folder [ lambda {ts target} { ::sweeper::moveset $ts $target rename }] $target ::sweeper::rmdir_if_empty $folder ::sweeper::qrecalc $folder $target return 1 } alias ::sweeper::action_fileundercreate ::sweeper::action_fileunder proc ::sweeper::action_renamefile {ts cmd arg folder} { set dir [$ts dir] if {$folder} { ::sweeper::folder_apply $dir \ ::sweeper::action_renamefile $cmd $arg 0 return 0 } set arg [system filename [::sweeper::expand $ts $arg [$ts bfile]]] set file [$ts get file] log "Renaming $file to $arg" 0 if {[file exists "$dir/$arg.ts"]} { log "... ERROR Target already exists" 0 return 0 } if {[::sweeper::samefile $file "$dir/$arg.ts"]} { log "... ERROR Target is the same as source" 0 return 0 } if {!$::sweeper::dryrun} { ts renamegroup $file $arg if {[file exists "$dir/$arg.ts"]} { $ts setfile "$dir/$arg.ts" set ::sweeper::renames($file) "$dir/$arg.ts" } else { log "... ERROR renamefile somehow failed" 0 } } return 0 } proc ::sweeper::action_settitle {ts cmd arg folder} { if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_settitle $cmd $arg 0 return 0 } set arg [::sweeper::expand $ts $arg [$ts get title]] log "Setting title for [$ts get file] to $arg" 0 if {!$::sweeper::dryrun} { $ts settitle $arg } return 0 } proc ::sweeper::action_setguidance {ts cmd arg folder} { if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_setguidance $cmd $arg 0 return 0 } set arg [::sweeper::expand $ts $arg [$ts get guidance]] log "Setting guidance for [$ts get file] to $arg" 0 if {!$::sweeper::dryrun} { $ts setguidance $arg } return 0 } proc ::sweeper::action_setsynopsis {ts cmd arg folder} { if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_setsynopsis $cmd $arg 0 return 0 } set arg [::sweeper::expand $ts $arg [$ts get synopsis]] log "Setting synopsis for [$ts get file] to $arg" 0 if {!$::sweeper::dryrun} { $ts setsynopsis $arg } return 0 } proc ::sweeper::action_lock {ts cmd arg folder} { if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_lock $cmd 0 0 return 0 } if {![$ts flag Locked]} { log "Locked [$ts get file]" 0 if {!$::sweeper::dryrun} { $ts lock } } return 0 } proc ::sweeper::action_unlock {ts cmd arg folder} { if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_unlock $cmd 0 0 return 0 } if {[$ts flag Locked]} { log "Unlocked [$ts get file]" 0 if {!$::sweeper::dryrun} { $ts unlock } } return 0 } proc ::sweeper::action_delete {ts cmd arg folder} { if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_delete $cmd 0 0 return } log "Deleting [$ts get file]" 0 if {!$::sweeper::dryrun} { safe_delete [$ts get file] sweeper } return 1 } proc ::sweeper::action_log {ts cmd arg folder} { if {$folder} { ::sweeper::folder_apply [$ts dir] \ ::sweeper::action_log $cmd $arg 0 return 0 } set arg [::sweeper::expand $ts $arg] log "LOG: '$arg'" 0 return 0 } proc ::sweeper::action_set {ts cmd arg folder} { set val [join [lassign [split $arg =] var] "="] set val [::sweeper::expand $ts $val] log "Set '$var'='$val'" if {![string length $val]} { unset -nocomplain $::sweeper::stack($var) } else { set ::sweeper::stack($var) $val } return 0 } proc ::sweeper::action_flag {ts cmd arg folder} { log "Flagged [$ts get file] as $arg" 0 if {!$::sweeper::dryrun} { $ts setflag $arg } else { $ts setflag $arg 1 } return 0 } proc ::sweeper::action_unflag {ts cmd arg folder} { log "Unflagged [$ts get file] as $arg" 0 if {!$::sweeper::dryrun} { $ts unsetflag $arg } else { $ts unsetflag $arg 1 } return 0 } proc ::sweeper::action_queue {ts cmd arg folder} { log "Queued [$ts get file] for $arg" 0 set opt [lassign $arg arg] log "ARG: ($arg) OPT: ($opt)" 2 if {!$::sweeper::dryrun} { set q [{queue insert} -hold $ts $arg] if {[string trim $opt] ne ""} { $q set args $opt } $q submit } return 0 } proc ::sweeper::action_dequeue {ts cmd arg folder} { log "De-queued [$ts get file] for $arg" 0 if {!$::sweeper::dryrun} { if {$arg eq "all"} { {queue delete} $ts } else { {queue delete} $ts $arg } } return 0 } eval_plugins sweeper ###################################################################### # Handle action proc ::sweeper::action {ts cmds folder} { lassign $cmds cmd rest log "ACTION: $cmd\($rest) \[$folder]" 2 return [::sweeper::action_$cmd $ts $cmd $rest $folder] } ###################################################################### # Handle clauses proc ::sweeper::or {ts clause folder} { log " --> OR:" 2 set ret 0 while {[llength $clause] > 1} { set clause [lassign $clause cmd arg] set ret [::sweeper::clause $folder $cmd $arg $ts] if {$ret} { log " <-- OR true." 2 break } } if {!$ret} { log " <-- OR false." 2 } return $ret } proc ::sweeper::and {ts clause folder} { log " --> AND:" 2 set ret 0 while {[llength $clause] > 1} { set clause [lassign $clause cmd arg] set ret [::sweeper::clause $folder $cmd $arg $ts] if {!$ret} { log " <-- AND false." 2 break } } if {$ret} { log " <-- AND true." 2 } return $ret } proc ::sweeper::clause {folder cmd arg ts} { log " $cmd\($arg)" 2 if {[string index $cmd 0] eq "!"} { set negate 1 set cmd [string range $cmd 1 end] } else { set negate 0 } set ret [::sweeper::$cmd $ts $arg $folder] if {$cmd eq "action"} { return $ret } if {$negate} { set ret $(!$ret) } if {!$ret} { log " Nomatch" 2 } else { log " MATCH" 2 } return $ret } ###################################################################### proc ::sweeper::runrule {ts rule folder} { log "Processing \[$rule]" 2 if {[string index $rule 0] eq "#" || [llength $rule] < 2} { return 0 } while {[llength $rule] > 1} { set rule [lassign $rule cmd arg] set ret [::sweeper::clause $folder $cmd $arg $ts] if {$cmd eq "action"} { set ::sweeper::lastruleresult 1 return $ret } if {!$ret} break } set ::sweeper::lastruleresult 0 return 0 } proc ::sweeper::apply_folder_rules {dir rules} { log "" 2 log " -- FOLDER RULES --" 2 log "" 2 foreach e [readdir -nocomplain $dir] { set entry "$dir/$e" if {![file isdirectory $entry]} continue if {[::sweeper::skipdir $e]} continue log "" 2 log "==== folder $entry ====" 2 log "" 2 if {[file exists "$entry/.nosweep"]} { log "No-sweep folder." 2 continue } set ts 0 foreach de [lsort -command [lambda {a b} { upvar entry e return $([file mtime "$e/$b"] - [file mtime "$e/$a"]) }] [lsearch -glob -all -inline \ [readdir -nocomplain $entry] {*.ts}]] { set dentry "$entry/$de" log " --- Considering $dentry" 2 if {[catch {set ts [ts fetch $dentry]} msg]} { log "Error reading TS file, $msg" 2 continue } if {$ts == "0"} { log "Invalid TS file." 2 continue } if {[$ts inuse]} { log "Recording in use." 2 set ts 0 continue } break } if {$ts == "0"} { log "No usable recordings in folder." 2 continue } set ::sweeper::stack {} foreach rule $rules { if {[llength $rule] < 1} continue if {[::sweeper::runrule $ts $rule 1]} break } } } proc ::sweeper::apply_rules {dir rules {depth 0} {seen {}}} { log "" 2 log "--- SWEEP($depth) STARTING FOR $dir ---" 2 log "" 2 if {$depth > 20} { log "ERROR: Maximum recursion depth exceeded." 0 return } file stat $dir st set key "$st(dev):$st(ino)" if {$key in $seen} { log "Already seen $dir ($key)" 2 return } lappend seen $key set dirs {} foreach e [readdir -nocomplain $dir] { set entry "$dir/$e" if {[file isdirectory $entry]} { if {![::sweeper::skipdir $e] && ![file exists "$entry/.nosweep"]} { lappend dirs $entry } continue } if {![string match {*.ts} $entry]} continue log "+ Sweeper processing $entry" 2 if {[catch {set ts [ts fetch $entry]} msg]} { log "Error reading TS file, $msg" 0 continue } if {$ts == "0"} { log "Invalid TS file." 2 continue } if {[$ts inuse]} { log "Recording in use." 2 continue } set ::sweeper::stack {} foreach rule $rules { if {[llength $rule] < 3} continue set rule [lassign $rule level] if {$level < $depth} continue if {[::sweeper::runrule $ts $rule 0]} break } } set moredepth 0 foreach rule $rules { set level [lindex $rule 0] if {$level > $depth} { incr moredepth break } } # No rules require greater recursion if {!$moredepth} return foreach dir $dirs { ::sweeper::apply_rules $dir $rules $($depth + 1) $seen file stat $dir st set key "$st(dev):$st(ino)" lappend seen $key } } proc ::sweeper::apply {dir cf} { if {[catch {set fp [open $cf r]} msg]} { log "Error opening sweeper ruleset ($cf), $msg" 0 return } set ::sweeper::recalc {} set _rules [split [read $fp] "\n"] $fp close set rules {} set folder_rules {} foreach rule $_rules { if {[string index $rule 0] eq "#"} continue switch -- [lindex $rule 0] { folder { lappend folder_rules [lrange $rule 1 end] } global { lset rule 0 1; lappend rules $rule } recurse { lappend rules [lrange $rule 1 end] } default { lappend rules [linsert $rule 0 0] } } } if {[llength $rules]} { ::sweeper::apply_rules $dir $rules } if {[llength $folder_rules]} { ::sweeper::apply_folder_rules $dir $folder_rules } foreach dir $::sweeper::recalc { log "Resetting unwatched recording flag for $dir" 0 ts resetnew $dir } } # Callback function which is called from scan_run proc ::sweeper::sweep {dir} { if {$dir eq $::auto::root} return apply $dir "$dir/.sweeper" } ###################################################################### # Auto API callbacks proc ::sweeper::run {} { apply $::auto::root $::sweeper::cf ::auto::flagscan $::auto::root sweeper ::sweeper::sweep } proc ::sweeper::rundir {dir} { if {$dir eq $::auto::root} { apply $::auto::root $::sweeper::cf } elseif {[file exists "$dir/.sweeper"]} { apply $dir "$dir/.sweeper" } } if {[file exists $::sweeper::cf]} { ::auto::register sweeper 700 ::auto::register_flag sweeper sweeper }