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:
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/[\- :]
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 }