3 #=======================================================================
5 # File ID: 4bec96e4-cc13-11de-a8a7-93dd800a3f5e
7 # Insert timestamp into file names
10 # ©opyleft 2009– Øyvind A. Holm <sunny@sunbase.org>
11 # License: GNU General Public License version 2 or later, see end of
12 # file for legal stuff.
13 #=======================================================================
23 my $std_exif_tag = "DateTimeOriginal";
31 'exif-tag' => $std_exif_tag,
45 $progname =~ s/^.*\/(.*?)$/$1/;
46 our $VERSION = '0.3.0';
48 Getopt
::Long
::Configure
('bundling');
51 'bwf' => \
$Opt{'bwf'},
52 'delete|d' => \
$Opt{'delete'},
53 'dry-run|n' => \
$Opt{'dry-run'},
54 'exif-tag|E=s' => \
$Opt{'exif-tag'},
55 'exif|e' => \
$Opt{'exif'},
56 'force|f' => \
$Opt{'force'},
57 'git|g' => \
$Opt{'git'},
58 'help|h' => \
$Opt{'help'},
59 'local|l' => \
$Opt{'local'},
60 'quiet|q+' => \
$Opt{'quiet'},
61 'replace|r' => \
$Opt{'replace'},
62 'skew|s=i' => \
$Opt{'skew'},
63 'verbose|v+' => \
$Opt{'verbose'},
64 'version' => \
$Opt{'version'},
66 ) || die("$progname: Option error. Use -h for help.\n");
68 $Opt{'verbose'} -= $Opt{'quiet'};
69 if ($Opt{'delete'} && $Opt{'replace'}) {
70 warn("$progname: Cannot mix -d/--delete and -r/--replace options\n");
73 $Opt{'help'} && usage
(0);
74 if ($Opt{'version'}) {
79 my $d = '[\dX]'; # Legal regexp digits, 0-9 or X (unknown)
80 my $r_date = "[12]$d$d$d" . # year
96 die("$progname: Missing filenames. Use -h for help.\n");
99 my $exiftool_version = `exiftool -ver 2>/dev/null`;
100 if (!defined($exiftool_version) || $exiftool_version !~ /^\d+\.\d+/) {
101 printf(STDERR
"$progname: exiftool(1) not found, " .
102 "required by -e/--exif\n");
107 for my $Curr (@ARGV) {
108 msg
(2, "Curr = '$Curr'");
120 warn("$progname: $File: Not a regular file\n");
123 if (!$Opt{'delete'} && !$Opt{'replace'} && numdates
($File) > 0) {
124 warn("$progname: $File: Filename already has date\n");
127 msg
(3, sprintf("mod_date(%s) = '%s'", $File, mod_date
($File)));
129 my $mod_date = $Opt{'exif'} ? exif_date
($File) : mod_date
($File);
130 my $start_date = start_date
($File);
131 return if (!$mod_date);
132 $mod_date += $Opt{'skew'};
133 $start_date += $Opt{'skew'} if ($start_date);
134 my $dates = sprintf("%s%s%s",
135 $start_date ? sec_to_string
($start_date) : "",
136 $start_date ?
"-" : "",
137 sec_to_string
($mod_date),
139 if (length($dates)) {
140 my ($basename, $dirname) = fileparse
($File);
141 my $new_name = $basename;
142 if ($Opt{'replace'}) {
143 $new_name = strip_date_from_filename
($new_name);
145 if ($Opt{'delete'}) {
146 $new_name = strip_date_from_filename
($new_name);
148 $new_name = "$dates.$new_name";
150 $dirname eq "./" && ($dirname = '');
151 $new_name = "$dirname$new_name";
152 if ($new_name eq "$File") {
153 msg
(1, "Filename for $File is unchanged");
156 if ($Opt{'dry-run'}) {
157 print("$progname: '$File' would be renamed to '$new_name'\n");
159 if (-e
$new_name && !$Opt{'force'}) {
160 warn("$progname: $new_name: File already exists, " .
161 "use --force to overwrite\n");
162 } elsif (rename_file
($File, $new_name)) {
163 print("$progname: '$File' renamed to '$new_name'\n");
165 warn("$progname: $File: Cannot rename file to '$new_name': " .
175 my ($oldname, $newname) = @_;
179 $retval = mysystem
('git', 'mv', $oldname, $newname);
182 $retval = rename($oldname, $newname);
194 msg
(0, "Executing \"" . join(' ', @cmd) . "\"...");
195 $retval = system(@cmd);
202 # Return file modification timestamp {{{
205 my @stat_array = stat($File);
206 if (scalar(@stat_array)) {
207 $Retval = $stat_array[9];
209 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst)
211 $Retval = timelocal
($sec, $min, $hour, $mday, $mon, $year);
214 warn("$progname: $File: Cannot stat file: $!\n");
225 if ($str =~ /^$r_date-$r_date/) {
227 } elsif ($str =~ /^$r_date/) {
232 msg
(3, "numdates('$str') returns '$retval'");
237 sub strip_date_from_filename
{
241 $retval =~ s/^20......T......*?Z\.(.*$)/$1/;
242 msg
(3, "strip_date_from_filename('$file') returns '$retval'");
245 } # strip_date_from_filename()
248 # Find start of recording {{{
252 my $bwf_date = bwf_date
($File);
257 msg
(2, "start_date($File) returns '$Retval'");
263 # Find start of recording in Broadcast Wave Format files {{{
264 # This is based on examining .wav files from the Zoom H4n, and it
265 # seems to work there. The file format may vary on other devices.
268 unless (open(InFP
, "<", $File)) {
269 warn("$progname: $File: Cannot open file to look for BWF data: $!\n");
273 my $numread = read(InFP
, $buf, 358);
274 if ($numread != 358) {
275 warn("$progname: $File: Could not read 358 bytes, but continuing: " .
278 if ($buf =~ /^.*(\d\d\d\d)-(\d\d)-(\d\d)(\d\d):(\d\d):(\d\d)$/s) {
279 $Retval = $Opt{'local'} ? timelocal
($6, $5, $4, $3, $2-1, $1)
280 : timegm
($6, $5, $4, $3, $2-1, $1);
283 msg
(2, "bwf_date($File) returns '$Retval'");
293 $retval = get_exif_data
($File, $Opt{'exif-tag'});
294 msg
(2, "exif_date(): \$retval before check = \"$retval\"");
295 if ($retval =~ /^(\d\d\d\d).(\d\d).(\d\d).(\d\d).(\d\d).(\d\d)$/) {
296 $retval = $Opt{'local'} ? timelocal
($6, $5, $4, $3, $2-1, $1)
297 : timegm
($6, $5, $4, $3, $2-1, $1);
302 msg
(1, "$File: No EXIF data found in file");
304 msg
(2, "exif_date() returns \"$retval\"");
311 my ($file, $tag) = @_;
315 if (!open(FromFP
, "exiftool -j \"$file\" |")) {
316 printf(STDERR
"$progname: $file: Cannot open file for read\n");
319 while ($line = <FromFP
>) {
320 if ($line =~ /"$tag"/) {
321 msg
(2, "get_exif_data() found \"$line\"");
322 $line =~ s/^.*?"$tag"\s*:\s*"(.*?)".*/$1/s;
323 msg
(2, "\$line after regexp: \"$line\"");
333 # Convert seconds since 1970 to "yyyymmddThhmmss[.frac]Z" {{{
334 my ($Seconds, $Sep) = @_;
335 length($Seconds) || return('');
336 ($Seconds =~ /^-?(\d*)(\.\d+)?$/) || return(undef);
337 my $Secfrac = ($Seconds =~ /^([\-\d]*)(\.\d+)$/) ?
1.0*$2 : "";
340 defined($Sep) || ($Sep = " ");
341 my @TA = gmtime($Seconds);
342 my($DateString) = sprintf("%04u%02u%02uT%02u%02u%02u%sZ",
343 $TA[5]+1900, $TA[4]+1, $TA[3],
344 $TA[2], $TA[1], $TA[0], $Secfrac);
350 # Print program version {{{
351 print("$progname $VERSION\n");
357 # Send the help message to stdout {{{
360 if ($Opt{'verbose'}) {
366 Insert filemod timestamp into filename, and start of recording if
367 available. At the moment only BWF (Broadcast Wave Format, standard .wav
368 with extra metadata) is supported.
372 No timestamp for start of recording:
373 yyyymmddThhmmssZ.OLDFILENAME
374 With timestamp for start of recording:
375 yyyymmddThhmmssZ-yyyymmddThhmmssZ.OLDFILENAME
377 Usage: $progname [options] file [files [...]]
382 Find start of recording in Broadcast Wave Format files. This is
383 based on examining .wav files from the Zoom H4n, and it seems to
384 work there. The file format may vary on other devices.
386 Delete timestamp from filename. Can not be used with -r/--replace.
388 Use timestamp from EXIF data in the file.
389 -E TAG, --exif-tag TAG
390 Use TAG when creating timestamp from the EXIF data.
391 Default: "$std_exif_tag".
393 If a file with the new name already exists, allow the program to
396 Use git commands when dealing with files. For example, execute the
397 command "git mv oldname newname" when renaming files.
399 Timestamps are stored in local time, convert to UTC before renaming
400 the files. If this option is not specified, timestamps in EXIF or
401 modtime are expected to be in UTC.
403 Don’t rename files, but report what would happen.
407 Be more quiet. Can be repeated to increase silence.
409 Replace date in filename with new value. Can not be used with
412 Adjust clock skew by adding X seconds to the timestamp. A negative
413 integer can also be specified.
415 Increase level of verbosity. Can be repeated.
417 Print version information.
425 # Print a status message to stderr based on verbosity level {{{
426 my ($verbose_level, $Txt) = @_;
428 if ($Opt{'verbose'} >= $verbose_level) {
429 print(STDERR
"$progname: $Txt\n");
437 # This program is free software; you can redistribute it and/or modify
438 # it under the terms of the GNU General Public License as published by
439 # the Free Software Foundation; either version 2 of the License, or (at
440 # your option) any later version.
442 # This program is distributed in the hope that it will be useful, but
443 # WITHOUT ANY WARRANTY; without even the implied warranty of
444 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
445 # See the GNU General Public License for more details.
447 # You should have received a copy of the GNU General Public License
448 # along with this program.
449 # If not, see L<http://www.gnu.org/licenses/>.
451 # vim: set fenc=UTF-8 ft=perl fdm=marker ts=4 sw=4 sts=4 et fo+=w :