00001 #!/usr/bin/perl
00002
00003 # check for recording anomolies -
00004 # based somewhat on greg froese's "myth.rebuilddatabase.pl"
00005 # -- Lincoln Dale <ltd@interlink.com.au>, September 2006
00006 # 2007-03-11: Added pretty print of unknown files vs. orphaned thumbnails. (Robert Kulagowski)
00007
00008 # The intent of this script is to be able to find orphaned rows in the 'recorded' table
00009 # (entries which don't have matching media files) and orphaned media files (potentially
00010 # taking up gigabytes of otherwise usable disk space) which have no matching row in
00011 # the 'recorded' db table.
00012 #
00013 # By default, running the script will simply return a list of problems it finds.
00014 # Running with --dodbdelete will remove db recorded rows for which there is no matching
00015 # media file. Running with --dodelete will delete media files for which there is no
00016 # matching db record.
00017 #
00018 # This script may be useful to fix up some orphaned db entries (causes mythweb to run
00019 # verrry slow) as well as reclaim some disk space from some orphaned media files.
00020 # (in an ideal world, neither of these would ever happen, but i've seen both happen in reality).
00021 # This script makes it easy to keep track of whether it has or hasn't happened, even if you
00022 # have thousands of recordings and terabytes of stored media.
00023 #
00024 # no warranties expressed or implied. if you run this and it deletes all your recordings
00025 # and sets mythtv to fill up all your disk space with The Home Shopping Network, its entirely
00026 # your fault.
00027
00028 my $progname = "myth.find_orphans.pl";
00029 my $revision = "0.20";
00030
00031 use DBI;
00032 use Sys::Hostname;
00033 use Getopt::Long;
00034
00035 #
00036 # options
00037 #
00038
00039 my $opt_host = hostname;
00040 my $opt_dbhost = $opt_host;
00041 my $opt_database = "mythconverg";
00042 my $opt_user = "mythtv";
00043 my $opt_pass = "mythtv";
00044 my $opt_ext = "{nuv,mpg,mpeg,avi}";
00045 my $opt_dir = "";
00046 my $opt_dodelete = 0;
00047 my $opt_dodbdelete = 0;
00048 my $debug = 0;
00049 my $opt_help = 0;
00050
00051 GetOptions(
00052 'host=s' => \$opt_host,
00053 'dbhost=s' => \$opt_dbhost,
00054 'database=s' => \$opt_database,
00055 'user=s' => \$opt_user,
00056 'pass=s' => \$opt_pass,
00057 'dir=s' => \$opt_dir,
00058 'dodelete' => \$opt_dodelete,
00059 'dodbdelete' => \$opt_dodbdelete,
00060 'debug+' => \$debug,
00061 'help' => \$opt_help,
00062 'h' => \$opt_help,
00063 'v' => \$opt_help);
00064
00065 if ($opt_help) {
00066 print<<EOF
00067 $progname (rev $revision)
00068 (checks MythTV recording directories for orphaned files)
00069
00070 options:
00071 --host=(host) MythTV backend host ($opt_host)
00072 --dbhost=(host) host where MySQL database for backend is ($opt_dbhost)
00073 --database=(dbname) MythTV database ($opt_database)
00074 --user=(user) MySQL MythTV database user ($opt_user)
00075 --pass=(pass) MySQL MythTV database password ($opt_pass)
00076 --dir=directories manually specify recording directories (otherwise setting is from database)
00077 --debug increase debug level
00078 --dodbdelete remove recorded db entries with no matching file (default: don't)
00079 --dodelete delete files with no record (default: don't)
00080
00081 EOF
00082 ;
00083 exit(0);
00084 }
00085
00086 #
00087 # go go go!
00088 #
00089
00090 my $valid_recordings = 0;
00091 my $missing_recordings = 0;
00092 my $errors = 0;
00093 my $unknown_files = 0;
00094 my $known_files = 0;
00095 my $unknown_size = 0;
00096 my $known_size = 0;
00097 my $unknown_thumbnail = 0;
00098
00099 if (!($dbh = DBI->connect("dbi:mysql:database=$opt_database:host=$opt_dbhost","$opt_user","$opt_pass"))) {
00100 die "Cannot connect to database $opt_database on host $opt_dbhost: $!\n";
00101 }
00102
00103 if ($opt_dir eq "") {
00104 &dir_lookup("SELECT dirname FROM storagegroup WHERE hostname=(?)");
00105 &dir_lookup("SELECT data FROM settings WHERE value='RecordFilePrefix' AND hostname=(?)");
00106
00107 printf STDERR "Recording directories ($opt_host): $opt_dir\n" if $debug;
00108 }
00109
00110 if ($opt_dir eq "") {
00111 printf "ERROR: no directory found or specified\n";
00112 exit 1;
00113 }
00114
00115 foreach $d (split(/,/,$opt_dir)) {
00116 $d =~ s/\/$
00117 $dirs{$d}++;
00118 }
00119
00120
00121 #
00122 # look in recorded table, make sure we can find every file ..
00123 #
00124
00125 my $q = "SELECT title, subtitle, starttime, endtime, chanid, basename FROM recorded WHERE hostname=(?) ORDER BY starttime";
00126 $sth = $dbh->prepare($q);
00127 $sth->execute($opt_host) || die "Could not execute ($q): $!\n";
00128
00129 while (my @row=$sth->fetchrow_array) {
00130 ($title, $subtitle, $starttime, $endtime, $channel, $basename) = @row;
00131
00132 # see if we can find it...
00133 $loc = find_file($basename);
00134 if ($loc eq "") {
00135 printf "Missing media: %s (title:%s, start:%s)\n",$basename,$title,$starttime;
00136 $missing_recordings++;
00137
00138 if ($opt_dodbdelete) {
00139 my $sql = sprintf "DELETE FROM recorded WHERE basename LIKE \"%s\" LIMIT 1",$basename;
00140 printf "performing database delete: %s\n",$sql;
00141 $dbh->do($sql) || die "Could not execute $sql: $!\n";
00142 }
00143 } else {
00144 $valid_recordings++;
00145 $seen_basename{$basename}++;
00146 $seen_basename{$basename.".png"}++; # thumbnail
00147 }
00148 }
00149
00150 #
00151 # look in recording directories, see if there are extra files not in database
00152 #
00153
00154 foreach my $this_dir (keys %dirs) {
00155 opendir(DIR, $this_dir) || die "cannot open directory $this_dir: $!\n";
00156 foreach $this_file (readdir(DIR)) {
00157 if (-f "$this_dir/$this_file") {
00158
00159 next if ($this_file eq "nfslockfile.lock");
00160
00161 my $this_filesize = -s "$this_dir/$this_file";
00162 if ($seen_basename{$this_file} == 0) {
00163 $sorted_filesizes{$this_filesize} .= sprintf "unknown file [%s]: %s/%s\n",pretty_filesize($this_filesize),$this_dir,$this_file;
00164 $unknown_size += $this_filesize;
00165 if (substr($this_file,-4) eq ".png") {
00166 $unknown_thumbnail++;
00167 }
00168 else {
00169 $unknown_files++;
00170 }
00171
00172 if ($opt_dodelete) {
00173 printf STDERR "deleting [%s]: %s/%s\n",pretty_filesize($this_filesize),$this_dir,$this_file;
00174 unlink "$this_dir/$this_file";
00175
00176 if (-f "$this_dir/$this_file") {
00177 $errors++;
00178 printf "ERROR: could not delete $this_dir/$this_file\n";
00179 }
00180 }
00181 } else {
00182 $known_files++;
00183 $known_size += $this_filesize;
00184 printf "KNOWN file [%s]: %s/%s\n",pretty_filesize($this_filesize),$this_dir,$this_file if $debug;
00185 }
00186 } else {
00187 printf "NOT A FILE: %s/%s\n",$this_dir,$this_file if $debug;
00188 }
00189 }
00190 closedir DIR;
00191 }
00192
00193
00194 #
00195 # finished, report results
00196 #
00197
00198 foreach my $key (sort { $a <=> $b } keys %sorted_filesizes) {
00199 printf $sorted_filesizes{$key};
00200 }
00201
00202 printf "Summary:\n";
00203 printf " Host: %s, Directories: %s\n", $opt_host, join(" ",keys %dirs);
00204 printf " %d ERRORS ENCOUNTERED (see above for details)\n",$errors if ($errors > 0);
00205 printf " %d valid recording%s, %d missing recording%s %s\n",
00206 $valid_recordings, ($valid_recordings != 1 ? "s" : ""),
00207 $missing_recordings, ($missing_recordings != 1 ? "s" : ""),
00208 ($missing_recordings > 0 ? ($opt_dodbdelete ? "were fixed" : "not fixed, check above is valid and use --dodbdelete to fix") : "");
00209 printf " %d known media files using %s\n %d orphaned thumbnails with no corresponding recording\n %d unknown files using %s %s\n",
00210 $known_files, pretty_filesize($known_size),
00211 $unknown_thumbnail,$unknown_files, pretty_filesize($unknown_size),
00212 ($unknown_files > 0 ? ($opt_dodelete ? "were fixed" : "not fixed, check above and use --dodelete to clean up if the above output is accurate") : "");
00213
00214 exit(0);
00215
00216 ###########################################################################
00217 # filesize bling
00218
00219 sub pretty_filesize
00220 {
00221 local($fsize) = @_;
00222 return sprintf "%0.1fGB",($fsize / 1000000000) if ($fsize >= 1000000000);
00223 return sprintf "%0.1fMB",($fsize / 1000000) if ($fsize >= 1000000);
00224 return sprintf "%0.1fKB",($fsize / 1000) if ($fsize >= 1000);
00225 return sprintf "%0.0fB",$fsize;
00226 }
00227
00228 ###########################################################################
00229 # find a file in directories without globbing
00230
00231 sub find_file
00232 {
00233 local($fname) = @_;
00234
00235 foreach my $d (keys %dirs) {
00236 my $f = $d."/".$fname;
00237 if (-e $f) {
00238 return $f;
00239 }
00240 }
00241 return;
00242 }
00243
00244 ###########################################################################
00245
00246 sub dir_lookup
00247 {
00248 my $query = shift;
00249
00250 $sth = $dbh->prepare($query);
00251 $sth->execute($opt_host) || die "Could not execute ($dir_query)";
00252 while (my @row = $sth->fetchrow_array) {
00253 $opt_dir .= "," if ($opt_dir ne "");
00254 $opt_dir .= $row[0];
00255 }
00256 }
00257
00258 ###########################################################################