#!/mod/bin/jimsh source /mod/webif/lib/setup require lock system.class ts.class tdelete pretty_size browse.class \ safe_delete settings.class plugin set settings [settings] set loglevel [$settings autolog] set audiomp3 [$settings audiomp3] if {![acquire_lock webif_auto]} { puts "Cannot acquire exclusive lock, terminating." exit } set logfile "/mod/tmp/auto.log" # Rotate log file if large enough. if {[file exists $logfile] && [file size $logfile] > 2097152} { file copy -force $logfile "/mod/tmp/auto_old.log" file delete $logfile } if {[lindex $argv 0] eq "-d"} { set argv [lrange $argv 1 end] set loglevel 2 set logfd stdout } else { set logfd [open "/mod/tmp/auto.log" "a+"] } proc log {msg {level 1}} { if {$level > $::loglevel} return puts $::logfd "[\ clock format [clock seconds] -format "%d/%m/%Y %H:%M"\ ] - $msg" flush $::logfd } proc elapsed {start} { return $(([clock milliseconds] - $start) / 1000.0) } proc startclock {} { set ::startclock_s [clock milliseconds] } proc endclock {size} { set el [elapsed $::startclock_s] set rate $($size / $el) return "[pretty_size $size] in $el seconds - [pretty_size $rate]/s" } set modules {decrypt dedup shrink mpg mp3 expire} foreach mod $modules { set "hook_pre${mod}scan" {} set "hook_pre$mod" {} set "hook_post$mod" {} set "hook_post${mod}scan" {} } proc register {type fn} { global "hook_$type" if {[info exists "hook_$type"]} { lappend "hook_$type" $fn log "Registered $fn for $type hook." 1 } else { log "Unknown hook hook_$type" 0 } } proc runplugin {name {ts 0}} { set var "hook_$name" global $var foreach p [subst $$var] { if {[catch {$p $ts} msg]} { log "Plugin error: $msg" 0 } } } eval_plugins auto set scanstart [clock milliseconds] log "-------------------------------------------------------" # is_listening is relatively expensive so it is checked once globally at # the start and then if the server is not listening then no decrypt # operations will be attempted for this run, even if the server starts # up halfway through. Otherwise the server is checked for every decryption # and if it goes away then decryption will not be attempted for the rest # of the run. if {[system is_listening 9000]} { set dlnaok 1 log "DLNA Server is running." 2 } else { set dlnaok 0 log "DLNA Server is NOT running." 2 } log "Media scan starting, DLNA server status: $dlnaok" proc dsc {{size 0}} { set free [system diskfree] # Required disk space is 1GiB + 3 times the file size. set req $($size * 3 + 1073741824) if {$free < $req} { log "Insufficient disk space. Require=$req, Free=$free" 0 exit } } dsc set tmp "/mod/tmp/webif_auto" if {![file exists $tmp]} { if {[catch {file mkdir $tmp} msg]} { log "Cannot create temporary directory - $tmp ($msg)" 0 exit } } elseif {![file isdirectory $tmp]} { log "Cannot create temporary directory - $tmp (file exists)" 0 exit } # Clean-up the temporary directory foreach file [readdir -nocomplain $tmp] { tdelete "$tmp/$file" } if {[system pkginst undelete]} { set dustbin "[system dustbin]" } else { set dustbin "" } log "Dustbin: $dustbin" 2 set recalc 0 proc dorecalc {dir} { global recalc if {!$recalc} return ts resetnew $dir set recalc 0 } proc dedup {dir} { log "DEDUP: \[$dir]" 2 loop i 0 1 { foreach line [split \ [exec /mod/webif/html/dedup/dedup -yes -auto $dir] "\n"] { log $line 2 } } } proc do_expire {ts} { global ax_days set file [$ts get file] # Calculate the age of the file in days. set age $(([clock seconds] - [$ts get start]) / 86400.0) log " EXPIRE: $file (age = $age)" 2 if {$age > $ax_days} { if {[$ts inuse]} { log " EXPIRE: $file ($age > $ax_days)" log " In use." return } runplugin preexpire $ts if {[safe_delete $file]} { log " EXPIRE: $file ($age > $ax_days)" 0 log " Deleted." 0 runplugin postexpire $ts incr ::recalc } } } proc do_shrink {ts} { global tmp dustbin tsgroup set file [$ts get file] if {[$ts flag "Shrunk"]} { log " $file - already shrunk." 2 return } set file [file rootname [$ts get file]] if {[$ts inuse]} { log " $file - in use." 2 return } if {[catch { set perc [exec /mod/bin/stripts -aq $file] } msg]} { log " Error: $msg" 0 return } if {[string match {*%} $perc]} { set perc [string range $perc 0 end-1] } else { set perc 0 } if {$perc == 0} { log " $file - already shrunk." 2 $ts set_shrunk return } set size [$ts size] dsc $size runplugin preshrink $ts startclock log " SHRINK: $file" 0 log " Estimate $perc% saving." 0 log " Shrinking..." 0 if {[catch { foreach line [split \ [exec nice -n 19 /mod/bin/stripts -q $file $tmp/shrunk] \ "\n"] { log $line 0 } } msg]} { log "Error during shrink: $msg" 0 system notify "$file - auto-shrink - error $msg." return } # The following steps are structured to minimise the risk of # things being left in an inconsistent state if the system goes # into standby. Renames within the same filesystem are very # quick so the risk is small, but even so... # Move the shrunken version back to the local directory. foreach f [glob "$tmp/shrunk.*"] { set ext [file extension $f] file rename $f "${file}_shrunk${ext}" } # Remove the old recording (-> bin if undelete is installed) safe_delete [$ts get file] "webif_autoshrink" # Finally, rename the shrunken recording again. foreach ext $tsgroup { set f "${file}_shrunk.$ext" if {[file exists $f]} { file rename $f "${file}.$ext" } } $ts set_shrunk log "Done... [endclock $size]" 0 runplugin postshrink $ts } proc do_decrypt {ts} { global tmp dustbin set file [$ts get file] set rfile [file rootname $file] set bfile [file tail $file] if {![$ts flag "ODEncrypted"]} { log " $file - Already decrypted." 2 return } lassign [$ts dlnaloc "127.0.0.1"] url if {$url eq ""} { log " $file - Not yet indexed." return } if {![system is_listening 9000]} { log " $file - DLNA Server not running." 2 set ::dlnaok 0 return } if {[$ts inuse]} { log " $file - In use." return } # Check that the file is not already decrypted by analysing it. set anencd [exec /mod/bin/stripts -qE $rfile] if {$anencd != "1"} { log " $file - already decrypted but the HMT flag is wrong." 0 system notify "$file - auto-decrypt - file is already decrypted but the HMT flag is wrong." return } # Perform the decryption by requesting the file from the DLNA server. set size [$ts size] dsc $size runplugin predecrypt $ts set flagfile "$tmp/decrypting.$bfile" file touch $flagfile startclock log " DECRYPT: $rfile" 0 log " DLNA: $url" 0 exec wget -O "$tmp/$bfile" $url if {[file size $file] != [file size "$tmp/$bfile"]} { log " $file - File size mismatch." 0 file delete "$tmp/$bfile" file delete $flagfile return } # Check if the file is in use. It is possible that the file is # now being played even though it was free when decryption started. if {[$ts inuse]} { log " $file - In use." file delete "$tmp/$bfile" file delete $flagfile return } # Copy the HMT file over for stripts set thmt "$tmp/[file rootname $bfile].hmt" file copy "$rfile.hmt" $thmt # Check that the file is no longer encrypted by analysing it. set anencd [exec /mod/bin/stripts -qE "$tmp/[file rootname $bfile]"] file delete $thmt if {$anencd != "0"} { log " $file - File did not decrypt properly." 0 system notify "$file - auto-decrypt failed." file delete "$tmp/$bfile" file delete $flagfile return } # Move the encrypted file out of the way. file rename $file "$rfile.encrypted" # Move the decrypted copy into place. file rename "$tmp/$bfile" $file # Set the file time to match the old file file touch $file "$rfile.encrypted" # Patch the HMT - quickest way to get back to a playable file. exec /mod/bin/hmt -encrypted "$rfile.hmt" log " Removing/binning old copy." 0 # Move the old recording to the bin if undelete is installed. if {$dustbin ne ""} { set bin [_del_bindir $file "webif_autodecrypt"] set tail [file tail $rfile] file rename "$rfile.encrypted" "$bin/$tail.ts" foreach ext {nts hmt thm} { if {[file exists "$rfile.$ext"]} { file copy $rfile.$ext "$bin/$tail.$ext" if {$ext eq "hmt"} { # Patch the binned HMT back exec /mod/bin/hmt +encrypted \ "$bin/$tail.hmt" } } } } else { tdelete "$rfile.encrypted" } log "Done... [endclock $size]" 0 file delete $flagfile runplugin postdecrypt $ts } proc do_mpg {ts} { global tmp tsgroup set file [file rootname [$ts get file]] if {[file exists $file.mpg]} { # Already done. return } if {[$ts flag "ODEncrypted"]} { log " $file - Not decrypted." 2 return } if {[$ts get definition] eq "HD"} { # Cannot extract a useful MPG from a HD recording. return } if {[$ts inuse]} { log " $file - In use." return } runplugin prempg $ts dsc [$ts size] log " MPG: $file" 0 log " Converting..." 0 if {[catch { foreach line [split \ [exec nice -n 19 /mod/bin/ffmpeg -y -benchmark -v 0 \ -i $file.ts \ -map 0:0 -map 0:1 \ -vcodec copy -acodec copy $tmp/mpg.mpg] "\n"] { log $line 0 } } msg]} { log "Error during mpg extract: $msg" 0 system notify "$file - auto-mpg - error $msg." return } # Move the MPG into the local directory file rename $tmp/mpg.mpg $file.mpg runplugin postmpg $ts } proc do_mp3 {ts} { global tmp tsgroup set file [file rootname [$ts get file]] if {[file exists $file.mp3]} { # Already done. return } if {[$ts flag "ODEncrypted"]} { log " $file - Not decrypted." 2 return } if {[$ts get definition] eq "HD"} { # Cannot extract a useful MP3 from a HD recording. log " $file - High definition." 2 return } if {[$ts inuse]} { log " $file - In use." return } runplugin premp3 $ts dsc [$ts size] log " MP3: $file" 0 log " Converting... [$::settings audiomp3descr $::audiomp3]" 0 if {[catch { set cmd [list nice -n 19 \ /mod/bin/ffmpeg -y -benchmark -v 0 -i "$file.ts" \ -f mp3 -vn] if {!$::audiomp3} { lappend cmd -acodec copy } lappend cmd "$tmp/mp3.mp3" foreach line [split [exec {*}$cmd] "\n"] { log $line 0 } } msg]} { log "Error during mp3 extract: $msg" 0 system notify "$file - auto-mp3 - error $msg." return } if {[system pkginst id3v2]} { log [exec /mod/bin/id3v2 \ --song "[$ts get title]" \ --comment "[$ts get synopsis]" \ --album "[$ts get channel_name]" \ --year "[clock format [$ts get start] -format {%Y}]" \ "$tmp/mp3.mp3"] 0 } # Move the MP3 into the local directory file rename $tmp/mp3.mp3 $file.mp3 runplugin postmp3 $ts } proc entries {dir callback} { foreach entry [readdir -nocomplain $dir] { if {![string match {*.ts} $entry} continue if {[catch {set ts [ts fetch "$dir/$entry"]}]} continue if {$ts == 0} continue $callback $ts } } proc shrink {dir} { log "SHRINK: \[$dir]" 2 entries $dir do_shrink } proc decrypt {dir} { log "DECRYPT: \[$dir]" 2 if {$::dlnaok} { entries $dir do_decrypt } } proc mpg {dir} { log "MPG: \[$dir]" 2 entries $dir do_mpg } proc mp3 {dir} { log "MP3: \[$dir]" 2 entries $dir do_mp3 } proc expire {dir} { global ax_days log "EXPIRE: \[$dir]" 2 set ax_days [{dir expiry} $dir] entries $dir do_expire } proc scan {dir attr {force 0} {recurse 1}} {{indent 0}} { global dustbin incr indent 2 log "[string repeat " " $indent]\[$dir]" 2 if {$dir eq $dustbin} { log "Dustbin, skipping." 2 incr indent -2 return } if {[string match {\[*} [file tail $dir]]} { # Special folder file stat "$dir/" st if {$st(dev) != $::rootdev} { log "Special folder on different device, skipping." 2 incr indent -2 return } if {$force} { set force 0 log "Special folder, overriding recursion." 2 } } # Recursion if {!$force && [file exists "$dir/.auto${attr}r"]} { log "[string repeat " " $indent] (R)" 2 set force 1 } dsc if {$force || [file exists "$dir/.auto$attr"]} { $attr $dir } foreach entry [readdir -nocomplain $dir] { if {$recurse && [file isdirectory "$dir/$entry"]} { scan "$dir/$entry" $attr $force } } dorecalc $dir incr indent -2 } proc scanup {dir flag} { global root set rl [string length $root] while {[string length $dir] >= $rl} { if {[string match {\[*} [file tail $dir]]} { return -1 } if {[file exists "$dir/.auto${flag}r"]} { log "scanup: Found ${flag}r ($dir)" 2 return 1 } set dir [file dirname $dir] } return 0 } proc scansingle {dirs} { global modules root foreach dir $dirs { log "Scanning single directory '$dir'" foreach arg $modules { set st [clock milliseconds] set sup [scanup $dir $arg] if {$sup == -1} { log "Encountered special directory." break } scan $dir $arg $sup 0 log "$arg scan completed in [elapsed $st] seconds." } } } set root [system mediaroot] file stat "$root/" rootstat set rootdev $rootstat(dev) log "Root device: $rootdev" 2 if {[lindex $argv 0] eq "-single"} { scansingle [lrange $argv 1 end] } elseif {[llength $argv] > 0} { set loglevel 2 foreach arg $argv { scan $root $arg } } else { foreach arg $modules { set st [clock milliseconds] runplugin "pre${arg}scan" scan $root $arg runplugin "post${arg}scan" log "$arg scan completed in [elapsed $st] seconds." } } release_lock webif_auto log "Media scan completed in [elapsed $scanstart] seconds."