# Backup and restore module by Andrew Whewell, http://www.boygenius.co.uk/tivo/
# with portions by http://www.tivocommunity.com user 'angra'
#
# Copyright (c) 2003-2004 Andrew Whewell, portions copyright (c) 2002-2003 angra
# Released under the GPL - see http://www.fsf.org/copyleft/gpl.txt
#
# USE AT YOUR OWN RISK! This program makes direct writes to the database
# on your TiVo. It can stop your TiVo from working. No suing me if
# it breaks anything. It is released entirely WITHOUT WARRANTY.
#
# I can be reached by private mail on www.tivocommunity.com, user 'agw'
#
# History (ddmmyy):
set global_backup_version "1.00.0017"
# 1.00.0017 090805 agw If there are no wishlists then the Theme entry isn't
# in MFS - I was assuming it was always there
# agw BTUX9 was starting Tivoweb at the UI menu and jumping
# straight to the backup menu. The backup menu doesn't
# mention what it is about to backup. Made the wording
# clearer for people who get into the module this way.
# 1.00.0016 010605 agw Ignore errors with events - latest US Tivo's don't
# support events
# agw Remove TuningPreferences from season passes before
# restoring them... they are keys to other records which
# are not stored in the backup file and don't exist on
# UK Tivo's. They're optional on Tivo's that support them
# 1.00.0015 311204 agw Fixed problem whereby the Series wasn't stored under
# Server by ServerID, but instead had extra characters
# appended (in this case, :4:0:0).
# agw The trace shown during restore used to have a preamble
# that implied it would be removed in a later version of
# the module. The trace is quite handy, it is now a
# permanent feature. The text has been changed to reflect
# this.
# 1.00.0014 091104 agw Changed all lsort -int(eger) on channel numbers to
# -real so that the new US HD OTA channel decimal format
# doesn't break the module. As per pbolya's suggestion on
# www.tivocommunity.com/tivo-vb/showthread.php?postid=2350373#post2350373
# agw Replaced 'Australian' with 'non-US/UK' in the warning
# about it not being a standard machine. Some Canadians
# were not impressed ;-)
# 1.00.0013 180404 agw Fixed problem with field list occasionally containing
# the number 0x40019, causing crash in snapshot
# 1.00.0012 070304 agw Added experimental support for Australian TiVo's
# agw Added version number of module to backup file for diagnostics
# 1.00.0011 180104 agw Bug in channel remap could cause crash - fixed
# agw Allow any channel to be remapped, not just missing ones
# (allows people to work around Tribune assigning same
# TMS ID to different channels - unlikely but not much
# work involved so did it anyway)
# 1.00.0010 291203 agw Fixed problem with the browser when TiVoWeb is working
# with relative rather than absolute directories
# Was accidentally writing "Files:
" to the console
# when displaying a directory in the browser
# 1.00.0009 281203 agw Implemented suggestion from robert@biro.net to have
# a link to the backup file so it can be downloaded
# through the browser.
# Added an upload facility to copy backup files onto
# the TiVo.
# Added a rudimentary directory and file browser
# 1.00.0008 140603 agw Fixed the problem with a TCL error after creating the
# backups directory
# 1.00.0007 080603 agw Put up a 'please wait' when taking backup snapshot
# Detect & discard SPs for wishlists that don't exist
# Make calls to tms_to_fsid cope with bad failure
# 1.00.0006 030403 agw When trimming stations during backup make very sure
# that all entries for a trimmed fsid are removed
# 1.00.0005 020403 agw When taking snapshot for backup only store the channels
# used by the season passes and add any that are used but
# not in the channel list
# agw Cope with backups made prior to this version where the
# station list stored in the backup may be incomplete
# 1.00.0004 280303 agw Remove the store checks entirely - not reliable enough
# 1.00.0003 240303 agw Remove the store checks when taking snapshots
# 1.00.0002 230303 agw Reduce store requirements
# agw Put in code to stop BEFORE depleting store
# 1.00.0001 160303 agw First release to beta
# 1.00.0000 010303 agw Initial development
# Module-level statics
set global_backup_default_dir "$source_dir/backups"
set global_backup_layout_version "1"
set global_backup_magic_number "# agw TiVo Web backup file"
set global_backup_nowrite "0"
set global_temporary_series_fname "temporary_series.tcl"
set global_trace ""
set global_root_token "&ROOT&"
# Module level statics that restore loads into
set restore_backup_date ""
set restore_fname ""
set restore_layout_version ""
set restore_load_version ""
set restore_version3 ""
set restore_cbl1 0
set restore_cby1 0
set restore_ignored_sp_badthm 0
# Statics that are used when coping with the missing TMS ID in Australian
# faked Series records. If the TMS ID is missing from the series loaded from
# backup then ServerID is used instead.
set restore_is_aus 0
set restore_series_unique_id TmsId
# Timings
set timing_load_backup ""
set timing_automap_stations ""
set timing_automap_themes ""
set timing_automap_series ""
set timing_automap_sps ""
# This procedure will release all of the store allocated by this module. Just
# in case anyone wants it. Might add it to the menu options if required.
# A 'full' wipe will remove arrays and not set them back up again.
proc backup_zap_store {full} {
reset_load_slots $full
reset_snapshot $full
}
proc action_backup {chan path env} {
# Make sure we're starting on a clean slate (and give a way to release
# resource lost in previous abandoned runs)
backup_zap_store 1
# Reset the path if required
if {$path == "" } {
set path "/"
}
# If the path is not empty then pass it to the path interpretter,
# otherwise offer up the standard backup menu
if {$path != "/" } {
process_root_path $chan $path 0
} else {
puts $chan [html_start "Backup and Restore"]
puts $chan [html_table_start "" "Backup and Restore
Season Passes & Wishlists" "COLSPAN=2"]
puts $chan [tr "" [td [html_link "/backup_create" "Create backup file"]]]
puts $chan [tr "" [td [html_link "/backup_restore" "Restore from backup file"]]]
puts $chan [tr "" [td [html_link "/backup_upload" "Upload backup file to TiVo"]]]
puts $chan [tr "" [td [html_link "/backup_browse" "Browse backup files"]]]
puts $chan [html_table_end]
puts $chan "
backup.itcl module version"
puts $chan [html_link "http://www.boygenius.co.uk/tivo/" $::global_backup_version]
show_store_usage_text $chan
puts $chan ""
puts $chan [html_end]
}
}
proc action_backup_disp_formatted {chan path env} {
# Display the file contents formatted one line at a time
process_root_path $chan $path 1
}
proc action_backup_create {chan path env} {
set default_savedir [get_default_backup_dir]
set default_filename "$default_savedir/settings"
puts $chan [html_start "Create Backup"]
puts $chan [html_form_start "POST" "/backup_create_write"]
puts $chan [html_table_start "" "" ""]
puts $chan [tr "" [td "Create Backup File"] [td [html_form_text 1 40 "fname" "$default_filename"]]]
puts $chan [html_table_end]
puts $chan [html_form_input "submit" "submit" "Create"]
puts $chan [html_form_end]
show_link_backup_menu $chan
puts $chan [html_end]
}
proc action_backup_restore {chan path env} {
set default_savedir [get_default_backup_dir]
set default_filename "$default_savedir/settings"
puts $chan [html_start "Restore from Backup"]
puts $chan [html_form_start "POST" "/backup_restore_read"]
puts $chan [html_table_start "" "" ""]
puts $chan [tr "" [td "Load Backup File"] [td [html_form_text 1 40 "fname" "$default_filename"]]]
puts $chan [html_table_end]
puts $chan [html_form_input "submit" "submit" "Load"]
puts $chan [html_form_end]
puts $chan ""
puts $chan "Clicking 'load' just reads the backup file into store - it does"
puts $chan "not change any data.
"
puts $chan "It can take MINUTES to load the backup file into store,"
puts $chan "especially if it contains a lot of season passes.
During this"
puts $chan "time you will be presented with a blank screen.
Don't panic!
"
puts $chan "If you manually remove or add season passes or wishlists after the"
puts $chan "load then you should reload the backup file before restoring."
puts $chan "
"
show_link_backup_menu $chan
puts $chan [html_end]
}
proc action_backup_upload {chan path env} {
set default_savedir [get_default_backup_dir]
puts $chan [html_start "Upload Backup File to TiVO"]
puts $chan [html_form_start "POST" "/backup_upload_perform" "enctype='multipart/form-data'"]
puts $chan [html_table_start "" "" ""]
# It is important that the uploaded file comes last. If it's first then
# the other items in the form get lost
puts $chan [tr "" [td "Destination Directory (on the TiVo)"] [td [html_form_text 1 40 "destdir" "$default_savedir"]]]
puts $chan [tr "" [td "Backup File (on your computer)"] [td [html_form_input "file" "file" ""]]]
puts $chan [html_table_end]
puts $chan [html_form_input "submit" "submit" "Upload"]
puts $chan [html_form_end]
puts $chan "
"
show_link_backup_menu $chan
puts $chan [html_end]
}
proc action_backup_browse {chan path env} {
set default_savedir [get_default_backup_dir]
puts $chan [html_start "Browse Backup Directory"]
puts $chan [html_form_start "POST" "/backup_browse_perform"]
puts $chan [html_table_start "" "" ""]
puts $chan [tr "" [td "Browse Directory"] [td [html_form_text 1 40 "browsedir" "$default_savedir"]]]
puts $chan [html_table_end]
puts $chan [html_form_input "submit" "submit" "Browse"]
puts $chan [html_form_end]
puts $chan "
"
show_link_backup_menu $chan
puts $chan [html_end]
}
proc action_backup_browse_perform {chan path env} {
global source_dir
eval $env
browse_directory $chan $browsedir
}
proc action_backup_upload_perform {chan path env} {
global source_dir
eval $env
puts $chan [html_start "Perform Upload"]
# Test that the source file is good and it uploaded OK
set file_is_good 0
if {$file == ""} {
puts $chan "The name of the backup file was not supplied"
} else {
set full_source "$source_dir/uploads/$file"
if {[file exists $full_source] == 0} {
puts $chan "'$file' could not be uploaded to '$source_dir/uploads'"
} elseif {[file isdirectory $full_source]} {
puts $chan "'$full_source' exists, but it is a directory"
} elseif {[file readable $full_source] == 0} {
puts $chan "'$full_source' exists but it cannot be read"
} elseif {[file size $full_source] == 0} {
puts $chan "'$file' either did not exist on your computer, or it was an empty file"
file delete $full_source
} else {
set file_is_good 1
}
}
# If the file is good then continue
if {$file_is_good} {
# Normalise the destination directory name - remove whitespace and trailing
# slashes
set destdir [string trim $destdir]
set destdir [string trimright $destdir "/"]
# Check to make sure the destination directory exists
set destdir "$destdir/"
if {[file exists $destdir] == 0} {
puts $chan "The destination directory '$destdir' does not exist"
} elseif {[file isdirectory $destdir] == 0} {
puts $chan "'$destdir' is not a directory"
} elseif {[file writable $destdir] == 0} {
puts $chan "'$destdir' is a read-only directory"
} else {
# Work out the full destination filename
set full_dest "$destdir$file"
# If the destination is a directory then stop
if {[file isdirectory $full_dest]} {
puts $chan "'full_dest' exists and is a directory - cannot overwrite it"
} else {
# If the file exists then take a backup
if {[file exists $full_dest]} {
set backup_previous "$full_dest.old"
puts $chan "Copying previous version of $file to $backup_previous
"
file copy -force $full_dest $backup_previous
}
# Now copy the uploaded file in
puts $chan "Copying uploaded file to $full_dest
"
file copy -force $full_source $full_dest
# Now remove the uploaded file
puts $chan "Removing file from temporary upload directory
"
file delete $full_source
# Finished - let them know we're done
puts $chan "
Backup file uploaded to $full_dest
"
}
}
}
# Finish off the page
puts $chan "
"
show_link_backup_menu $chan
puts $chan [html_end]
}
proc action_backup_create_write {chan path env} {
puts $chan [html_start "Create Backup"]
set submit ""
eval $env
if {$fname == ""} {
puts $chan "No filename supplied"
} else {
if {[validate_file $chan $fname 1]} {
create_backup $chan $fname
}
}
puts $chan [html_end]
}
proc action_backup_restore_read {chan path env} {
puts $chan [html_start "Loading Backup"]
set submit ""
eval $env
if {$fname == ""} {
puts $chan "No filename supplied"
} else {
if {[validate_file $chan $fname 0]} {
if {[validate_is_backup_file $chan $fname]} {
if {[load_backup $chan $fname]} {
set ::restore_fname $fname
present_backup $chan
}
}
}
}
puts $chan [html_end]
}
proc action_backup_present_restore {chan path env} {
puts $chan [html_start "Summary of Backup File Content"]
present_backup $chan
puts $chan [html_end]
}
proc action_backup_show_station {chan path env} {
global restore_station
global restore_station_by_num
global snapshot_station
puts $chan [html_start "Channels Used by Season Passes in Backup File"]
show_link_backup_summary $chan
puts $chan "
"
puts $chan [html_table_start "" "Channels Used by Season Passes" "COLSPAN=4"]
puts $chan [tr "ALIGN=CENTER" [th "Channel"] [th "Callsign"] [th "Name"] [th "Mapped To"]]
foreach num [lsort -real [array names restore_station_by_num]] {
set fsid $restore_station_by_num($num)
set data $restore_station($fsid)
set callsign [agextract $data CallSign]
set name [agextract $data Name]
set map_fsid [agextract $data agwMap]
set map_callsign "No longer in channel lineup!"
if {$map_fsid != ""} {
set map_callsign [agextract $snapshot_station($map_fsid) CallSign]
}
# Commented out test below for version 11 - if you put this test back then
# only stations that are not on the new TiVo will be offered up for remap
# if {[agextract $data agwWasMissing] == 1} {
set map_callsign [html_link "/backup_remap_station/$fsid" $map_callsign]
# }
puts $chan [tr "" [td $num] [td $callsign] [td $name] [td $map_callsign]]
}
puts $chan [html_table_end]
puts $chan "
"
puts $chan "Clicking the link under 'Mapped To' will allow you to map all"
puts $chan "season passes for that channel to any channel on your TiVo."
puts $chan "If you remap an existing channel (i.e. one that has the same"
puts $chan "Tribune Media Services identifier as the channel on the original"
puts $chan "TiVo) then all season passes for"
puts $chan "that channel will be restored to the mapped channel.
"
puts $chan "
This will NOT affect season passes that are already on"
puts $chan "your TiVo.
"
puts $chan "Refer to the season pass"
puts $chan [html_link "/backup_show_sp" "summary"]
puts $chan "before restoring to see what"
puts $chan "affect your changes here will have on the restore.
"
show_link_backup_summary $chan
puts $chan [html_end]
}
proc action_backup_remap_station {chan path env} {
puts $chan [html_start "Remap Missing Channel"]
set fsid ""
if {[string index $path 0] == "/"} {
set fsid [string range $path 1 end]
}
if {$fsid == ""} {
puts $chan "Internal error - no fsid was supplied to the channel remapper"
} else {
global restore_station
set restore_data $restore_station($fsid)
set current_map [agextract $restore_data agwMap]
global snapshot_station
global snapshot_station_by_num
set snapstat_vals "{}"
set snapstat_labs "{Unmapped}"
set snapstat_def ""
foreach snap_num [lsort -real [array names snapshot_station_by_num]] {
set snap_fsid $snapshot_station_by_num($snap_num)
set snap_data $snapshot_station($snap_fsid)
set snap_name [string trim [agextract $snap_data Name] "{}"]
set snaptext "$snap_num - $snap_name"
lappend snapstat_vals $snap_fsid
lappend snapstat_labs "$snap_num - $snap_name"
if {$snap_fsid == $current_map} {
set snapstat_def $snap_fsid
}
}
set original_channel [agextract $restore_data agwChannelNum]
set original_callsign [agextract $restore_data CallSign]
set original_name [agextract $restore_data Name]
puts $chan [h1 "Remap Channel"]
puts $chan [html_form_start "POST" "/backup_perform_remap_channel"]
puts $chan [html_form_hidden "fsid" $fsid]
puts $chan [html_table_start "" "" ""]
puts $chan [tr "" [td "Channel number"] [td $original_channel]]
puts $chan [tr "" [td "Callsign"] [td $original_callsign]]
puts $chan [tr "" [td "Name"] [td $original_name]]
puts $chan [tr "" [td "Map to channel"] [td [html_form_select "map_fsid" $snapstat_vals $snapstat_labs $snapstat_def]]]
puts $chan [html_table_end]
puts $chan [html_form_input "submit" "submit" "Map"]
puts $chan "
" show_link_backup_summary $chan puts $chan "
"
if {$current_map != ""} {
puts $chan "Mapping this channel will force all season passes not already"
puts $chan "on this channel to be restored to the new channel.
"
puts $chan "It will"
puts $chan "not affect season passes already on the TiVo and it will"
puts $chan "not make ANY changes to the channel database on your TiVo."
} else {
puts $chan "This was in the channel lineup at the time the backup was made."
puts $chan "However it cannot be found in the current channel lineup. Any"
puts $chan "season passes on this channel will not be restored until you"
puts $chan "map this defunct channel onto a good one. There are no ill"
puts $chan "effects if you don't do this - all that will happen is that any"
puts $chan "season pass on this channel will not be restored."
}
}
puts $chan [html_end]
}
proc action_backup_perform_remap_channel {chan path env} {
puts $chan [html_start "Remap Channel"]
eval $env
if {$fsid == ""} {
puts $chan "No fsid supplied"
} else {
global restore_station
set restore_data $restore_station($fsid)
agwrite restore_data agwMap fsid $map_fsid
set restore_station($fsid) $restore_data
set new_TmsId ""
if {$map_fsid == ""} {
puts $chan "The channel is now unmapped - while in this state you"
puts $chan "cannot restore season passes to it"
} else {
global snapshot_station
set snapshot_data $snapshot_station($map_fsid)
set original_number [agextract $restore_data agwChannelNum]
set new_number [agextract $snapshot_data agwChannelNum]
set original_name [string trim [agextract $restore_data Name] "{}"]
set new_name [string trim [agextract $snapshot_data Name] "{}"]
set new_TmsId [agextract $snapshot_data TmsId]
puts $chan "Mapped channel $new_number ($new_name) onto"
puts $chan "channel $original_number ($original_name)"
}
# Loop through the season passes and turn on/off any passes for this
puts $chan "
Please wait while the season passes are updated..."
puts $chan "this may take a while
"
# Take a snapshot of the current season passes - the find_matching_sp
# function will use this, and will remove entries from it as it sees fit
take_snapshot_sp 0 0
# Loop through the restore season pass list and check them off...
global restore_sp
foreach spfsid [array names restore_sp] {
set spdata $restore_sp($spfsid)
set spmap [agextract $spdata agwMap]
if {$spmap == ""} {
set station [agextract $spdata Station]
if {$station == $fsid} {
# Set the restore value to 0 if this season pass is already on
# this station... also this is quite expensive so save the value
set agwReMap ""
if {$map_fsid != ""} {
set agwReMap [find_matching_sp $spdata $new_TmsId]
}
agwrite spdata agwReMap fsid $agwReMap
# Establish the restore value
set restore_value 1
if {$map_fsid == "" || $agwReMap != ""} {
set restore_value 0
}
# Update the restore value for this station
agwrite spdata agwRestore bool $restore_value
set restore_sp($spfsid) $spdata
}
}
}
wipe_snapshot_sp_arrays 1 0
puts $chan "Season passes updated
"
}
puts $chan "
" show_link_station_summary $chan puts $chan [html_end] } proc action_backup_show_theme {chan path env} { global restore_theme global restore_theme_by_name puts $chan [html_start "Wishlists from Backup File"] set count_restorable [get_missing_theme_count] write_fsid_toggle_script $chan $count_restorable show_link_backup_summary $chan if {$count_restorable != 0} { puts $chan [html_form_start "POST" "/backup_toggle_theme" "name=\"form\""] } puts $chan [html_table_start "" "Wishlists from Backup" "COLSPAN=4"] puts $chan [tr "ALIGN=CENTER" [th "Name"] [th "Type"] [th "Status"] [th [write_fsid_toggle_checkall "Toggle Restore" $count_restorable]]] set type_labels [wishlist_type_labels] foreach name [lsort [array names restore_theme_by_name]] { set fsid $restore_theme_by_name($name) set data $restore_theme($fsid) set type_index [expr [defaultval 1 [agextract $data ThemeType]] - 1] set type_text [lindex $type_labels $type_index] set theme_status "Already Exists" set do_restore [td ""] if {[agextract $data agwMap] == ""} { set theme_status "Restorable" set default_restore "" if {[agextract $data agwRestore] != 0} { set default_restore "1" } set do_restore [td "ALIGN=CENTER" [html_form_checkbox "fsid_$fsid" $default_restore]] } puts $chan [tr "" [td $name] [td $type_text] [td $theme_status] $do_restore] } puts $chan [html_table_end] if {$count_restorable != 0} { puts $chan [html_form_input "submit" "submit" "Save"] puts $chan [html_form_end] puts $chan "
" puts $chan "This screen allows you to choose which wishlists to restore - it" puts $chan "does not actually restore anything" puts $chan "
" } show_link_backup_summary $chan puts $chan [html_end] } proc action_backup_toggle_theme {chan path env} { puts $chan [html_start "Toggle Wishlist Restores"] eval $env # Get a list of all of the wishlists that are ticked... set fsidlist "" foreach raw_fsid [info locals "fsid_*"] { lappend fsidlist [lindex [split $raw_fsid "_"] 1] } # ... and apply those to the wishlists we've got in store global restore_theme global restore_theme_by_name puts $chan [html_table_start "" "Restorable Wishlists" "COLSPAN=3"] puts $chan [tr "ALIGN=CENTER" [th "Name"] [th "Type"] [th "Restore"]] set type_labels [wishlist_type_labels] foreach name [lsort [array names restore_theme_by_name]] { set fsid $restore_theme_by_name($name) set data $restore_theme($fsid) if {[agextract $data agwMap] == ""} { set restorable 0 if {[lsearch $fsidlist $fsid] != -1} { set restorable 1 } agwrite data agwRestore bool $restorable set restore_theme($fsid) $data set type_index [expr [defaultval 1 [agextract $data ThemeType]] - 1] set type_text [lindex $type_labels $type_index] set will_restore No if {$restorable != 0} { set will_restore Yes } puts $chan [tr "" [td $name] [td $type_text] [td $will_restore]] } } puts $chan [html_table_end] show_link_backup_summary $chan puts $chan [html_end] } proc action_backup_show_series {chan path env} { global restore_series global snapshot_series global temporary_series_by_tmsid set orphans [get_orphaned_series] puts $chan [html_start "Series from Backup File"] # We don't build a sorted list of fsid's for the series because, aside from # here, we never really need it. So we'll just nip through and build one up # in-situ set series_by_name(0) "" reset_load_array series_by_name 0 foreach fsid [array names restore_series] { set data $restore_series($fsid) set title [string trim [agextract $data Title] "{}"] set series_by_name($title) $fsid } puts $chan "This is for reference purposes only. If a series is required by" puts $chan "a season pass and it isn't already on the TiVo then it will be" puts $chan "added back in." puts $chan "
" show_link_backup_summary $chan puts $chan "
"
puts $chan [html_table_start "" "Series from Backup" "COLSPAN=4"]
puts $chan [tr "ALIGN=CENTER" [th "Title"] [th "$::restore_series_unique_id"] [th "IndexPath"] [th "Status"]]
set deep_search_reqd 0
foreach name [lsort [array names series_by_name]] {
set fsid $series_by_name($name)
set data $restore_series($fsid)
set title $name
set tmsid [agextract $data $::restore_series_unique_id]
set indexpath ""
set status ""
set agwMap [agextract $data agwMap]
if {$agwMap == "" || $agwMap == "0"} {
set status "Not present, will add if used"
} else {
set real $snapshot_series($agwMap)
set realname [string trim [agextract $real Title] "{}"]
set indexpath [agextract $real IndexPath]
set status "Already on TiVo as \"$realname\""
if {[lsearch -exact $orphans $fsid] != -1} {
set deep_search_reqd 1
set status "Orphaned series - deep search required!"
}
if {[lsearch -exact [array names temporary_series_by_tmsid] $tmsid] != -1} {
set status "$status
(added by restore, not yet indexed)"
}
}
puts $chan [tr "" [td $title] [td $tmsid] [td $indexpath] [td $status]]
}
puts $chan [html_table_end]
if {$deep_search_reqd} {
puts $chan "The program guide data appears to be quite new on this TiVo."
puts $chan "Before season passes can be restored to some of these series"
puts $chan "the program will have to perform a deep search of the database"
puts $chan "to get the correct series mappings.
"
puts $chan "
"
puts $chan "This can take over half an hour!
"
puts $chan "If you want to run the deep search then click the 'Search' button
"
puts $chan [html_form_start "POST" "/backup_deep_search_series"]
puts $chan [html_form_input "submit" "submit" "Search"]
puts $chan [html_form_end]
}
show_link_backup_summary $chan
puts $chan [html_end]
reset_load_array series_by_name 1
}
proc action_backup_deep_search_series {chan path env} {
puts $chan [html_start "Deep Search for Series Identifiers"]
eval $env
puts $chan "TiVo Web has determined that some series on the TiVo"
puts $chan "are orphaned. This can happen when the program data guide is"
puts $chan "only a few days old. Tivo Web will now have to look for the"
puts $chan "correct identifiers the hard way. At present, this means doing"
puts $chan "a deep search for the values it needs.
"
puts $chan "
"
puts $chan "This will take a LONG time. Occasionally you will see"
puts $chan "a message pop up. After about 30 or 40 minutes(!) you will"
puts $chan "get a prompt right at the bottom of the list (scroll down)"
puts $chan "back to the series summary. When you can see that then"
puts $chan "the search has completed.
"
puts $chan "
"
puts $chan "Moving away from this page will interrupt the search.
"
puts $chan "
"
# Ok - get the list of orphans and build up a corresponding list of ID's
global restore_series
global snapshot_series
set orphan_fsid [get_orphaned_series]
set orphan_tmsid ""
set orphan_count [llength $orphan_fsid]
foreach fsid $orphan_fsid {
set series $restore_series($fsid)
lappend orphan_tmsid [agextract $series $::restore_series_unique_id]
}
if {$orphan_count == 0} {
puts $chan "No orphans to search for!"
} else {
# I hate this. But it'll do as a first step.
# Loop through /Server... this is going to take a while!
puts $chan "Looking for $orphan_count series...
"
global db
set count 0
ForeachMfsFileTrans fsid name type "/Server" "" 20 {
# Show a pulse
incr count
if {[expr $count % 500] == 0} {
puts $chan "Examined $count records
"
}
# Read the object and check to see if it's a series
if {[catch {set object [db $db openid $fsid]}]} {
puts $chan "Warning - could not open object $fsid
"
} else {
set objtype [dbobj $object type]
if {$objtype == "Series"} {
# Right tmsid?
set tmsid [dbobj $object get $::restore_series_unique_id]
set orphan [lsearch -exact $orphan_tmsid $tmsid]
if {$orphan != -1} {
# Blimey! Found one... yay... record the real fsid and get it out
# of the lists. Stop searching if it's the last orphan
set restore_fsid [lindex $orphan_fsid $orphan]
set series $restore_series($restore_fsid)
agwrite series agwMap fsid $fsid
set restore_series($restore_fsid) $series
set title [string trim [agextract $series Title] "{}"]
puts $chan "Found the correct FSID for series $title
"
set series_data [db $db openid $fsid]
set series_fields [dbobj $series_data attrs]
set content [construct_record_content $series_data $series_fields]
snapshot_add_series $fsid $content 0
set orphan_fsid [lreplace $orphan_fsid $orphan $orphan]
set orphan_tmsid [lreplace $orphan_tmsid $orphan $orphan]
incr orphan_count -1
puts $chan "$orphan_count more series to find...
"
if {$orphan_count == 0} {
break
}
}
}
}
}
}
if {$orphan_count == 0} {
puts $chan "All done!
"
} else {
puts $chan "Couldn't find the identifiers for $orphan_count series!
"
puts $chan "It will not be possible to restore the season passes for"
puts $chan "these series...
"
}
show_link_series_summary $chan
puts $chan [html_end]
}
proc action_backup_show_sp {chan path env} {
global restore_sp
global restore_sp_by_priority
global restore_station
puts $chan [html_start "Season Passes from Backup File"]
set count_restorable [get_missing_sp_count]
write_fsid_toggle_script $chan $count_restorable
show_link_backup_summary $chan
if {$count_restorable != 0} {
puts $chan [html_form_start "POST" "/backup_toggle_sp" "name=\"form\""]
}
puts $chan [html_table_start "" "Season Passes from Backup" "COLSPAN=6"]
puts $chan [tr "ALIGN=CENTER" [th "Priority"] [th "Type"] [th "Name"] [th "Channel"] [th "Status"] [th [write_fsid_toggle_checkall "Toggle Restore" $count_restorable]]]
foreach priority [lsort -dictionary [array names restore_sp_by_priority]] {
set fsid $restore_sp_by_priority($priority)
set data $restore_sp($fsid)
set type [agextract $data agwSPType]
set name [get_sp_name $data 1]
set callsign [get_sp_callsign $data 1]
set status [get_sp_status $data 1]
set do_restore [td ""]
if {[lindex $status 0] == "Restorable"} {
set default_restore ""
if {[agextract $data agwRestore] != 0} {
set default_restore "1"
}
set do_restore [td "ALIGN=CENTER" [html_form_checkbox "fsid_$fsid" $default_restore]]
}
puts $chan [tr "" [td [expr $priority + 1]] [td $type] [td $name] [td $callsign] [td $status] $do_restore]
}
puts $chan [html_table_end]
if {$count_restorable != 0} {
puts $chan [html_form_input "submit" "submit" "Save"]
puts $chan [html_form_end]
puts $chan "
" puts $chan "This screen allows you to choose which season passes to restore" puts $chan "- it does not actually restore anything" puts $chan "
"
}
show_link_backup_summary $chan
puts $chan [html_end]
}
proc action_backup_toggle_sp {chan path env} {
puts $chan [html_start "Toggle Season Pass Restores"]
eval $env
# Get a list of all of the season passes that are ticked...
set fsidlist ""
foreach raw_fsid [info locals "fsid_*"] {
lappend fsidlist [lindex [split $raw_fsid "_"] 1]
}
# ... and apply those to the season passes we've got in store
global restore_sp
global restore_sp_by_priority
puts $chan [html_table_start "" "Restorable Season Passes" "COLSPAN=5"]
puts $chan [tr "ALIGN=CENTER" [th "Priority"] [th "Type"] [th "Name"] [th "Channel"] [th "Restore"]]
foreach priority [lsort -dictionary [array names restore_sp_by_priority]] {
set fsid $restore_sp_by_priority($priority)
set data $restore_sp($fsid)
if {[agextract $data agwMap] == ""} {
set restorable 0
if {[lsearch $fsidlist $fsid] != -1} {
set restorable 1
}
agwrite data agwRestore bool $restorable
set restore_sp($fsid) $data
set type [agextract $data agwSPType]
set name [get_sp_name $data 1]
set callsign [get_sp_callsign $data 1]
set will_restore No
if {$restorable != 0} {
set will_restore Yes
}
puts $chan [tr "" [td $priority] [td $type] [td $name] [td $callsign] [td $will_restore]]
}
}
puts $chan [html_table_end]
show_link_backup_summary $chan
puts $chan [html_end]
}
proc action_backup_perform_restore {chan path env} {
puts $chan [html_start "Restore"]
set global_trace $chan
puts $chan "During the restore the module will dump"
puts $chan "a trace of what it is doing to screen. If there is anything to"
puts $chan "restore then you will see the trace scrolling off underneath"
puts $chan "this text as you're reading it."
puts $chan "
"
puts $chan "Scroll down through the trace until you see the link back to the"
puts $chan "summary page. If you see an error please make a copy of the text"
puts $chan "of the error message and report the problem."
puts $chan "
"
perform_restore_themes $chan
perform_restore_sps $chan
puts $chan "
"
show_link_backup_summary $chan
puts $chan [html_end]
}
proc show_link_backup_summary {chan} {
puts $chan [html_link "/backup_present_restore" "Return to Summary"]
}
proc show_link_backup_menu {chan} {
puts $chan [html_link "/backup" "Return to Backup Menu"]
}
proc show_link_series_summary {chan} {
puts $chan [html_link "/backup_show_series" "Return to the series summary"]
}
proc show_link_station_summary {chan} {
puts $chan [html_link "/backup_show_station" "Return to the channel summary"]
}
proc write_fsid_toggle_script {chan count_restorable} {
if {$count_restorable != 0} {
puts $chan ""
puts $chan ""
puts $chan ""
}
}
proc write_fsid_toggle_checkall {title count_restorable} {
set result "Restore"
if {$count_restorable} {
set result ""
}
return $result
}
proc take_snapshot_for_backup {} {
take_snapshot_station 1
take_snapshot_theme 1
take_snapshot_sp 1 1
# Trim off the stations that are not used by any season pass
trim_station_list
}
proc take_snapshot_for_restore {} {
take_snapshot_station 0
take_snapshot_theme 0
take_snapshot_sp 0 1
}
proc take_snapshot_station {full_info} {
reset_snapshot_station 0
global db
global channeltablenum
foreach channum [lsort -real [array names channeltablenum]] {
set stationfsid $channeltablenum($channum)
RetryTransaction {
set station [db $db openid $stationfsid]
set fields [dbobj $station attrs]
set content [construct_record_content $station $fields]
snapshot_add_station $stationfsid $content $channum $full_info
}
}
}
proc snapshot_add_station {fsid content channum full_info} {
global snapshot_station
global snapshot_station_by_num
set content "agwChannelNum int $channum $content"
if {!$full_info} {
set content [filter_content $content "agwChannelNum CallSign Name TmsId" ""]
}
set snapshot_station($fsid) $content
set snapshot_station_by_num($channum) $fsid
}
proc take_snapshot_theme {full_info} {
global snapshot_theme
global snapshot_theme_by_name
reset_load_array snapshot_theme 0
reset_load_array snapshot_theme_by_name 0
RetryTransaction {
set wishlists_missing [catch {mfs find "/Theme"}]
}
if {!$wishlists_missing} {
global db
ForeachMfsFileTrans fsid name type "/Theme" "" 20 {
set theme [db $db openid $fsid]
set fields [dbobj $theme attrs]
set content [construct_record_content $theme $fields]
snapshot_add_theme $fsid $content $full_info
}
}
}
proc snapshot_add_theme {fsid content full_info} {
global snapshot_theme
global snapshot_theme_by_name
if {!$full_info} {
set content [filter_content $content "Name ThemeType" ""]
}
set snapshot_theme($fsid) $content
if {$full_info} {
set snapshot_theme_by_name([agextract $content Name]) $fsid
}
#check_store "on taking snapshot theme"
}
proc take_snapshot_sp {full_info wipe_series} {
global snapshot_sp
global snapshot_sp_by_priority
global snapshot_series
wipe_snapshot_sp_arrays 0 $wipe_series
global db
global seasonpassdir
ForeachMfsFileTrans fsid name type $seasonpassdir "" 20 {
set sp [db $db openid $fsid]
set fields [dbobj $sp attrs]
set content [construct_record_content $sp $fields]
agappend content agwSPType string [sptype $content]
snapshot_add_sp $fsid $content $full_info
set series [dbobj $sp get Series]
if {[string length $series] != 0} {
set seriesfsid [dbobj $series fsid]
if {![info exists snapshot_series($seriesfsid)]} {
set seriesfields [dbobj $series attrs]
set seriescontent [construct_record_content $series $seriesfields]
snapshot_add_series $seriesfsid $seriescontent $full_info
}
}
}
# Load the persistent temporary series from disk
load_temporary_series
}
proc wipe_snapshot_sp_arrays {full_wipe wipe_series} {
global snapshot_sp
global snapshot_sp_by_priority
reset_load_array snapshot_sp $full_wipe
reset_load_array snapshot_sp_by_priority $full_wipe
if {$wipe_series} {
global snapshot_series
reset_load_array snapshot_series $full_wipe
}
}
proc snapshot_add_sp {fsid content full_info} {
if {!$full_info} {
set content [filter_content $content "agwSPType Series Station Theme DayOfWeekLocal StartTimeLocal Duration" ""]
}
set ::snapshot_sp($fsid) $content
if {$full_info} {
set ::snapshot_sp_by_priority([agextract $content Priority]) $fsid
}
#check_store "on taking snapshot season pass"
}
proc snapshot_add_series {fsid content full_info} {
if {!$full_info} {
set content [filter_content $content "Title IndexPath TmsId ServerId" ""]
}
set ::snapshot_series($fsid) $content
#check_store "on taking snapshot series"
}
proc create_backup {chan fname} {
global global_trace
set global_trace $chan
backup_zap_store 0
set open_action "Create"
set backup_previous ""
if {[file exists $fname]} {
set open_action "Overwrite"
set backup_previous "$fname.old"
file copy -force $fname $backup_previous
}
set fd 0
if {[open_file fd $chan $fname 1]} {
# show_store_usage_text_2 $chan "before snapshot"
puts $chan "Taking snapshot of season passes - please wait
"
take_snapshot_for_backup
# show_store_usage_text_2 $chan "after snapshot"
puts $chan "
"
puts $chan [html_table_start "" "Backup Checklist" "COLSPAN=2"]
if {$backup_previous != ""} {
create_backup_progress $chan "Copy existing backup file to '$backup_previous'" ""
}
create_backup_progress $chan "$open_action backup file '$fname'" ""
create_backup_write_header $chan $fd
create_backup_write_version $chan $fd
create_backup_write_channels $chan $fd
create_backup_write_themes $chan $fd
create_backup_write_season_passes $chan $fd
show_store_usage_row $chan "after writing file"
puts $chan [html_table_end]
puts $fd "\}"
close $fd
# Release the store allocated to the snapshot
backup_zap_store 1
# Put in a link to the backup file
puts $chan "You can save this backup file on your computer by right-clicking "
puts $chan "the link below and choosing the browser's 'save target as' option:
"
puts $chan "
"
puts $chan [html_link "/backup$fname/" "$fname"]
puts $chan "
(Don't worry if it saves it as a HTML file, it will still work)
"
puts $chan "
"
# Put up the back button
show_link_backup_menu $chan
}
}
proc create_backup_progress {chan message result} {
if {$result == ""} {
set result "Done"
}
puts $chan [tr "" [td $message] [td $result]]
# puts $chan "$message: $result
"
}
proc create_backup_write_header {chan fd} {
puts $fd "$::global_backup_magic_number - do not edit on or before this line"
puts $fd "# Layout: $::global_backup_layout_version"
puts $fd "# Created by module version $::global_backup_version"
puts $fd "# The latest version of backup.itcl can be found somewhere under"
puts $fd "# http://www.boygenius.co.uk/tivo"
puts $fd "# This is executable code - TiVo Web runs it to load the data into store. If"
puts $fd "# you must edit it then please be careful."
puts $fd ""
puts $fd "# The load procedure"
puts $fd "proc load_file_content \{\} \{"
puts $fd " # Store the date that the backup was taken"
set todaystr [clock format [clock seconds] -format "%a %b %d %Y %H:%M:%S"]
puts $fd " load_backup_date \"$todaystr\""
create_backup_progress $chan "Write header" ""
}
proc create_backup_write_version {chan fd} {
puts $fd ""
puts $fd " # Load the layout version"
puts $fd " load_layout_version \"$::global_backup_layout_version\""
puts $fd ""
create_backup_progress $chan "Store layout version number $::global_backup_layout_version" ""
puts $fd " # Load the TiVo software version running on the TiVo that was backed up"
puts $fd " load_version \{$::tivoswversion\}"
create_backup_progress $chan "Store TiVo version number $::tivoswversion" ""
}
proc create_backup_write_channels {chan fd} {
puts $fd ""
puts $fd " # These are the channels used by the season passes in the backup"
puts $fd " # The list is for reference only and doesn't reflect the full"
puts $fd " # list of channels in the lineup at the time of the backup"
global snapshot_station
global snapshot_station_by_num
set count 0
foreach channum [lsort -real [array names snapshot_station_by_num]] {
incr count
set fsid $snapshot_station_by_num($channum)
puts $fd " load_channel {$fsid} \{$snapshot_station($fsid)\}"
}
create_backup_progress $chan "Store details for $count channels (for reference only, may contain duplicates)" ""
}
proc create_backup_write_themes {chan fd} {
puts $fd ""
puts $fd " # Load the wishlists on the TiVo at the time of backup"
global snapshot_theme
global snapshot_theme_by_name
set count 0
foreach name [lsort [array names snapshot_theme_by_name]] {
incr count
set fsid $snapshot_theme_by_name($name)
puts $fd " load_theme {$fsid} \{$snapshot_theme($fsid)\}"
}
create_backup_progress $chan "Store details for $count wishlists" ""
}
proc create_backup_write_season_passes {chan fd} {
puts $fd ""
puts $fd " # Load the season passes on the TiVo at the time of backup"
global snapshot_sp
global snapshot_sp_by_priority
set count 0
foreach priority [lsort -dictionary [array names snapshot_sp_by_priority]] {
set fsid $snapshot_sp_by_priority($priority)
set data $snapshot_sp($fsid)
if {[sanity_check_sp $chan $fsid $data] == 1} {
incr count
agremove data agwSPType
puts $fd " load_season_pass {$fsid} \{$data\}"
}
}
create_backup_progress $chan "Store details for $count season passes" ""
global snapshot_series
if {[array size snapshot_series] != 0} {
puts $fd ""
puts $fd " # Load the series used by the season passes"
set count 0
foreach seriesfsid [array names snapshot_series] {
incr count
puts $fd " load_series {$seriesfsid} \{$snapshot_series($seriesfsid)\}"
}
create_backup_progress $chan "Store details for $count series" ""
}
}
proc sanity_check_sp {chan fsid data} {
set result 1
# Double-check that the wishlist is there - snoopy had a bizarre backup that
# had an SP referring to a missing theme
set sptype [agextract $data agwSPType]
set sppri [agextract $data Priority]
incr sppri
if {$sptype == "Wishlist"} {
set chkthm_fsid [agextract $data Theme]
if {[lsearch -exact [array names ::snapshot_theme] $chkthm_fsid] == -1} {
set result 0
create_backup_progress $chan "Season pass #$sppri NOT written to backup - refers to missing wishlist $chkthm_fsid" "WARNING"
}
}
return $result
}
proc load_backup {chan fname} {
global global_trace
set global_trace $chan
set errorCode ""
set errorInfo ""
backup_zap_store 0
global restore_cbl1
global restore_cby1
get_store_usage restore_cbl1 restore_cby1
# We won't catch errors in the generated file... the screen display will help
# when debugging glitches in the thing...
source $fname
global timing_load_file
set timing_load_file [agtime {load_file_content}]
# What we have here is a bunch of data. We need to figure out what channels
# are missing, ditto wishlists, season passes etc.
global timing_automap_stations
global timing_automap_themes
global timing_automap_series
global timing_automap_sps
take_snapshot_for_restore
set timing_automap_stations [agtime {automap_restore_stations}]
set timing_automap_themes [agtime {automap_restore_themes}]
set timing_automap_series [agtime {automap_restore_series}]
# This one uses the map values established by automap_themes and
# automap_series - it *MUST* run after those have finished...
set timing_automap_sps [agtime {automap_restore_sps}]
# How many bad themes did we see?
if {$::restore_ignored_sp_badthm != 0} {
set t_pass "pass"
set t_wlst "a wishlist"
set t_has "has"
if {$::restore_ignored_sp_badthm > 1} {
set t_pass "passes"
set t_wlst "wishlists"
set t_has "have"
}
puts $chan "The TiVo that this backup was taken on had "
puts $chan "$::restore_ignored_sp_badthm season $t_pass "
puts $chan "for $t_wlst that did not exist. The season $t_pass $t_has "
puts $chan "been removed from the restore. Please check your wishlist "
puts $chan "SPs after the restore.
"
puts $chan "
"
}
# Release any remaining snapshot SP data
wipe_snapshot_sp_arrays 1 0
return 1
}
proc reset_load_slots {full} {
reset_load_arrays $full
set $::restore_backup_date ""
set $::restore_layout_version ""
set $::restore_load_version ""
}
proc reset_snapshot {full} {
reset_snapshot_station $full
reset_snapshot_theme $full
reset_snapshot_sp $full
global temporary_series_by_tmsid
reset_load_array temporary_series_by_tmsid $full
}
proc reset_snapshot_station {full} {
global snapshot_station
global snapshot_station_by_num
reset_load_array snapshot_station $full
reset_load_array snapshot_station_by_num $full
}
proc reset_snapshot_theme {full} {
global snapshot_theme
global snapshot_theme_by_name
reset_load_array snapshot_theme $full
reset_load_array snapshot_theme_by_name $full
}
proc reset_snapshot_sp {full} {
global snapshot_sp
global snapshot_sp_by_priority
global snapshot_series
reset_load_array snapshot_sp $full
reset_load_array snapshot_sp_by_priority $full
reset_load_array snapshot_series $full
}
proc reset_load_arrays {full} {
global restore_series
global restore_sp
global restore_sp_by_priority
global restore_station
global restore_station_by_num
global restore_theme
global restore_theme_by_name
reset_load_array restore_series $full
reset_load_array restore_sp $full
reset_load_array restore_sp_by_priority $full
reset_load_array restore_station $full
reset_load_array restore_station_by_num $full
reset_load_array restore_theme $full
reset_load_array restore_theme_by_name $full
}
proc reset_load_array {arrayref full} {
upvar $arrayref arr
if {[info exists arr]} {
unset arr
}
if {!$full} {
array set arr ""
}
}
proc load_backup_date {v} {
set ::restore_backup_date $v
check_store "on loading backup date"
}
proc load_channel {fsid data} {
# Channels are unique amongst the arrays that are built up in that they do
# not get written into the database. The backup file contains an awful lot of
# information about the channel that we can just discard.
set data [filter_content $data "agwChannelNum CallSign Name TmsId" ""]
set num [agextract $data agwChannelNum]
set ::restore_station_by_num($num) $fsid
set ::restore_station($fsid) $data
check_store "on loading channel"
}
proc load_layout_version {v} {
set ::restore_layout_version $v
check_store "on loading version"
}
proc load_season_pass {fsid data} {
# To reduce store requirements we'll remove large and unnecessary fields
# I've added TuningPreferences to the list of stripped fields - this is
# because it's a key to information that I don't store in the backup file.
# The information is trivial and optional. I can't test storing TuningPrefs
# on a UK Tivo so I'm very uneasy about writing code to handle them... so I'm
# not going to. If anyone wants to add them in themselves then please, feel
# free...
set data [filter_content $data "" "IndexPath IndexUsedBy TuningPreferences"]
# Override the Priority in the file and set it to the next available slot
# in the array - this way we can change priority just by changing the order
# of the lines in the file, and not have to worry about changing values
set pri [array size ::restore_sp]
agwrite data Priority int $pri
# Figure out what type of season pass we have.
# This information was present in the snapshot that was originally used to
# create the backup but I didn't like to rely on it. The backup should be
# raw data and values inferred from the raw data should be current as of
# the restore script, not current as of the backup script.
set agwSPType [sptype $data]
agappend data agwSPType string $agwSPType
set ::restore_sp($fsid) $data
set ::restore_sp_by_priority($pri) $fsid
check_store "on loading season pass"
}
proc load_series {seriesfsid data} {
# To reduce store requirements we'll remove large and unnecessary fields
set data [filter_content $data "" "IndexUsedBy"]
set ::restore_series($seriesfsid) $data
if {[array size ::restore_series] == 1} {
# Check to see if this is an Australian TiVo - they have no TmsId
set TmsId [agextract $data TmsId]
if {$TmsId == ""} {
set ::restore_is_aus 1
set ::restore_series_unique_id "ServerId"
} else {
set ::restore_is_aus 0
set ::restore_series_unique_id "TmsId"
}
}
check_store "on loading series"
}
proc load_theme {fsid data} {
# To reduce store requirements we'll remove large and unnecessary fields
set data [filter_content $data "" "IndexPath IndexUsedBy"]
set ::restore_theme($fsid) $data
set ::restore_theme_by_name([agextract $data Name]) $fsid
check_store "on loading theme"
}
proc load_version {v} {
set ::restore_load_version $v
# This has been copied from the http-td.tcl module - it figures out if this
# backup was made on a version3 TiVo or not...
set ::restore_version3 0
if {[string range $::restore_load_version 0 2] >= 3.0} {
set ::restore_version3 1
}
check_store "on loading version"
}
proc automap_restore_stations {} {
global restore_station
global snapshot_station
# Loop through the channels we're restoring and write a agwMap fsid value
# that refers back to the channel on the TiVo we're currently running on. If
# the channel no longer exists then write a null agwMap entry. In order to
# keep things as quick as possible the snapshot stations are not looped in
# the main loop - the relevent TmsId and fsid are kept in a list and searched
set snapstat_fsid ""
set snapstat_tmsid ""
foreach snapfsid [array names snapshot_station] {
lappend snapstat_fsid $snapfsid
lappend snapstat_tmsid [agextract $snapshot_station($snapfsid) TmsId]
}
foreach restore_fsid [array names restore_station] {
set restore_data $restore_station($restore_fsid)
set tmsid [agextract $restore_data TmsId]
set map_fsid ""
set snap_element [lsearch -exact $snapstat_tmsid $tmsid]
if {$snap_element != -1} {
set map_fsid [lindex $snapstat_fsid $snap_element]
}
agappend restore_data agwMap fsid $map_fsid
if {$map_fsid == ""} {
agappend restore_data agwWasMissing bool 1
}
unset restore_station($restore_fsid)
set restore_station($restore_fsid) $restore_data
}
}
proc automap_restore_themes {} {
global restore_theme
global snapshot_theme
# Loop through the wishlists and see if there is already a wishlist entry for
# each one that was backed up. If there is then write this into agwMap for
# the restore. A missing agwMap indicates a wishlist that can be restored.
# A wishlist exists if there is another wishlist for the same name and type.
foreach restore_fsid [array names restore_theme] {
set restore_data $restore_theme($restore_fsid)
set restore_name [agextract $restore_data Name]
set restore_type [agextract $restore_data ThemeType]
set agwMap ""
foreach snapshot_fsid [array names snapshot_theme] {
set snapshot_data $snapshot_theme($snapshot_fsid)
if {[agextract $snapshot_data Name] == $restore_name} {
if {[agextract $snapshot_data ThemeType] == $restore_type} {
set agwMap $snapshot_fsid
break
}
}
}
agappend restore_data agwMap fsid $agwMap
if {$agwMap == ""} {
agappend restore_data agwRestore bool "1"
}
unset restore_theme($restore_fsid)
set restore_theme($restore_fsid) $restore_data
}
}
proc automap_restore_series {} {
# Loop through and see if this series is already on the TiVo. There is no
# snapshot of the series on the TiVo - there could be tons of them - so this
# will have to read the database
global restore_series
foreach restore_fsid [array names restore_series] {
set restore_data $restore_series($restore_fsid)
set agwMap [get_series_fsid $restore_data]
agappend restore_data agwMap fsid $agwMap
unset restore_series($restore_fsid)
set restore_series($restore_fsid) $restore_data
}
}
proc automap_restore_sps {} {
# Loop through the season passes we loaded in and figure out which ones can
# be restored and which ones can't. We can NEVER restore a season pass if it
# is already in the database - an SP is in the database if it is for a theme
# and there is already an SP for the theme, or if it is for a series and
# there is already an SP for the series AND channel.
#
# If the SP is missing then we need to make sure we have a good map for the
# channel it is for. If we don't then we can't restore it (at least until the
# user maps the channel).
#
# It is likely that this validation will take a long time.
global restore_sp
# Get the list of stations in the backup file - this is used to add dummy
# stations for any backup files affected by the pre-1.00.0005 bug that could
# leave some stations out of the backup file
global restore_station
global restore_station_by_num
set restore_stnlist [array names restore_station]
set next_unused_channel 0
foreach num [array names restore_station_by_num] {
if {$num > $next_unused_channel} {
set next_unused_channel $num
}
}
set next_unused_channel [expr $next_unused_channel + 1]
set remove_list_badthm ""
# Finally! Loop through the season passes
foreach restore_fsid [array names restore_sp] {
set restore_data $restore_sp($restore_fsid)
# There was a problem where one user (snoopy) had a backup that contained a
# season pass that referred to a non-existent wishlist. This was the only
# report so for the time being I'll test for it here and print a report but
# leave it out of the main summary. That way the rest of the backup will
# still work.
set restore_type [agextract $restore_data agwSPType]
set sp_removed 0
if {$restore_type == "Wishlist"} {
set chkthm_fsid [agextract $restore_data Theme]
if {[lsearch -exact [array names ::restore_theme] $chkthm_fsid] == -1} {
set sp_removed 1
lappend remove_list_badthm $restore_fsid
}
}
if {$sp_removed == 0} {
# Check that the station for this season pass is in the backup file. There
# was a problem whereby the station list could be incomplete
# (fixed 1.00.0005)
set station [agextract $restore_data Station]
if {$station != "" && [lsearch -exact $restore_stnlist $station] == -1} {
# Create a dummy entry in the restored stations list - the user will have
# to remap entries on this season pass
set content ""
agappend content agwChannelNum int $next_unused_channel
agappend content agwMap fsid ""
agappend content agwWasMissing bool 1
agappend content CallSign string "MISSING"
agappend content Name string "Missing"
agappend content TmsId string "agwMISSING"
set restore_station($station) $content
set restore_station_by_num($next_unused_channel) $station
lappend restore_stnlist $station
# Figure new unique channel number
set next_unused_channel [expr $next_unused_channel + 1]
}
# Loop through the snapshot season passes looking for a match for this one
# The FSID of the matching pass is written, as usual, into agwMap
set agwMap [find_matching_sp $restore_data ""]
# If the season pass can be restored then we need to see if the station it
# uses is good... if it has one that is...
agappend restore_data agwMap fsid $agwMap
set agwRestore 0
if {$agwMap == ""} {
if {[station_is_mapped $station]} {
set agwRestore 1
}
}
# Update the array
agappend restore_data agwRestore bool $agwRestore
unset restore_sp($restore_fsid)
set restore_sp($restore_fsid) $restore_data
}
}
# Remove any season passes that need zapping because they had a bad theme id
set ::restore_ignored_sp_badthm [llength $remove_list_badthm]
foreach badthm_fsid $remove_list_badthm {
unset restore_sp($badthm_fsid)
foreach badthm_priority [array names ::restore_sp_by_priority] {
set badthm_pri_fsid $::restore_sp_by_priority($badthm_priority)
if {$badthm_pri_fsid == $badthm_fsid} {
unset ::restore_sp_by_priority($badthm_priority)
break
}
}
}
}
proc find_matching_sp {restore_data override_TmsId} {
set result ""
set restore_type [agextract $restore_data agwSPType]
global snapshot_sp
foreach snapshot_fsid [array names snapshot_sp] {
if {[sp_matches $restore_data $snapshot_sp($snapshot_fsid) $restore_type $override_TmsId]} {
# We have a match - if this is a series or wishlist SP then remove the
# SP to shorten future matches
set result $snapshot_fsid
if {$restore_type == "Series" || $restore_type == "Wishlist"} {
unset snapshot_sp($snapshot_fsid)
}
break
}
}
return $result
}
proc sp_matches {restore snapshot restore_type override_TmsId} {
set result 0
if {$restore_type == [agextract $snapshot agwSPType]} {
switch -exact $restore_type {
Series {
set restore_fsid [agextract $restore Series]
set snapshot_fsid [agextract $snapshot Series]
if {[are_same_series $restore_fsid $snapshot_fsid]} {
# The series match... do the channels match?
set restore_tmsid $override_TmsId
if {$restore_tmsid == ""} {
set rstn [extract_station $restore ::restore_station]
set restore_tmsid [agextract $rstn TmsId]
}
set sstn [extract_station $snapshot ::snapshot_station]
if {$restore_tmsid == [agextract $sstn TmsId]} {
set result 1
}
}
}
Wishlist {
set restore_fsid [agextract $restore Theme]
set snapshot_fsid [agextract $snapshot Theme]
set result [are_same_theme $restore_fsid $snapshot_fsid]
}
Manual {
set result [are_same_manual $restore $snapshot]
}
default {
set result 0
}
}
}
return $result
}
proc get_unmapped_stations_count {} {
global restore_station
return [get_empty_agwMap_count restore_station]
}
proc get_missing_theme_count {} {
global restore_theme
return [get_empty_agwMap_count restore_theme]
}
proc get_missing_sp_count {} {
global restore_sp
return [get_empty_agwMap_count restore_sp]
}
proc get_restore_theme_count {} {
global restore_theme
return [get_restore_count restore_theme]
}
proc get_restore_sp_count {} {
global restore_sp
return [get_restore_count restore_sp]
}
proc get_empty_agwMap_count {arrref} {
upvar $arrref arr
set result 0
foreach fsid [array names arr] {
if {[agextract $arr($fsid) agwMap] == ""} {
incr result
}
}
return $result
}
proc get_restore_count {arrref} {
upvar $arrref arr
set result 0
foreach fsid [array names arr] {
set data $arr($fsid)
if {[agextract $data agwMap] == "" && [agextract $data agwRestore]} {
incr result
}
}
return $result
}
proc present_backup {chan} {
global restore_series
global restore_station
global restore_sp
global restore_theme
puts $chan [html_table_start "" "Backup Contents" "COLSPAN=2"]
puts $chan [tr "" [td "Blocks / bytes before loading file"] [td "$::restore_cbl1 / $::restore_cby1"]]
puts $chan [tr "" [td "Backup file"] [td "$::restore_fname"]]
puts $chan [tr "" [td "Creation date and time"] [td $::restore_backup_date]]
puts $chan [tr "" [td "Version of TiVo that produced backup"] [td $::restore_load_version]]
puts $chan [tr "" [td "Version of backup file layout"] [td $::restore_layout_version]]
puts $chan [tr "" [td "Time taken to load backup file"] [td $::timing_load_file]]
puts $chan [tr "" [td "Time taken to validate stations"] [td $::timing_automap_stations]]
puts $chan [tr "" [td "Time taken to validate wishlists"] [td $::timing_automap_themes]]
puts $chan [tr "" [td "Time taken to validate series"] [td $::timing_automap_series]]
puts $chan [tr "" [td "Time taken to validate season passes"] [td $::timing_automap_sps]]
puts $chan [tr "" [td "Number of season pass [html_link "/backup_show_station" "channels"] in lineup at time of backup"] [td [array size restore_station]]]
puts $chan [tr "" [td "Number of [html_link "/backup_show_theme" "wishlists"] in backup"] [td [array size restore_theme]]]
puts $chan [tr "" [td "Number of unique [html_link "/backup_show_series" "series"] in backup"] [td [array size restore_series]]]
puts $chan [tr "" [td "Number of [html_link "/backup_show_sp" "season passes"] in backup"] [td [array size restore_sp]]]
if {$::restore_ignored_sp_badthm != 0} {
puts $chan [tr "" [td "Number of ignored season passes thru missing wishlists"] [td $::restore_ignored_sp_badthm]]
}
show_store_usage_row $chan "currently free"
puts $chan [html_table_end]
set unmapped_stations [get_unmapped_stations_count]
if {$unmapped_stations != 0} {
puts $chan "There are $unmapped_stations unmapped channels. Click"
puts $chan [html_link "/backup_show_station" "here"]
puts $chan "to map them to the channels you now have.
"
} else {
puts $chan "All of the TV channels used by the season passes seem to be"
puts $chan "present. Click"
puts $chan [html_link "/backup_show_station" "here"]
puts $chan "to make sure.
"
}
set count_restore_themes [get_restore_theme_count]
puts $chan "There are currently $count_restore_themes wishlists to restore. Click"
puts $chan [html_link "/backup_show_theme" "here"]
puts $chan "to examine the wishlists.
"
set count_restore_sp [get_restore_sp_count]
puts $chan "There are currently $count_restore_sp season passes to restore. Click"
puts $chan [html_link "/backup_show_sp" "here"]
puts $chan "to examine the season passes.
"
set orphans [get_orphaned_series]
set orphan_count [llength $orphans]
# If we think this is an Australian TiVo then say so
if {$::restore_is_aus != 0} {
puts $chan "
This backup appears to have been made on a"
puts $chan "non-US/UK TiVo. Support for these is experimental!
"
puts $chan "Please check the [html_link "/backup_show_series" "series"] to make sure they look reasonable.
"
puts $chan "Please report any problems with restoring entries to the Series database.
"
}
# Show the restore button if we've got anything to restore
puts $chan "
"
if {$orphan_count != 0} {
puts $chan ""
puts $chan "There are orphaned series - any season pass attached to an"
puts $chan "orphaned series will be SKIPPED. You should examine the"
puts $chan [html_link "/backup_show_series" "series"]
puts $chan "link and run a deep search to fathom the correct identifiers"
puts $chan "for the orphans.
"
puts $chan ""
}
if {[get_missing_sp_count] == 0 && [get_missing_theme_count] == 0} {
puts $chan "All of the wishlists and season passes in this backup file are"
puts $chan "already on the TiVo. There is nothing to restore.
"
} else {
puts $chan [html_form_start "POST" "/backup_perform_restore"]
puts $chan "Use the links above to select the wishlists and season passes"
puts $chan "you want to restore, and then click the 'Restore' button below"
puts $chan "when you're ready:
"
puts $chan [html_form_input "submit" "submit" "Restore"]
puts $chan [html_form_end]
}
show_link_backup_menu $chan
puts $chan [html_end]
}
proc get_default_backup_dir {} {
global source_dir
global global_backup_default_dir
# Create the default backup directory on the TiVo, if not already there
# and if we can't manage it (bad perms etc.) fallback to /tmp
set result $global_backup_default_dir
if {![file isdirectory $result]} {
catch {file mkdir $result}
if {![file isdirectory $result]} {
set result "/tmp"
}
}
set result [ensure_absolute_path $result]
return $result
}
proc validate_file {chan fname is_write} {
set failmsg ""
# In all cases they must not supply a directory name
if {[file isdirectory $fname]} {
set failmsg "$fname is a directory - you must supply a filename"
} else {
# All the other tests rather depend on whether this is a read or a write
if {$is_write} {
# Does the file exist? If so then check perms. If not then create it and
# check result
if {[file exists $fname]} {
if {![file writable $fname]} {
set failmsg "$fname cannot be overwritten - please choose another file"
}
}
} else {
# We're reading the file - it has to exist and be readable
if {![file exists $fname]} {
set failmsg "$fname does not exist - please choose a file that exists"
} elseif {![file readable $fname]} {
set failmsg "$fname exists, but permissions stop it from being read"
}
}
}
set result 1
if {$failmsg != ""} {
puts $chan "$failmsg"
set result 0
}
return $result
}
proc validate_is_backup_file {chan fname} {
set result 0
set fd 0
if {[open_file fd $chan $fname 0]} {
set magic_number [read $fd [string length $::global_backup_magic_number]]
close $fd
set result 1
if {$magic_number != $::global_backup_magic_number} {
puts $chan "$fname is not a TiVo Web backup file"
set result 0
}
}
return $result
}
proc open_file {handleref chan fname is_write} {
upvar $handleref fd
set flags r
if {$is_write} {
set flags w
}
set result 1
set failed [catch {set fd [open $fname $flags]}]
if {$failed == 1} {
puts $chan "Cannot open $fname"
set result 0
}
return $result
}
proc construct_record_content {dbobjid fields} {
set result ""
foreach field $fields {
# There was a bit of an odd bug whereby some fields would have the name
# 0x40019 which would then fail when we'd try to read them. There were
# two reports of this. So... skip fields that start with 0x! I don't use
# any field like that in the backup.
if {[string first "0x" $field] == 0} {
continue
}
# Get the basic information
set field_type [dbobj $dbobjid attrtype $field]
set field_value [dbobj $dbobjid get $field]
set subvalue ""
if {$field_type == "object"} {
foreach sub $field_value {
set subfsid [dbobj $sub fsid]
lappend subvalue $subfsid
}
set field_type fsid
set field_value $subvalue
}
# Add the field, type and value to the list
lappend result $field $field_type $field_value
}
return $result
}
proc agcount {source} {
set result [expr [llength $source] / 3]
return $result
}
proc agextract {source field} {
set items [expr [llength $source] / 3]
for {set i 0; set element 0} {$i < $items} {incr i; incr element 3} {
if {[lindex $source $element] == $field} {
return [lindex $source [expr $element + 2]]
}
}
return ""
}
proc agexists {source field} {
set result 0
set items [agcount $source]
for {set i 0} {$i < $items} {incr i} {
set element [expr $i * 3]
set ifield [lindex $source $element]
if {$ifield == $field} {
set result 1
break
}
}
return $result
}
proc agfindelement {arrref field value} {
upvar $arrref arr
set result ""
foreach element [array names arr] {
set field_value [agextract $arr($element) $field]
if {$field_value == $value} {
set result $element
break
}
}
return $result
}
proc agwrite {sourceref field type value} {
upvar $sourceref source
set items [agcount $source]
set replaced 0
for {set i 0} {$i < $items} {incr i} {
set element [expr $i * 3]
set ifield [lindex $source $element]
if {$ifield == $field} {
set source [lreplace $source [expr $element + 1] [expr $element + 2] $type $value]
set replaced 1
break
}
}
if {$replaced == 0} {
lappend source $field $type $value
}
}
proc agprepend {sourceref field type value} {
upvar $sourceref source
lappend result $field $type $value $source
set source $result
}
proc agappend {sourceref field type value} {
upvar $sourceref source
lappend source $field $type $value
}
proc agremove {sourceref field} {
upvar $sourceref source
set items [agcount $source]
for {set i 0} {$i < $items} {incr i} {
set element [expr $i * 3]
set ifield [lindex $source $element]
if {$ifield == $field} {
set source [lreplace $source $element [expr $element + 2]]
break
}
}
}
proc wishlist_type_labels {} {
return "Keyword Actor Director Advanced Category Title"
}
proc agtime {script} {
set result [time {$script}]
set result [lindex $result 0]
set result [expr $result / 1000000.0]
set result "$result seconds"
return $result
}
proc get_series_fsid {data} {
global temporary_series_by_tmsid
set result [agextract $data agwMap]
if {$result == ""} {
# Because no snapshot is taken of the series we should always check the db
# if we've not found an FSID for the show up until now...
set tmsid [agextract $data $::restore_series_unique_id]
if {$tmsid != ""} {
# Try the server ID first - easier but may fail over version differences
# Note that for Australian TiVo's ServerId is the *only* supported method
set fsid [serverid_to_fsid [agextract $data ServerId] [agextract $data IndexPath] $tmsid]
if {$fsid == ""} {
set fsid [safe_tms_to_fsid $tmsid]
}
if {$fsid == "" || $fsid == 0} {
# Can't find it in the guide data - is it in the temporary list?
if {[lsearch -exact [array names temporary_series_by_tmsid] $tmsid] != -1} {
set fsid $temporary_series_by_tmsid($tmsid)
} else {
set fsid ""
}
}
set result $fsid
if {$fsid != ""} {
RetryTransaction {
set series_data [db $::db openid $fsid]
set series_fields [dbobj $series_data attrs]
set content [construct_record_content $series_data $series_fields]
snapshot_add_series $fsid $content 0
}
}
}
}
return $result
}
proc are_same_theme {restore_fsid snapshot_fsid} {
global restore_theme
set restore_data $restore_theme($restore_fsid)
return [are_same_agwMap $restore_data $snapshot_fsid]
}
proc are_same_series {restore_fsid snapshot_fsid} {
global snapshot_series
global restore_series
set restore_data $restore_series($restore_fsid)
set result [are_same_agwMap $restore_data $snapshot_fsid]
if {!$result && $snapshot_fsid != ""} {
# If the user is suffering from orphan series then the fsid's could be
# different but the TMSID the same...
set snapshot_data $snapshot_series($snapshot_fsid)
if {[agextract $snapshot_data $::restore_series_unique_id] == [agextract $restore_data $::restore_series_unique_id]} {
set result 1
}
}
return $result
}
proc are_same_agwMap {restore_data snapshot_fsid} {
set restore_map [agextract $restore_data agwMap]
set result 0
if {$restore_map == $snapshot_fsid} {
set result 1
}
return $result
}
proc are_same_manual {restore snapshot} {
# Are they for the same channel?
set result 0
set rs [extract_station $restore ::restore_station]
set ss [extract_station $snapshot ::snapshot_station]
if {$rs != "" && $ss != ""} {
if {[agextract $rs TmsId] == [agextract $ss TmsId]} {
# OK, they're the same station. Do the recording days overlap?
set rdow [agextract $restore DayOfWeekLocal]
set sdow [agextract $snapshot DayOfWeekLocal]
set dowmatch 0
foreach dow $rdow {
if {[lsearch $sdow $dow] != -1} {
set dowmatch 1
break
}
}
if {$dowmatch} {
# Same station and same day. Do the times overlap?
set rstart [agextract $restore StartTimeLocal]
set sstart [agextract $snapshot StartTimeLocal]
set rend [expr $rstart + [agextract $restore Duration]]
set ssend [expr $sstart + [agextract $snapshot Duration]]
if {($rstart >= $sstart && $rstart <= $ssend) || ($rend >= $sstart && $rend <= $ssend)} {
# We have a match...
set result 1
}
}
}
}
return $result
}
proc extract_station {data arrref} {
upvar $arrref arr
set result ""
set station [agextract $data Station]
if {$station != ""} {
set result $arr($station)
}
return $result
}
proc station_is_mapped {fsid} {
# A station is mapped if it is missing (i.e. it came from a wishlist SP) or
# if the fsid we were sent corresponds to a mapped entry in restore_station.
global restore_station
set result 1
if {$fsid != ""} {
set result 0
set data $restore_station($fsid)
if {[agextract $data agwMap] != ""} {
set result 1
}
}
return $result
}
proc sptype {sp} {
set result "Unknown"
switch -exact [agextract $sp Type] {
1 {set result "Series"}
2 {set result "Manual"}
3 {set result "Wishlist"}
default {
if {[agexists $sp Series]} {
set result "Series"
} elseif {[agexists $sp Theme]} {
set result "Theme"
} elseif {[agexists $sp DayOfWeek]} {
set result "Manual"
}
}
}
return $result
}
proc get_sp_name {data use_restore} {
switch -exact [agextract $data agwSPType] {
Series {
set series_fsid [agextract $data Series]
set series [get_series $series_fsid $use_restore]
set result [agextract $series Title]
}
Wishlist {
set theme_fsid [agextract $data Theme]
set theme [get_theme $theme_fsid $use_restore]
set result [agextract $theme Name]
}
Manual {
set result [get_manual_description $data]
}
default {
set result "Unknown"
}
}
return [string trim $result "{}"]
}
proc get_series {fsid use_restore} {
set result ""
if {$use_restore} {
global restore_series
set result $restore_series($fsid)
} else {
global db
RetryTransaction {
set series [db $db openid $fsid]
set fields [dbobj $series attrs]
set result [construct_record_content $series $fields]
}
}
return $result
}
proc get_theme {fsid use_restore} {
set result ""
if {$use_restore} {
global restore_theme
set result $restore_theme($fsid)
} else {
global snapshot_theme
set result $snapshot_theme($fsid)
}
return $result
}
proc get_manual_description {data} {
set dow [agextract $data DayOfWeekLocal]
set datelabel "Sun Mon Tue Wed Thu Fri Sat"
set days ""
switch -exact [llength $dow] {
7 {set days "Every Day"}
5 {set days "Mon-Fri"}
default {
foreach day $dow {
if {$day != ""} {
set days "$days "
}
set days "$days[lindex $datelabel $day]"
}
}
}
global tzoffset
set start [agextract $data StartTimeLocal]
set start [expr $start - $tzoffset]
set start_time [agdispsecs $start]
set end [expr $start + [agextract $data Duration]]
set end_time [agdispsecs $end]
set result "$days $start_time-$end_time"
return $result
}
proc agdispsecs {secs} {
set hours [expr $secs / 3600]
set secs [expr $secs - ($hours * 3600)]
set mins [expr $secs / 60]
# If I ever need the seconds it's... set secs [expr secs - ($mins * 60)]
return [format "%02d:%02d" $hours $mins]
}
proc get_sp_station {data use_restore} {
if {$use_restore} {
global restore_station
set station [extract_station $data restore_station]
} else {
global snapshot_station
set station [extract_station $data snapshot_station]
}
return $station
}
proc get_sp_callsign {data use_restore} {
set station [get_sp_station $data $use_restore]
set callsign [agextract $station CallSign]
if {$callsign == ""} {
set callsign "Any"
}
return $callsign
}
proc get_sp_status {data use_restore} {
set station [get_sp_station $data $use_restore]
set result ""
if {[agextract $data agwMap] != ""} {
set result "Already on TiVo"
} else {
set stnmap [agextract $station agwMap]
if {$station != "" && $stnmap == ""} {
set result "Channel no longer exists"
} else {
set result "Restorable"
if {$station != ""} {
global snapshot_station
set sstn $snapshot_station($stnmap)
set sstn_callsign [agextract $sstn CallSign]
# Check that the season pass isn't already on the channel they're
# mapped to
set agwReMap [agextract $data agwReMap]
if {$agwReMap != "" && $agwReMap != "0"} {
set result "Already on TiVo
(after remap to $sstn_callsign)"
} else {
if {[agextract $station CallSign] != $sstn_callsign} {
set result "$result to $sstn_callsign"
}
}
}
}
}
return $result
}
proc perform_restore_themes {chan} {
global restore_theme
global restore_theme_by_name
set good_count 0
set bad_count 0
foreach name [lsort [array names restore_theme_by_name]] {
set fsid $restore_theme_by_name($name)
set theme $restore_theme($fsid)
set agwMap [agextract $theme agwMap]
set agwRestore [agextract $theme agwRestore]
if {$agwMap == "" && $agwRestore == "1"} {
if {![conditional_restore_theme $chan $fsid]} {
incr bad_count
} else {
incr good_count
set name [string trim [agextract $theme Name] "{}"]
puts $chan "Restored wishlist $name
"
}
}
}
puts $chan "Restored $good_count wishlists"
if {$bad_count} {
puts $chan "and failed to restore $bad_count!"
}
puts $chan "
"
}
proc perform_restore_sps {chan} {
global restore_sp
global restore_sp_by_priority
# Get the list of orphaned series - we skip anything that is in this list!
set orphans [get_orphaned_series]
set good_count 0
set bad_count 0
foreach priority [lsort -dictionary [array names restore_sp_by_priority]] {
set fsid $restore_sp_by_priority($priority)
set sp $restore_sp($fsid)
set agwMap [agextract $sp agwMap]
set agwRestore [agextract $sp agwRestore]
if {($agwMap == "" || $agwMap == "0") && $agwRestore == "1"} {
if {[station_is_mapped [agextract $sp Station]]} {
set type [sptype $sp]
set name [get_sp_name $sp 1]
set series [agextract $sp Series]
if {$series != "" && [lsearch -exact $orphans $series] != -1} {
puts $chan "SKIPPING $type season pass $name
"
puts $chan "It is associated with an orphaned series
"
} else {
if {[conditional_restore_theme $chan [agextract $sp Theme]]} {
if {[conditional_restore_series $chan [agextract $sp Series]]} {
set real_priority [establish_next_priority]
agwrite sp Priority int $real_priority
set agwMap [create_object $chan SeasonPass $sp]
agwrite sp Priority int $priority
if {$agwMap == "" || $agwMap == 0} {
incr bad_count
} else {
incr good_count
puts $chan "Restored $type season pass $name
"
catch {event send $TmkEvent::EVT_DATA_CHANGED $TmkDataChanged::SEASON_PASS $agwMap}
}
agwrite sp agwMap fsid $agwMap
unset restore_sp($fsid)
set restore_sp($fsid) $sp
}
}
}
}
}
}
puts $chan "Restored $good_count season passes"
if {$bad_count} {
puts $chan "and failed to restore $bad_count!"
}
puts $chan "
"
if {$good_count} {
puts $chan "It may take some time to schedule the new season passes
"
}
puts $chan "
"
}
proc conditional_restore_theme {chan fsid} {
global restore_theme
set created_item 0
return [conditional_restore $chan $fsid Theme restore_theme created_item]
}
proc conditional_restore_series {chan fsid} {
global restore_series
set created_item 0
set result [conditional_restore $chan $fsid Series restore_series created_item]
if {$created_item} {
global temporary_series_by_tmsid
set series $restore_series($fsid)
set tmsid [agextract $series $::restore_series_unique_id]
set agwMap [agextract $series agwMap]
set temporary_series_by_tmsid($tmsid) $agwMap
# That database has been changed... make damn sure we record the new fsid
# right away, just in case the script goes arse over tit and we forget the
# fsid in a future run
save_temporary_series
# Finally just run the ID through get_series_fsid so that it gets read into
# the snapshot, just in case the user displays series details later on
agremove series agwMap
get_series_fsid $series
}
return $result
}
proc conditional_restore {chan fsid object_id arrref ciref} {
set result 1
upvar $ciref created_item
set created_item 0
if {$fsid != "" && $fsid != "0"} {
upvar $arrref arr
set object $arr($fsid)
set agwMap [agextract $object agwMap]
if {$agwMap == "" || $agwMap == "0"} {
set agwMap [create_object $chan $object_id $object]
if {$agwMap == "" || $agwMap == "0"} {
set result 0
} else {
agwrite object agwMap fsid $agwMap
set arr($fsid) $object
set created_item 1
}
}
}
return $result
}
proc create_object {chan object_id object} {
# The basis of this code was lifted from the original backup routine by Angra
set result ""
# Check that we're not going to run out of store while we're doing this
check_store "BEFORE creating an object"
# Make absolutely positively sure that every object this object refers to is
# now in the database
if {[validate_references $object_id $object]} {
# OK - we're good. Start the transaction.
global db
RetryTransaction {
# Create the object - this starts off empty...
global global_backup_nowrite
set global_backup_nowrite 0
if {!$global_backup_nowrite} {
set new_object [db $db create $object_id]
set fsid [dbobj $new_object fsid]
puts $chan "Created new $object_id (fsid $fsid)
"
} else {
puts $chan "set new_object \[db \$db create \$object_id ($object_id)\]
"
set fsid 1
}
# Now loop through the members of the object and fill it in
set item_count [agcount $object]
for {set i 0} {$i < $item_count} {incr i} {
# Extract the field and value
set element [expr $i * 3]
set field [lindex $object $element]
set type [lindex $object [expr $element + 1]]
set value [lindex $object [expr $element + 2]]
# If the version that made the backup isn't the same as current then
# check to see if we need to rename the field or normalise the value
if {$::version3 != $::restore_version3} {
modify_field_for_version $object_id field type value
}
# If the field is one of my internal fields then discard it
if {$field == "agwMap" || $field == "agwReMap" || $field == "agwRestore" || $field == "agwSPType"} {
continue
}
# If we're inserting a theme and this is the IndexUsedBy or SeasonPass
# items them discard them both... we're just inserting a plain theme
if {[can_skip_field $object_id $field]} {
continue
}
# If the type is an index then retrieve the mapped value
if {$type == "fsid"} {
set value [get_map_value $field $value]
set value [db $db openid $value]
}
# If the value is a string then add it in one go
if {$type == "string"} {
set goodvalue [string trim $value "{}"]
if {$global_backup_nowrite} {
puts $chan " dbobj \$new_object set \$field ($field) \$goodvalue ($goodvalue)
"
} else {
puts $::global_trace "Setting $field $goodvalue
"
dbobj $new_object set $field $goodvalue
}
} else {
# Add the value(s - could be a list) to the object
set index 0
foreach subvalue $value {
if {$index == 0} {
if {$global_backup_nowrite} {
puts $chan " dbobj \$new_object set \$field ($field) \$subvalue ($subvalue)
"
} else {
puts $::global_trace "Setting $field $subvalue
"
dbobj $new_object set $field $subvalue
}
} else {
if {$global_backup_nowrite} {
puts $chan " dbobj \$new_object add \$field ($field) \$subvalue ($subvalue)
"
} else {
puts $::global_trace "(adding to $field - $subvalue)
"
dbobj $new_object add $field $subvalue
}
}
incr index
}
}
}
set result $fsid
}
}
return $result
}
proc modify_field_for_version {object_id fieldref typeref valueref} {
# At the moment we only care about season passes. If we're currently running
# on V3 then (at time of writing) the backup must be V2, so check for the
# FirstRun field and rename it. Ditto the other way, if we're on V2 then the
# backup must be V3 if we're here, so rename ShowStatus to FirstRun and check
# for the V3-only value "2"... nearest equiv. is 0
if {$object_id == "SeasonPass"} {
upvar $fieldref field
if {$::version3 && $field == "FirstRun"} {
set field "ShowStatus"
} elseif {$::restore_version3 && $field == "ShowStatus"} {
set field "FirstRun"
upvar $valueref value
if {$value == "2"} {
set value "0"
}
}
}
}
proc validate_references {object_id object} {
global restore_theme
global restore_series
set result 1
set item_count [agcount $object]
for {set i 0} {$result && $i < $item_count} {incr i} {
set element [expr $i * 3]
set type [lindex $object [expr $element + 1]]
if {$type == "fsid"} {
set field [lindex $object $element]
set value [lindex $object [expr $element + 2]]
if {![can_skip_field $object_id $field]} {
switch $field {
Theme {
global restore_theme
set result [validate_stored_reference restore_theme $field $object_id $object $value]
}
Station {
global restore_station
set result [validate_stored_reference restore_station $field $object_id $object $value]
}
Series {
global restore_series
set result [validate_stored_reference restore_series $field $object_id $object $value]
}
agwMap {}
agwReMap {}
default {
puts $::global_trace "
Unknown sub-object $field seen for $object_id $object
"
puts $::global_trace "SKIPPING THE ABOVE OBJECT - IT WILL NOT BE RESTORED
"
set result 0
}
}
}
}
}
return $result
}
proc validate_stored_reference {arrref field object_id object value} {
set result 1
upvar $arrref arr
set sub_object $arr($value)
set agwMap [agextract $sub_object agwMap]
if {$agwMap == "" || $agwMap == "0"} {
puts $::global_trace "
Unrestored $field seen for $object_id $object
"
puts $::global_trace "SKIPPING THIS OBJECT - IT WILL NOT BE RESTORED
"
set result 0
}
return $result
}
proc get_map_value {field value} {
set result ""
# These should all have been validated before we get here, so SHOULD be
# plain sailing...
switch $field {
Theme {
global restore_theme
set theme $restore_theme($value)
set result [agextract $theme agwMap]
}
Series {
global restore_series
set series $restore_series($value)
set result [agextract $series agwMap]
}
Station {
global restore_station
set station $restore_station($value)
set result [agextract $station agwMap]
}
default {}
}
return $result
}
proc can_skip_field {object_id field} {
set result 0
# Any of the IndexPath / IndexUsedBy / IndexUsed etc. fields can be discounted
if {[regexp "^Index" $field]} {
set result 1
} elseif {$object_id == "Theme" && $field == "SeasonPass"} {
set result 1
} elseif {$object_id == "Series" && $field == "SeriesStationOBSOLETE"} {
set result 1
}
return $result
}
# Based on CreateSeasonPass in sched.itcl, except this version will cope with
# there being no season passes... the original returned a base priority of 1,
# not 0
proc establish_next_priority {} {
set result 0
global seasonpassdir
ForeachMfsFile fsid name type $seasonpassdir "" {
scan $name "%d~%d" num dummy
if {$num >= $result} {
set result [expr $num + 1]
}
}
return $result
}
proc load_temporary_series {} {
global temporary_series_by_tmsid
reset_load_array temporary_series_by_tmsid 0
set fname "[get_default_backup_dir]/$::global_temporary_series_fname"
if {[file exists $fname]} {
source $fname
perform_load_temporary_series temporary_series_by_tmsid
# We have the list in store... we now need to go through and check that
# everything is in order
global db
foreach tmsid [array names temporary_series_by_tmsid] {
set fsid $temporary_series_by_tmsid($tmsid)
set remove_record 0
# Is it in the TMS index? If so then we can ditch it from this
set guide_fsid [safe_tms_to_fsid $tmsid]
if {$guide_fsid >= 0} {
set remove_record 1
} else {
# Is this file so old that the fsid has since been wiped by the TiVo? If
# so then we will need to recreate the series, so we can't have it here
RetryTransaction {
set obj ""
if {[catch {set obj [db $db openid $fsid]}]} {
# It errored... we'll assume it isn't there
set remove_record 1
}
}
}
# Remove the record if we no longer require it
if {$remove_record} {
unset temporary_series_by_tmsid($tmsid)
}
}
}
}
proc save_temporary_series {} {
set fname "[get_default_backup_dir]/$::global_temporary_series_fname"
set fd 0
if {![catch {set fd [open $fname w]}]} {
puts $fd "# $::global_temporary_series_fname"
puts $fd "# This is a temporary file generated by backup.itcl - it holds a list of the"
puts $fd "# series restored by the backup module. These series will not show up in the"
puts $fd "# list of series held by the TiVo until after the next re-index, a few hours"
puts $fd "# after the next call home. Until then backup.itcl cannot quickly find out"
puts $fd "# if the series exists on the TiVo. So it keeps a list internally of all of"
puts $fd "# the series that it creates, and it stores it in this file. If it doesn't"
puts $fd "# find the series on the TiVo then it checks this file before it creates a"
puts $fd "# new series. Every time it loads this file it checks to see whether the"
puts $fd "# series in here is now indexed, and so no longer required. If it is - it"
puts $fd "# it removes it from the file."
puts $fd "#"
puts $fd "# The short version of the spiel is - please do not edit this file"
# Whew! Now to actually do some work. I wonder if anyone will read that?
puts $fd ""
puts $fd "proc perform_load_temporary_series \{arrref\} \{"
puts $fd " upvar \$arrref arr"
global temporary_series_by_tmsid
foreach tmsid [array names temporary_series_by_tmsid] {
set fsid $temporary_series_by_tmsid($tmsid)
puts $fd " set arr($tmsid) $fsid"
}
puts $fd "\}"
close $fd
}
}
proc get_orphaned_series {} {
global restore_series
global snapshot_series
set result ""
foreach fsid [array names restore_series] {
set series $restore_series($fsid)
set agwMap [agextract $series agwMap]
if {$agwMap != ""} {
set real $snapshot_series($agwMap)
set indexpath [agextract $real IndexPath]
if {[regexp "^/Database/Orphan" $indexpath]} {
lappend result $fsid
}
}
}
return $result
}
proc filter_content {content include_fields exclude_fields} {
set result ""
# Save time - is this an "include only include_fields" or "include everything
# except the exclude fields"?
set include_all_but 1
set exclude_all_but 0
if {$include_fields != ""} {
set include_all_but 0
set exclude_all_but 1
}
# Loop through every field
set items [agcount $content]
for {set i 0} {$i < $items} {incr i} {
set element [expr $i * 3]
set field [lindex $content $element]
# Does this field get included?
set do_include 0
if {$include_all_but} {
set do_include 1
if {[lsearch -exact $exclude_fields $field] != -1} {
set do_include 0
}
} else {
set do_include 0
if {[lsearch -exact $include_fields $field] != -1} {
set do_include 1
}
}
if {$do_include} {
set type [lindex $content [expr $element + 1]]
set value [lindex $content [expr $element + 2]]
lappend result $field $type $value
}
}
return $result
}
proc check_store {msg} {
return
set result 1
get_store_usage blocksfree memfree
global global_trace
if {$blocksfree < 500} {
puts $global_trace "
Cannot continue - less than 500 blocks free in the pool $msg"
set result 0
}
if {$memfree < 16000} {
puts $global_trace "
Cannot continue - less than 16K free in the pool $msg"
set result 0
}
if {!$result} {
# Running low on store, stop the backup or restore
# Easiest way for now is to force a parser error
puts $global_trace "
Forcing an 'INTERNAL SERVER ERROR' to stop the module now"
stopthisscriptnow
}
}
set global_pinfo_good 0
set global_pinfo_est 0
proc get_store_usage {blocksfreeref memfreeref} {
upvar $blocksfreeref blocksfree
upvar $memfreeref memfree
set blocksfree 0
set memfree 0
# Just in case this pool stuff only works on some versions of the TiVo we'll
# do a check, first time in, to see if the call looks alright
global global_pinfo_good
global global_pinfo_est
if {!$global_pinfo_est} {
set global_pinfo_good 0
set global_pinfo_est 1
if {![catch {set phandles [pool handles]}]} {
if {$phandles != ""} {
set phandle [lindex $phandles 0]
if {![catch {set pinfo [pool $phandle info]}]} {
if {[llength $pinfo] > 7 } {
if {[lindex $pinfo 0] == "nblocks" && [lindex $pinfo 2] == "maxblocks"} {
if {[lindex $pinfo 4] == "nbytes" && [lindex $pinfo 6] == "maxbytes"} {
set global_pinfo_good 1
}
}
}
}
}
}
}
if {!$global_pinfo_good} {
# Can't trust it - disable memory checks with reasonable figures
set blocksfree 99999
set memfree 99999
} else {
foreach p [pool handles] {
set pinfo [pool $p info]
set nblocks [lindex $pinfo 1]
set maxblocks [lindex $pinfo 3]
set nbytes [lindex $pinfo 5]
set maxbytes [lindex $pinfo 7]
set blocksfree [expr $blocksfree + ($maxblocks - $nblocks)]
set memfree [expr $memfree + ($maxbytes - $nbytes)]
}
}
}
proc show_store_usage_row {chan msg} {
return
get_store_usage blocks bytes
puts $chan [tr "" [td "Blocks / bytes free $msg"] [td "$blocks / $bytes"]]
}
proc show_store_usage_text {chan} {
return
get_store_usage blocks bytes
puts $chan "
($blocks blocks and $bytes bytes free)"
}
proc show_store_usage_text_2 {chan msg} {
get_store_usage blocks bytes
puts $chan "$blocks blocks / $bytes bytes free $msg
"
}
proc serverid_to_fsid {serverid indexpath tmsid} {
set result ""
if {$serverid != "" && $tmsid != ""} {
set result [search_for_series $indexpath $tmsid]
if {$result == ""} {
# This was the original search - however some machines had extra chars
# appended to the server path so now I test server path as well as this
# old behaviour. IndexPath seems more logical, so I test for it first.
set result [search_for_series "/Server/$serverid" $tmsid]
}
}
return $result
}
proc search_for_series {serverpath tmsid} {
set result ""
RetryTransaction {
if {![catch {mfs find $serverpath} res]} {
if {![catch {set series [db $::db openid [lindex $res 0]]}]} {
if {[dbobj $series type] == "Series"} {
if {[dbobj $series get $::restore_series_unique_id] == $tmsid} {
set result [dbobj $series fsid]
}
}
}
}
}
return $result
}
proc trim_station_list {} {
# This is called after the snapshots of the stations and season passes has
# been taken for a backup. It removes those stations that are not used by
# any season pass.
#
# As well as trimming unused stations this function will also add in any
# stations that TiVo Web didn't pick up
global snapshot_station
global snapshot_station_by_num
global snapshot_sp
global db
# Figure out the next unused channel number, just in case we need to add one
set next_unused_channel 0
foreach num [array names snapshot_station_by_num] {
if {$num > $next_unused_channel} {
set next_unused_channel $num
}
}
set next_unused_channel [expr $next_unused_channel + 1]
# To save time get the list of station fsid's
set stnfsid [array names snapshot_station]
set copyfsid ""
# Loop through the season passes taken in the backup
foreach fsid [array names snapshot_sp] {
set sp $snapshot_sp($fsid)
set station [agextract $sp Station]
if {$station != ""} {
# Have we already copied this station?
if {[lsearch -exact $copyfsid $station] == -1} {
# Is this station in the snapshot?
if {[lsearch -exact $stnfsid $station] == -1} {
# The station doesn't exist in the snapshot - add it
RetryTransaction {
set new_station [db $db openid $station]
set fields [dbobj $new_station attrs]
set content [construct_record_content $new_station $fields]
snapshot_add_station $station $content $next_unused_channel 1
lappend stnfsid $station
set next_unused_channel [expr $next_unused_channel + 1]
}
}
# Copy this station into the list of copied stations
lappend copyfsid $station
}
}
}
# Destroy the entries in the snapshot arrays that are not in the copy list
foreach fsid [array names snapshot_station] {
if {[lsearch -exact $copyfsid $fsid] == -1} {
unset snapshot_station($fsid)
}
}
# Had a bit of a wierd error report whereby a _by_num element referred back
# to an fsid that wasn't there. Can only think that the wipe above of _by_num
# was incomplete. So now I go through each by_num and explicitly zap those
# that aren't in the fsid list, as opposed to extracting the channel number
# above and zapping that
foreach num [array names snapshot_station_by_num] {
set num_fsid $snapshot_station_by_num($num)
if {[lsearch -exact $copyfsid $num_fsid] == -1} {
unset snapshot_station_by_num($num)
}
}
}
# tms_to_fsid is an experimental function in tivo web that is not used by any
# other module. It maps the TMS ID bad to an FSID. TMS IDs seem to be constant
# over different releases of TiVo software, whereas the ServerID is not, so we
# prefer them to ServerID. However we have to make sure we can cope if the
# function fails. All calls to it pass through here and this function makes
# sure it copes with unexpected failures.
#
# If you change this to just return 0 then the rest of the module will cope -
# it will use the ServerID, which will work if the software versions remain
# constant between backup and restore, and if that fails it will tell the user
# to do a deep search.
proc safe_tms_to_fsid {tmsid} {
# return 0
# Remove the hash from the line above to stop the module using tms_to_fsid
# The code below calls tms_to_fsid and returns 0 if it has a total breakdown
set result 0
if {[catch {set result [tms_to_fsid $tmsid]}]} {
set result 0
}
return $result
}
# This function processes calls to the backup module that happen because we
# were called with a URL of "/backup/YADDA" - this gets the "/YADDA" part and
# processes it. I'm using it to dump the file out through the browser. This
# is probably a big security hole if the TiVo is on the Internet! I may have
# to have an option to turn this off or something.
proc process_root_path {chan path formatted} {
# Remove the hash from the line below if this function makes you nervous:
# return
# Remove the trailing slash from the filename. I had to put this in here so
# that files with full-stops in them wouldn't cause TiVoWeb to issue error
# 401
#
# After playing around I noticed that the trailing slash is ignored when the
# file gets opened, so strictly speaking I don't need this bit. I think I'll
# leave it in though, just in case.
global global_root_token
set path [string trimright $path "/"]
if {$path == "/$global_root_token"} {
set path "/"
}
# If the file is a directory then pass it onto browse_directory
if {[file isdirectory $path]} {
browse_directory $chan $path
} else {
# Copy the file out through the browser
set failed [catch {set fd [open $path r]}]
if {$failed == 1} {
puts $chan "Cannot open $path"
} else {
# Unformatted output (useful for saving file contents etc.) is easy...
if {$formatted == 0} {
fcopy $fd $chan
} else {
# Formatted output is slightly more tedious... start by producing a
# simple HTML header that says we're sending pre-formatted text
puts $chan "
" # Loop through each line in the file set text "" set line_length [gets $fd text] while {$line_length != -1} { # Convert the HTML characters regsub -all {\&} $text {\&} text regsub -all {\"} $text {\"} text regsub -all {<} $text {\<} text regsub -all {>} $text {\>} text # Display the result and fetch the next line puts $chan "$text" set line_length [gets $fd text] } # Finish off the HTML page puts $chan "" } close $fd } } } proc browse_directory {chan path} { # Normalise the path - note that / for root would get us the backup menu # so we have to code that as &ROOT&/ - an invalid Unix filename global global_root_token set rawpath [string trimright $path "/"] if {$rawpath == "/$global_root_token"} { set rawpath "" } set path "$rawpath/" # Display the contents of the directory puts $chan [html_start "Listing of '$path'"] if {[file isdirectory $path] == 0} { puts $chan "'$path' is not a directory