MythTV  0.26-pre
mythlink.pl
Go to the documentation of this file.
00001 #!/usr/bin/perl -w
00002 #
00003 # Creates symlinks to mythtv recordings using more-human-readable filenames.
00004 # See --help for instructions.
00005 #
00006 # Automatically detects database settings from mysql.txt, and loads
00007 # the mythtv recording directory from the database (code from nuvexport).
00008 #
00009 # @url       $URL$
00010 # @date      $Date$
00011 # @version   $Revision$
00012 # @author    $Author$
00013 # @license   GPL
00014 #
00015 
00016 # Includes
00017     use DBI;
00018     use Getopt::Long;
00019     use File::Path;
00020     use File::Basename;
00021     use File::Find;
00022     use MythTV;
00023 
00024 # Some variables we'll use here
00025     our ($dest, $format, $usage, $underscores, $live, $rename, $maxlength);
00026     our ($chanid, $starttime, $filename);
00027     our ($dformat, $dseparator, $dreplacement, $separator, $replacement);
00028     our ($db_host, $db_user, $db_name, $db_pass, $video_dir, $verbose);
00029     our ($hostname, $dbh, $sh, $q, $count, $base_dir);
00030 
00031 # Default filename format
00032     $dformat = '%T %- %Y-%m-%d, %g-%i %A %- %S';
00033 # Default separator character
00034     $dseparator = '-';
00035 # Default replacement character
00036     $dreplacement = '-';
00037 
00038 # Provide default values for GetOptions
00039     $format      = $dformat;
00040     $separator   = $dseparator;
00041     $replacement = $dreplacement;
00042     $maxlength   = -1;
00043 
00044 # Load the cli options
00045     GetOptions('link|destination|path:s'      => \$dest,
00046                'chanid=s'                     => \$chanid,
00047                'starttime=s'                  => \$starttime,
00048                'filename=s'                   => \$filename,
00049                'format=s'                     => \$format,
00050                'live'                         => \$live,
00051                'separator=s'                  => \$separator,
00052                'replacement=s'                => \$replacement,
00053                'rename'                       => \$rename,
00054                'maxlength=i'                  => \$maxlength,
00055                'usage|help'                   => \$usage,
00056                'underscores'                  => \$underscores,
00057                'verbose'                      => \$verbose
00058               );
00059 
00060 # Print usage
00061     if ($usage) {
00062         print <<EOF;
00063 $0 usage:
00064 
00065 options:
00066 
00067 --link [destination directory]
00068 --destination [destination directory]
00069 --path [destination directory]
00070 
00071     Specify the directory for the links.  If no pathname is given, links will
00072     be created in the show_names directory inside of the current mythtv data
00073     directory on this machine.  eg:
00074 
00075     /var/video/show_names/
00076 
00077     WARNING: ALL symlinks within the destination directory and its
00078     subdirectories (recursive) will be removed.
00079 
00080 --chanid chanid
00081 
00082     Create a link only for the specified recording file. Use with --starttime
00083     to specify a recording. This argument may be used with the event-driven
00084     notification system's "Recording started" event or in a post-recording
00085     user job.
00086 
00087 --starttime starttime
00088 
00089     Create a link only for the specified recording file. Use with --chanid
00090     to specify a recording. This argument may be used with the event-driven
00091     notification system's "Recording started" event or in a post-recording
00092     user job.
00093 
00094 --filename absolute_filename
00095 
00096     Create a link only for the specified recording file. This argument may be
00097     used with the event-driven notification system's "Recording started" event
00098     or in a post-recording user job.
00099 
00100 --live
00101 
00102     Include live tv recordings.
00103 
00104     default: do not link live tv recordings
00105 
00106 --format
00107 
00108     default:  $dformat
00109 
00110     \%T   = Title (show name)
00111     \%S   = Subtitle (episode name)
00112     \%R   = Description
00113     \%C   = Category
00114     \%U   = RecGroup
00115     \%hn  = Hostname of the machine where the file resides
00116     \%c   = Channel:  MythTV chanid
00117     \%cn  = Channel:  channum
00118     \%cc  = Channel:  callsign
00119     \%cN  = Channel:  channel name
00120     \%y   = Recording start time:  year, 2 digits
00121     \%Y   = Recording start time:  year, 4 digits
00122     \%n   = Recording start time:  month
00123     \%m   = Recording start time:  month, leading zero
00124     \%j   = Recording start time:  day of month
00125     \%d   = Recording start time:  day of month, leading zero
00126     \%g   = Recording start time:  12-hour hour
00127     \%G   = Recording start time:  24-hour hour
00128     \%h   = Recording start time:  12-hour hour, with leading zero
00129     \%H   = Recording start time:  24-hour hour, with leading zero
00130     \%i   = Recording start time:  minutes
00131     \%s   = Recording start time:  seconds
00132     \%a   = Recording start time:  am/pm
00133     \%A   = Recording start time:  AM/PM
00134     \%ey  = Recording end time:  year, 2 digits
00135     \%eY  = Recording end time:  year, 4 digits
00136     \%en  = Recording end time:  month
00137     \%em  = Recording end time:  month, leading zero
00138     \%ej  = Recording end time:  day of month
00139     \%ed  = Recording end time:  day of month, leading zero
00140     \%eg  = Recording end time:  12-hour hour
00141     \%eG  = Recording end time:  24-hour hour
00142     \%eh  = Recording end time:  12-hour hour, with leading zero
00143     \%eH  = Recording end time:  24-hour hour, with leading zero
00144     \%ei  = Recording end time:  minutes
00145     \%es  = Recording end time:  seconds
00146     \%ea  = Recording end time:  am/pm
00147     \%eA  = Recording end time:  AM/PM
00148     \%py  = Program start time:  year, 2 digits
00149     \%pY  = Program start time:  year, 4 digits
00150     \%pn  = Program start time:  month
00151     \%pm  = Program start time:  month, leading zero
00152     \%pj  = Program start time:  day of month
00153     \%pd  = Program start time:  day of month, leading zero
00154     \%pg  = Program start time:  12-hour hour
00155     \%pG  = Program start time:  24-hour hour
00156     \%ph  = Program start time:  12-hour hour, with leading zero
00157     \%pH  = Program start time:  24-hour hour, with leading zero
00158     \%pi  = Program start time:  minutes
00159     \%ps  = Program start time:  seconds
00160     \%pa  = Program start time:  am/pm
00161     \%pA  = Program start time:  AM/PM
00162     \%pey = Program end time:  year, 2 digits
00163     \%peY = Program end time:  year, 4 digits
00164     \%pen = Program end time:  month
00165     \%pem = Program end time:  month, leading zero
00166     \%pej = Program end time:  day of month
00167     \%ped = Program end time:  day of month, leading zero
00168     \%peg = Program end time:  12-hour hour
00169     \%peG = Program end time:  24-hour hour
00170     \%peh = Program end time:  12-hour hour, with leading zero
00171     \%peH = Program end time:  24-hour hour, with leading zero
00172     \%pei = Program end time:  minutes
00173     \%pes = Program end time:  seconds
00174     \%pea = Program end time:  am/pm
00175     \%peA = Program end time:  AM/PM
00176     \%oy  = Original Airdate:  year, 2 digits
00177     \%oY  = Original Airdate:  year, 4 digits
00178     \%on  = Original Airdate:  month
00179     \%om  = Original Airdate:  month, leading zero
00180     \%oj  = Original Airdate:  day of month
00181     \%od  = Original Airdate:  day of month, leading zero
00182     \%%   = a literal % character
00183 
00184     * The program start time is the time from the listings data and is not
00185       affected by when the recording started.  Therefore, using program start
00186       (or end) times may result in duplicate names.  In that case, the script
00187       will append a "counter" value to the name.
00188 
00189     * A suffix of .mpg or .nuv will be added where appropriate.
00190 
00191     * To separate links into subdirectories, include the / format specifier
00192       between the appropriate fields.  For example, "\%T/\%S" would create
00193       a directory for each title containing links for each recording named
00194       by subtitle.  You may use any number of subdirectories in your format
00195       specifier.
00196 
00197 --separator
00198 
00199     The string used to separate sections of the link name.  Specifying the
00200     separator allows trailing separators to be removed from the link name and
00201     multiple separators caused by missing data to be consolidated. Indicate the
00202     separator character in the format string using either a literal character
00203     or the \%- specifier.
00204 
00205     default:  '$dseparator'
00206 
00207 --replacement
00208 
00209     Characters in the link name which are not legal on some filesystems will
00210     be replaced with the given character
00211 
00212     illegal characters:  \\ : * ? < > | "
00213 
00214     default:  '$dreplacement'
00215 
00216 --underscores
00217 
00218     Replace whitespace in filenames with underscore characters.
00219 
00220     default:  No underscores
00221 
00222 --rename
00223 
00224     Rename the recording files back to their default names.  If you had
00225     previously used mythrename.pl to rename files (rather than creating links
00226     to the files), use this option to restore the file names to their default
00227     format.
00228 
00229     Renaming the recording files is no longer supported.  Instead, use
00230     http://www.mythtv.org/wiki/mythfs.py to create a FUSE file system that
00231     represents recordings using human-readable file names or use mythlink.pl to
00232     create links with human-readable names to the recording files.
00233 
00234 --maxlength length
00235 
00236     Ensure the link name is length or fewer characters.  If the link name is
00237     longer than length, truncate the name. Zero or any negative value for
00238     length disables length checking.
00239 
00240     Note that this option does not take into account the path length, so on a
00241     file system used by applications with small path limitations (such as
00242     Windows Explorer and Windows Command Prompt), you should specify a length
00243     that takes into account characters used by the path to the dest directory.
00244 
00245     default:  Unlimited
00246 
00247 --verbose
00248 
00249     Print debug info.
00250 
00251     default:  No info printed to console
00252 
00253 --help
00254 --usage
00255 
00256     Show this help text.
00257 
00258 EOF
00259         exit;
00260     }
00261 
00262 # Ensure --chanid and --starttime were specified together, if at all
00263     if ((defined($chanid) or defined($starttime)) and
00264         !(defined($chanid) and defined($starttime))) {
00265         die "The arguments --chanid and --starttime must be used together.\n";
00266     }
00267 
00268 # Ensure --maxlength specifies a reasonable value (though filenames may
00269 # still be useless at such short lengths)
00270     if ($maxlength > 0 and $maxlength < 19) {
00271         die "The --maxlength must be 20 or higher.\n";
00272     }
00273 
00274 # Check the separator and replacement characters for illegal characters
00275     if ($separator =~ /(?:[\/\\:*?<>|"])/) {
00276         die "The separator cannot contain any of the following characters:  /\\:*?<>|\"\n";
00277     }
00278     elsif ($replacement =~ /(?:[\/\\:*?<>|"])/) {
00279         die "The replacement cannot contain any of the following characters:  /\\:*?<>|\"\n";
00280     }
00281 
00282 # Escape where necessary
00283     our $safe_sep = $separator;
00284         $safe_sep =~ s/([^\w\s])/\\$1/sg;
00285     our $safe_rep = $replacement;
00286         $safe_rep =~ s/([^\w\s])/\\$1/sg;
00287 
00288 # Get the hostname of this machine
00289     $hostname = `hostname`;
00290     chomp($hostname);
00291 
00292 # Connect to mythbackend
00293     my $Myth = new MythTV();
00294 
00295 # Connect to the database
00296     $dbh = $Myth->{'dbh'};
00297     END {
00298         $sh->finish  if ($sh);
00299     }
00300 
00301     my $sgroup = new MythTV::StorageGroup();
00302 
00303 # Only if we're renaming files back to "default" names
00304     if ($rename) {
00305         do_rename();
00306     }
00307 
00308 # Get our base location
00309     $base_dir = $sgroup->FindRecordingDir('show_names');
00310     if ($base_dir eq '') {
00311         $base_dir = $sgroup->GetFirstStorageDir();
00312     }
00313 
00314 # Link destination
00315 # Double-check the destination
00316     $dest ||= "$base_dir/show_names";
00317 # Alert the user
00318     vprint("Link destination directory:  $dest");
00319 # Create nonexistent paths
00320     unless (-e $dest) {
00321         mkpath($dest, 0, 0775) or die "Failed to create $dest:  $!\n";
00322     }
00323 # Bad path
00324     die "$dest is not a directory.\n" unless (-d $dest);
00325 # Delete old links/directories unless linking only one recording
00326     if (!defined($filename) and !defined($chanid)) {
00327     # Delete any old links
00328         find sub { if (-l $_) {
00329                        unlink $_ or die "Couldn't remove old symlink $_: $!\n";
00330                    }
00331                  }, $dest;
00332     # Delete empty directories (should this be an option?)
00333     # Let this fail silently for non-empty directories
00334         finddepth sub { rmdir $_; }, $dest;
00335     }
00336 
00337 # Create symlinks for the files on this machine
00338     my %rows = ();
00339     if (defined($chanid)) {
00340         %rows = $Myth->backend_rows('QUERY_RECORDING TIMESLOT '.
00341                                     "$chanid $starttime");
00342 
00343     }
00344     else {
00345         %rows = $Myth->backend_rows('QUERY_RECORDINGS Descending');
00346     }
00347     foreach my $row (@{$rows{'rows'}}) {
00348         my $show = new MythTV::Recording(@$row);
00349     # Skip LiveTV recordings?
00350         next unless (defined($live) || $show->{'recgroup'} ne 'LiveTV');
00351     # File doesn't exist locally
00352         next unless (-e $show->{'local_path'});
00353     # Check if this is the file to link if only linking one file
00354         if (defined($filename)) {
00355             next unless (($show->{'basename'} eq $filename) or
00356                          ($show->{'local_path'} eq $filename));
00357         }
00358         elsif (defined($chanid)) {
00359             next unless ($show->{'chanid'} eq $chanid);
00360             my $recstartts = unix_to_myth_time($show->{'recstartts'});
00361         # Check starttime in MythTV time format (yyyy-MM-ddThh:mm:ss)
00362             if ($recstartts ne $starttime) {
00363             # Check starttime in ISO time format (yyyy-MM-dd hh:mm:ss)
00364                 $recstartts =~ tr/T/ /;
00365                 if ($recstartts ne $starttime) {
00366                 # Check starttime in job queue time format (yyyyMMddhhmmss)
00367                     $recstartts =~ s/[\- :]//g;
00368                     next unless ($recstartts eq $starttime);
00369                 }
00370             }
00371         }
00372     # Format the name
00373         my $name = $show->format_name($format,$separator,$replacement,$dest,$underscores);
00374     # Get a shell-safe version of the filename (yes, I know it's not needed in this case, but I'm anal about such things)
00375         my $safe_file = $show->{'local_path'};
00376         $safe_file =~ s/'/'\\''/sg;
00377         $safe_file = "'$safe_file'";
00378     # Figure out the suffix
00379         my ($suffix) = ($show->{'basename'} =~ /(\.\w+)$/);
00380     # Check the link name's length
00381         $name = cut_down_name($name, $suffix);
00382     # Link destination
00383     # Check for duplicates
00384         if (($name) and -e "$dest/$name$suffix") {
00385             if ((!defined($filename) and !defined($chanid)) or
00386                 (! -l "$dest/$name$suffix")) {
00387                 $count = 2;
00388                 $name = cut_down_name($name, ".$count$suffix");
00389                 while (($name) and -e "$dest/$name.$count$suffix") {
00390                     $count++;
00391                     $name = cut_down_name($name, ".$count$suffix");
00392                 }
00393                 $name .= ".$count" if (($name));
00394             } else {
00395                 unlink "$dest/$name$suffix" or die "Couldn't remove ".
00396                        "old symlink $dest/$name$suffix: $!\n";
00397             }
00398         }
00399         if (!($name)) {
00400             vprint("Unable to represent recording; maxlength too small.");
00401             next;
00402         }
00403         $name .= $suffix;
00404     # Create the link
00405         my $directory = dirname("$dest/$name");
00406         unless (-e $directory) {
00407             mkpath($directory, 0, 0775)
00408                 or die "Failed to create $directory:  $!\n";
00409         }
00410         symlink $show->{'local_path'}, "$dest/$name"
00411             or die "Can't create symlink $dest/$name:  $!\n";
00412         vprint("$dest/$name");
00413     }
00414 
00415 # Check the length of the link name
00416     sub cut_down_name {
00417         my $name = shift;
00418         my $suffix = shift;
00419         if ($maxlength > 0) {
00420             my $charsavailable = $maxlength - length($suffix);
00421             if ($charsavailable > 0) {
00422                 $name = substr($name, 0, $charsavailable);
00423             }
00424             else {
00425                 $name = '';
00426             }
00427         }
00428         return $name;
00429     }
00430 
00431 # Print the message, but only if verbosity is enabled
00432     sub vprint {
00433         return unless (defined($verbose));
00434         print join("\n", @_), "\n";
00435     }
00436 
00437 # Rename the file back to default format
00438     sub do_rename {
00439         $q  = 'UPDATE recorded SET basename=? WHERE chanid=? AND starttime=FROM_UNIXTIME(?)';
00440         $sh = $dbh->prepare($q);
00441         my %rows = $Myth->backend_rows('QUERY_RECORDINGS Descending');
00442         foreach my $row (@{$rows{'rows'}}) {
00443             my $show = new MythTV::Recording(@$row);
00444         # File doesn't exist locally
00445             next unless (-e $show->{'local_path'});
00446         # Format the name
00447             my $name = $show->format_name('%c_%Y%m%d%H%i%s');
00448         # Get a shell-safe version of the filename (yes, I know it's not needed in this case, but I'm anal about such things)
00449             my $safe_file = $show->{'local_path'};
00450             $safe_file =~ s/'/'\\''/sg;
00451             $safe_file = "'$safe_file'";
00452         # Figure out the suffix
00453             my ($suffix) = ($show->{'basename'} =~ /(\.\w+)$/);
00454         # Rename the file, but only if it's a real file
00455             if ($show->{'basename'} ne $name.$suffix) {
00456             # Check for duplicates
00457                 $video_dir = $sgroup->FindRecordingDir($show->{'basename'});
00458                 if (-e "$video_dir/$name$suffix") {
00459                     $count = 2;
00460                     while (-e "$video_dir/$name.$count$suffix") {
00461                         $count++;
00462                     }
00463                     $name .= ".$count";
00464                 }
00465                 $name .= $suffix;
00466             # Update the database
00467                 my $rows = $sh->execute($name, $show->{'chanid'}, $show->{'recstartts'});
00468                 die "Couldn't update basename in database for ".$show->{'basename'}.":  ($q)\n" unless ($rows == 1);
00469                 my $ret = rename $show->{'local_path'}, "$video_dir/$name";
00470             # Rename failed -- Move the database back to how it was (man, do I miss transactions)
00471                 if (!$ret) {
00472                     $rows = $sh->execute($show->{'basename'}, $show->{'chanid'}, $show->{'recstartts'});
00473                     die "Couldn't restore original basename in database for ".$show->{'basename'}.":  ($q)\n" unless ($rows == 1);
00474                 }
00475                 vprint($show->{'basename'}."\t-> $name");
00476             # Rename previews
00477                 opendir DIR, $video_dir;
00478                 foreach my $thumb (grep /\.png$/, readdir DIR) {
00479                     next unless ($thumb =~ /^$show->{'basename'}((?:\.\d+)?(?:\.\d+x\d+(?:x\d+)?)?)\.png$/);
00480                     my $dim = $1;
00481                     $ret = rename "$video_dir/$thumb", "$video_dir/$name$dim.png";
00482                 # If the rename fails, try to delete the preview from the
00483                 # cache (it will automatically be re-created with the
00484                 # proper name, when necessary)
00485                     if (!$ret) {
00486                         unlink "$video_dir/$thumb"
00487                             or vprint("Unable to rename preview image: '$video_dir/$thumb'.");
00488                     }
00489                 }
00490                 closedir DIR;
00491             }
00492         }
00493         exit 0;
00494     }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends