sweeper/webif/plugin/sweeper/auto.hook

944 lines
21 KiB
Plaintext

set ::sweeper::cf "/mod/etc/sweeper.conf"
set ::sweeper::dryrun 0
set ::sweeper::lastruleresult 0
proc ::sweeper::unknown {cmd args} {
log "Unknown sweeper rule clause '$cmd'" 0
return 0
}
######################################################################
# Utility functions
# 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 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 {2}
regsub {2}
}
proc ::sweeper::expand_replace {ts &ret search replace} {
set ret [string map [list $search $replace] $ret]
}
proc ::sweeper::expand_regsub {ts &ret search replace} {
if {[catch {
regsub -all -- $search $ret $replace ret
} msg]} {
log "Error. %regsub - $msg"
}
}
# Expand a string containing tokens
proc ::sweeper::expand {ts str {orig ""}} {
if {[string first "%" $str] == -1} {
return $str
}
set glist [ts genrelist]
set tsg [$ts get genre]
if {![dict exists $glist $tsg]} {
set genre "Unknown"
} else {
set genre [lindex $glist($tsg) 0]
}
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] \
"%lcn" [$ts get channel_num] \
"%channel" [$ts get channel_name] \
"%duration" [$ts duration] \
\
%epname [$ts episode_name] \
%episode [$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 $map 2
set ret [string map $map $str]
log " Expanded \[$str] -> \[$ret]" 2
foreach {fn params} $::sweeper::expand_fns {
log " Looking for %$fn" 2
lassign $params numargs
set li -1
while {[set i [string first "%$fn" $ret]] >= 0} {
if {$i <= $li} break
set li $i
# Fetch the delimiter
set chpos $($i + [string length $fn] + 1)
set ch [string index $ret $chpos]
log " - found at $i (delim\[$ch])" 2
# Extract the arguments
set pos $chpos
set fnargs {}
while {[llength $fnargs] < $numargs} {
incr pos
set e [string first $ch $ret $pos]
if {$e == -1} {
log "Error. %$fn - [llength $fnargs]/$numargs parameters found."
break
}
lappend fnargs [string range $ret \
$pos $($e - 1)]
set pos $e
}
set ret [string replace $ret $i $pos]
if {[llength $fnargs] == $numargs} {
log " - Calling expand_$fn\($fnargs)" 2
::sweeper::expand_$fn $ts ret {*}$fnargs
}
}
}
return $ret
}
proc ::sweeper::resolvedir {dir} {
global root
if {$dir eq ""} { return $root }
if {[string index $dir 0] eq "/"} { return $dir }
return "$root/$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} {
set dustbin [system dustbin 1]
foreach e [readdir -nocomplain $root] {
regsub -all -- {//} "$root/$e" "/" entry
if {![file isdirectory $entry]} continue
if {[string match {\[*} [string trimleft $e]]} continue
if {$e eq $dustbin} 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 $::root} return
if {[file exists "$dir/.sweeper"]} return
if {![system rmdir_if_empty $dir]} {
log "Failed to remove directory" 0
foreach l [system rmdir_if_empty $dir 1] {
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::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} {
set glist [ts genrelist]
set tsg [$ts get genre]
if {![dict exists $glist $tsg]} { return 0 }
if {[lindex $glist($tsg) 0] 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::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
}
########################
# 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
}
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 {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 runfolder 0
set nrules 0
foreach rule $rules {
switch -- [lindex $rule 0] {
folder -
global { incr runfolder }
}
if {[string index $rule 0] ne "#" && [llength $rule] > 1} {
incr nrules
}
}
if {!$nrules} return
log "" 2
log "--- SWEEP SCAN STARTING FOR $dir ($nrules) ---" 2
log "" 2
foreach e [readdir -nocomplain $dir] {
set entry "$dir/$e"
if {[file isdirectory $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
}
foreach rule $rules {
if {[string index $rule 0] eq "#" ||
[llength $rule] < 2} continue
switch -- [lindex $rule 0] {
folder continue
global { set rule [lrange $rule 1 end] }
}
if {[::sweeper::runrule $ts $rule 0]} break
}
}
if {$runfolder > 0} {
log "" 2
log " -- FOLDER RULES --" 2
log "" 2
set dustbin [system dustbin 1]
foreach e [readdir -nocomplain $dir] {
set entry "$dir/$e"
if {![file isdirectory $entry]} continue
if {[string match {\[*} [string trimleft $e]]} continue
if {$e eq $dustbin} 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
}
foreach rule $rules {
if {[string index $rule 0] eq "#" ||
[llength $rule] < 3} continue
switch -- [lindex $rule 0] {
folder -
global { set rule [lrange $rule 1 end] }
default continue
}
if {[::sweeper::runrule $ts $rule 1]} break
}
}
}
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 $::root} return
::sweeper::apply $dir "$dir/.sweeper"
}
proc ::sweeper::scan {&files} {
log "::sweeper::scan files - ($files)" 2
set ::sweeper::renames {}
::sweeper::apply $::root $::sweeper::cf
scan_run $::root sweeper ::sweeper::sweep
log "::sweeper::scan renames - ($::sweeper::renames)" 2
if {[llength $::sweeper::renames]} {
foreach k [array names ::sweeper::renames] {
set pos [lsearch $files $k]
log "$k = pos $pos" 2
if {$pos == -1} continue
set files [lreplace $files $pos $pos \
$::sweeper::renames($k)]
}
log "::sweeper::scan files - ($files)" 2
}
}
proc ::sweeper::scansingledir {dir} {
if {$dir eq $::root} {
::sweeper::apply $::root $::sweeper::cf
} elseif {[file exists "$dir/.sweeper"]} {
::sweeper::apply $dir "$dir/.sweeper"
}
}
if {[file exists $::sweeper::cf]} {
register postdecryptscan ::sweeper::scan
register postdecryptsingledir ::sweeper::scansingledir
}