if {![exists -command class]} { package require oo } if {![exists -command pack]} { package require pack } if {![exists -command xconv]} { package require xconv } if {![exists -command binary]} { package require binary } source /mod/webif/lib/setup require system.class tvdb.class classdump set tsgroup {ts nts hmt thm} class ts { file "" base "" title "" synopsis "" definition "" channel_num 0 channel_name "" start 0 end 0 flags "" error "" guidance "" bookmarks 0 schedstart 0 scheddur 0 genre 0 resume 0 status "" series "" seriescached 1 seriesnum 0 episodenum 0 episodetot 0 episodename "" tvdb_method "" tvdb_series {} tvdb_data {} } ts method bfile {} { return [file tail [file rootname $file]] } ts method dir {} { return [file dirname $file] } ts method duration {{raw 0}} { set d [expr $end - $start] if {!$raw} { set d $($d / 60) } return $d } ts method size {} { return [file size $file] } ts method _parse {line} { set vars [split $line "\t"] lassign [split $line "\t"] \ title synopsis definition channel_num channel_name \ start end flags_list guidance bookmarks schedstart scheddur \ genre resume status seriesnum episodenum episodetot set synopsis [xconv $synopsis] set flags [split [string range $flags_list 0 end-1] ,] } ts method lastmod {} { return [file mtime "[file rootname $file].hmt"] } ts method inuse {} { return [system inuse $file] } ts method bookmarks {{aslist 0}} { set marks [split [string trim [exec /mod/bin/hmt -bookmarks $file]]] if {$aslist} { return $marks } return [join $marks " "] } ts method setbookmarks {marks} { exec /mod/bin/hmt +setbookmarks=[join $marks :] $file } ts method storeepisode {{data {}}} { if {[llength $data]} { set d [join $data ","] } else { set d "$seriesnum,$episodenum,$episodetot" } exec /mod/bin/hmt +setseries=$d $file } ts method clearepdata {} { set seriesnum 0 set episodenum 0 set episodetot 0 } ts method flag {f} { if {$f in $flags} {return 1} else {return 0} } ts method unflag {f} { lremove flags $f } ts method unlock {} { set cmd [list /mod/bin/hmt -lock $file] exec {*}$cmd lremove flags "Locked" return 1 } ts method lock {} { set cmd [list /mod/bin/hmt +lock $file] exec {*}$cmd ladd flags "Locked" return 1 } set ::ts::flagmap { detectads Addetection dedup Deduped encrypted ODEncrypted protect Encrypted } ts method setflag {flag {dummy 0}} { if {!$dummy} { set cmd [list /mod/bin/hmt +$flag $file] if {[catch {exec {*}$cmd}]} { throw 20 "Unknown flag." } } if {[dict exists $::ts::flagmap $flag]} { ladd flags $::ts::flagmap($flag) } else { ladd flags [string totitle $flag] } return 1 } ts method unsetflag {flag {dummy 0}} { if {!$dummy} { set cmd [list /mod/bin/hmt -$flag $file] if {[catch {exec {*}$cmd}]} { throw 20 "Unknown flag." } } if {[dict exists $::ts::flagmap $flag]} { lremove flags $::ts::flagmap($flag) } else { lremove flags [string totitle $flag] } return 1 } ts method set_shrunk {} { set cmd [list /mod/bin/hmt +shrunk $file] exec {*}$cmd ladd flags "Shrunk" return 1 } ts method set_deduped {} { set cmd [list /mod/bin/hmt +dedup $file] exec {*}$cmd ladd flags "Deduped" return 1 } ts method unset_deduped {} { set cmd [list /mod/bin/hmt -dedup $file] exec {*}$cmd lremove flags "Deduped" return 1 } ts method unenc {} { set cmd [list /mod/bin/hmt -protect $file] exec {*}$cmd lremove flags "Encrypted" return 1 } ts method enc {} { set cmd [list /mod/bin/hmt +protect $file] exec {*}$cmd ladd flags "Encrypted" return 1 } ts method set_new {} { set cmd [list /mod/bin/hmt +new $file] exec {*}$cmd ladd flags "New" return 1 } ts method set_watched {} { set cmd [list /mod/bin/hmt -new $file] exec {*}$cmd lremove flags "New" return 1 } ts method setfile {f} { set file $f } proc {ts parse} {file line} { set e [ts new] $e setfile $file $e _parse $line return $e } proc {ts exec} {file} { set raw 0 set cmd [list /mod/bin/hmt] lappend cmd "-p" lappend cmd $file #puts "CMD -$cmd-" return [exec {*}$cmd] } ts method fileset {} { global tsgroup set root [file rootname $file] set fset {} foreach ext $tsgroup { if {[file exists "$root.$ext"]} { lappend fset "$root.$ext" } } return $fset } proc {ts fetch_or_error} {file {checked 0}} { if {[catch {set ts [ts fetch $file $checked]}] || $ts == 0} { print "Could not load ts file $file" return 0 } return $ts } proc {ts fetch} {file {checked 0}} { # Check that this is a .ts file which has at least one sidecar # file (.hmt) if {!$checked} { if {[file extension $file] ne ".ts"} { return 0 } if {![file exists "[file rootname $file].hmt"]} { return 0 } } if {[file extension $file] ne ".ts"} { set file "[file rootname $file].ts" } return [ts parse $file [ts exec $file]] } ts method delete {} { foreach f [$self fileset] { file tdelete $f puts "Removed $f
" } return 1 } ts method move {dst {touch 0} {force 0}} { foreach f [$self fileset] { set nf "$dst/[file tail $f]" while {[file exists $nf]} { set nf "$dst/_[file tail $nf]" } file rename $f $nf if {$touch} { exec /mod/bin/busybox/touch $nf } } return 1 } ts method copy {dst} { foreach f [$self fileset] { file copy $f "$dst/[file tail $f]" } return 1 } ts method settitle {newtitle} { if {[string length $newtitle] > 48} { return } exec /mod/bin/hmt "+settitle=${newtitle}" $file set title $newtitle } ts method setsynopsis {newsynopsis} { if {[string length $newsynopsis] > 252} { return } exec /mod/bin/hmt "+setsynopsis=${newsynopsis}" $file set synopsis $newsynopsis } ts method setguidance {newguidance} { if {[string length $newguidance] > 74} { return } if {$newguidance eq ""} { exec /mod/bin/hmt "-guidance" $file } else { exec /mod/bin/hmt "+setguidance=${newguidance}" $file } set guidance $newguidance } proc {ts genre} {genre} { if {![string is integer $genre] || $genre < 0} { set genre 0 } if {$genre <= 15} { set genre $($genre << 4) } return $genre } ts method setgenre {newgenre} { set newgenre [ts genre $newgenre] exec /mod/bin/hmt "+setgenre=-${newgenre}" $file set genre $newgenre } ts method dlnaloc {{urlbase ""}} { return [system dlnaurl [file normalize $file] $urlbase] } ts method cleanbmp {} { set bfile [file rootname $file] foreach f [glob -nocomplain "${bfile}*.bmp"] { file delete $f } } ts method mkbmps {{offset 0}} { set bfile [file rootname $file] if {[catch { exec /mod/bin/ffmpeg -loglevel fatal -ss $offset -i $file \ -vf fps=fps=2 -frames 5 \ -pix_fmt argb -vf vflip -s 140x78 "${bfile}%d.bmp" } msg]} { puts "ERROR: $msg" return 0 } return 1 } ts method mkbmp {{offset 0} {ext ""}} { set bfile [file rootname $file] set bmpfile "$bfile$ext.bmp" set cmd [list /mod/bin/ffmpeg -loglevel fatal -ss $offset -i $file \ -frames 1 -pix_fmt argb -vf vflip -s 140x78 -y $bmpfile] catch { exec {*}$cmd } if {![catch { file stat $bmpfile stbmp }]} { if {[dict get $stbmp size] != 0} { return 1 } } return 0 } ts method mkthm {{offset 0}} { if {![$self mkbmp $offset]} { return 0 } set bfile [file rootname $file] # Trim the bitmap header from the start of the file if {[catch { exec /bin/dd if=$bfile.bmp of=$bfile.thm~ bs=54 skip=1 } msg]} { puts "ERROR: $msg" return 0 } exec /bin/echo -n " " >> $bfile.thm~ file rename -force $bfile.thm~ $bfile.thm file tdelete $bfile.bmp $self setflag thumbnail return 1 } # From MontysEvilTwin # - https://hummy.tv/forum/threads/7787/page-2#post-106826 # ffmpeg -i "File 1.ts" -c:a mp3 -b:a 128k "File 1.mp3" # ffmpeg -i "File 1.ts" -c:a copy "File 1.mp2" # ffmpeg -i "File 1.ts" -c:a copy "File 1.loas" ts method mkmp3 {{slow false} {tmp ""} {v 0} {br 128}} { set rfile [file rootname $file] if {$slow} { set opts [list -c:a mp3 -b:a ${br}k] set ext mp3 } else { set opts [list -c:a copy] if {$definition eq "HD"} { set ext loas } else { set ext mp2 } } set cmd [list /mod/bin/ffmpeg \ -y -benchmark -vn -v $v \ -i $file {*}$opts \ ] if {$tmp eq ""} { lappend cmd "${rfile}.$ext" } else { lappend cmd "$tmp.$ext" } set output [exec {*}$cmd] if {$tmp ne ""} { file rename "$tmp.$ext" "${rfile}.mp3" } elseif {$ext ne "mp3"} { file rename "${rfile}.$ext" "${rfile}.mp3" } exec /mod/bin/id3v2 \ --song $title \ --comment $synopsis \ --album $channel_name \ --year "[clock format $start -format {%Y}]" \ "${rfile}.mp3" return $output } ts method mkmpg {{tmp ""}} { set rfile [file rootname $file] set cmd [list /mod/bin/ffmpeg \ -y -benchmark -v 0 \ -i $file \ -map 0:0 -map 0:1 \ -vcodec copy -acodec copy] if {$tmp eq ""} { lappend cmd "${rfile}.mpg" } else { lappend cmd "$tmp.mpg" } set output [exec {*}$cmd] if {$tmp ne ""} { file rename "$tmp.mpg" "${rfile}.mpg" } return $output } proc {ts renamegroup} {from to} { global tsgroup set dir [file dirname $from] set root [file rootname $from] # Catch from string without a . character in it if {$root eq $from} { return } foreach ext $tsgroup { set f "$root.$ext" if {![file exists $f]} continue file rename $f "${dir}/${to}.${ext}" } exec /mod/bin/hmt "+setfilename=$to" "${dir}/${to}.hmt" # set ndir [file normalize $dir] # # if {![catch {set db [sqlite3.open $::dmsfile]}]} { # catch { # set x [lindex [$db query {select mediaid from tblMedia # where localUrl = '%s'} [file normalize $from]] 0] # lassign $x key mediaid # if {$mediaid ne ""} { # $db query {update tblMedia set localUrl = '%s' # where mediaid = %s} "${ndir}/{$to}.ts" $mediaid # $db query {update tblMedia set title = '%s' # where mediaid = %s} "{$to}.ts" $mediaid # } # } # $db close # } } proc {ts touchgroup} {target ref} { global tsgroup set dir [file dirname $target] set root [file rootname $target] # Catch from string without a . character in it if {$root eq $target} { return } foreach ext $tsgroup { set f "$root.$ext" if {![file exists $f]} continue file touch $f $ref } } proc {ts resetnew} {dir} { if {![file isdirectory $dir]} return if {![file exists "$dir/.series"]} { set fd [open "$dir/.series" "w"] puts -nonewline $fd [string repeat "\x0" 276] close $fd } set tot 0 set watched 0 foreach file [readdir -nocomplain $dir] { if {![string match {*.ts} $file]} { continue } incr tot if {[set ts [ts fetch "$dir/$file"]] != 0} { if {![$ts flag "New"]} { incr watched } } } if {!$tot} { file delete "$dir/.series" return } set fd [open "$dir/.series"] set bytes [read $fd] close $fd set recs [unpack $bytes -uintle 0 32] set played [unpack $bytes -uintle 32 32] #puts "Current: $played/$recs" #hexdump $bytes #puts "Calculated: $watched/$tot" pack bytes $tot -intle 32 0 pack bytes $watched -intle 32 32 #hexdump $bytes set fd [open "$dir/.series" "w"] puts -nonewline $fd $bytes close $fd } proc {ts iterate} {callback {verbose 0} {dir ""} {nospecial 0}} {{rootdev 0}} { require system.class if {$dir eq ""} { set dir [system mediaroot] file stat "$dir/" rootstat set rootdev $rootstat(dev) } if {$verbose} { puts "Scanning directory ($dir)" } if {$rootdev != 0} { file stat "$dir/" st if {$st(dev) != $rootdev} return } if {$nospecial && [system specialdir $dir]} return foreach entry [readdir -nocomplain $dir] { if {[file isdirectory "$dir/$entry"]} { ts iterate $callback $verbose "$dir/$entry" $nospecial continue } if {![string match {*.ts} $entry]} continue if {[catch {set ts [ts fetch "$dir/$entry"]}]} continue if {$ts == 0} continue $callback $ts } } # # Attempt to extract the series/episode names using a variety of techniques # ts method series_name {} { # For recorded series, use the folder name set dir [file dirname $file] if {[file exists "$dir/.series"]} { set s [file tail $dir] } else { set s $title } foreach x { {^new: *} } { regsub -nocase -all -- $x $s "" s } return $s } set ::ts::episode_prefixes { {^new series\.* *} {^cbeebies\.* *} {^cbbc\.* *} {^t4: *} {^brand new series *[-:]* *} {^\.+} { *\(Part [0-9] of [0-9]\) *} {, Part [0-9]} } ts method _tvdb_resolve {seriesid} { # See if we can find a TVDB series for this recording. set dir [file dirname $file] set tvdb_series [set v [tvdb series "" $seriesid]] if {[$v get seriesid] == 0} { return } # Got one. # Easiest case - we can explicitly request the episode. if {$seriesnum && $episodenum} { if {$seriescached} { set tvdb_method "cached values" } else { set tvdb_method "series and episode number" } return [$v episodebynum $seriesnum $episodenum] } # Now try to find the episode using the current episode name # (using series or episode number if available) set k [$v episodebyname $episodename $seriesnum $episodenum] if {[llength $k]} { set tvdb_method "episode name ($episodename)" return $k } # More problematic but can at least narrow the list of candidates # using the episode or series numbers if we have them. if {$episodenum} { set tvdb_method "episode number" return [$v episodebyepnum $episodenum $synopsis] } if {$seriesnum} { set tvdb_method "series and synopsis" return [$v episodebyseries $seriesnum $synopsis] } # Most difficult - try and match based on synopsis alone set tvdb_method "synopsis text" return [$v episodebysynopsis $synopsis] } proc {ts serieslist} {dir} { set idfile "$dir/.tvdbseriesid" if {![file exists $idfile]} { return {} } return [lmap i [split [file read $idfile] "\n"] { string trim $i }] } ts method extract_numbers {} { ###################################################################### # Check for embedded Series/Episode number. # Thank you broadcasters for the variation! # Least trustworthy first. # Episode 5 regexp -nocase -- {Episode (\d+)} $synopsis x episodenum # ^23/27. regexp -nocase -- {^\s*(\d+)/(\d+)} $synopsis x episodenum episodetot # (8/8) regexp -nocase -- {\((\d+)/(\d+)\)} $synopsis x episodenum episodetot # (Episode 5/10) # (Ep5/10) # (Ep 3 of 3) # (Ep3) regexp -nocase -- {Epi?s?o?d?e?\s*(\d+)\s*(of|/)?\s*(\d+)?} $synopsis \ x episodenum x episodetot # (S2 Ep1) # S.02 Ep.002 # S01 Ep52 # (S4 Ep 7) # (S1, ep 2) # (S8, Ep2) # (S4 Ep22/24) regexp -nocase -- {S\.*(\d+),?\s*Ep\.?\s*(\d+)(/(\d+))?} $synopsis \ x seriesnum episodenum x episodetot # (Part 5/10) # (Pt. 5/10) # (Part 5 of 10) # (Pt. 5 of 10) # (Pt5) regexp -nocase -- {P(art|t\.?)\s*(\d+)\s*(of|/)?\s*(\d+)?} $synopsis \ x x episodenum x episodetot foreach v {seriesnum episodenum episodetot} { if {[set $v] eq ""} { set $v 0 } else { incr $v 0 } } } ts method episode_name {} { set s $synopsis ###################################################################### # Attempt to determine the episode name from the synopsis # Strip common prefixes foreach prefix $::ts::episode_prefixes { regsub -nocase -all -- $prefix $s "" s } # Strip anything following a colon. regsub -all -- { *[:].*$} $s "" s # If the resulting string is longer than 40 characters then # split around . and take the left hand side if appropriate. if {[string length $s] > 40} { lassign [split $s "."] v w set s $v if {[string length $s] < 6 && [string length $w] < 6} { append s "_$w" } } # Shorten if too long. if {[string length $s] > 40} { set s [string range $s 0 39] } set episodename $s if {$episodenum == 0} { $self extract_numbers set seriescached 0 } # Now see if TVDB has anything to add set fbase "[file dirname $file]/.tvdb" if {![system has tvdb] || ![file exists "${fbase}seriesid"]} { return $s } if {!$seriesnum && [file exists "${fbase}series"]} { set seriesnum [string trim [file read "${fbase}series"]] } foreach seriesid [ts serieslist [$self dir]] { set tvdb_data [$self _tvdb_resolve $seriesid] if {![dict exists $tvdb_data name]} continue set flag 0 if {!$seriesnum} { set seriesnum $tvdb_data(series) incr flag } if {!$episodenum} { set episodenum $tvdb_data(episode) incr flag } if {$flag} { $self storeepisode } return $tvdb_data(name) } return $s } ts method epstr {{format "s%se%E/%n"}} { set map {} if {$seriesnum eq 0} { set map(%s) "?" set map(%S) "??" } else { set map(%s) $seriesnum set map(%S) [format "%02d" $seriesnum] } if {$episodenum eq 0} { set map(%e) "?" set map(%E) "??" } else { set map(%e) $episodenum set map(%E) [format "%02d" $episodenum] } if {$episodetot eq 0} { set map(%n) "?" set map(%N) "??" } else { set map(%n) $episodetot set map(%N) [format "%02d" $episodetot] } return [string map $map $format] } ts method tsr {} { set fd [open "[file rootname $file].nts"] set bytes [read $fd 0x20] close $fd set tsr [unpack $bytes -uintle $(8 * 0x1f) 8] return $tsr } ts method genrenib {} { if {[catch {set v $($genre >> 4)}]} { set v 0 } return $v } ts method genre_info {} { set g [$self genrenib] lassign $::epg::genrelist($g) txt img if {$img eq "Unclassified"} { set img "/images/173_3_26_G3_$img.png" } else { set img "/images/173_3_00_G3_$img.png" } return [list $txt $img] } proc {ts genrelist} {} { require epg.class set glist {} foreach {k v} $::epg::genrelist { lappend glist $($k << 4) $v } return $glist } # return the key that will decrypt the file in the mode, or nothing ts method getkey {mode} { # mode: dlna (active key), direct set rfile [file rootname $file] set keys {} # the active key set key [string range [system nugget cryptokey -key] 0 31] if {$key ne ""} { lappend keys $key } if { $mode ne "dlna" } { # also try other keys, such as this - same as active? set key [system customencryptionkey] if {$key ne ""} { ladd keys $key } # the native key if {![catch {set key [system encryptionkey]}]} { ladd keys $key } # support a file listing other keys in hex, one-per-line # eg, for recordings imported from a broken box try { set fd [open "/mod/etc/keys" r] foreach key [split [$fd read -nonewline] "\n"] { ladd keys $key } } on error {} { } finally { catch {$fd close} } } foreach key $keys { if {[catch { set ret [exec /mod/bin/stripts -q/ $key $rfile] }]} continue if {$ret eq "1"} { return $key } } return }