forked from hummypkg/sweeper
1220 lines
27 KiB
Plaintext
1220 lines
27 KiB
Plaintext
|
|
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_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
|
|
}
|
|
|