9 use Ogg::Vorbis::Header;
14 arename.pl - automatically rename audio files by tagging information
18 arename.pl [OPTION(s)] FILE(s)...
20 =head1 OPTIONS AND ARGUMENTS
30 Overwrite files if needed.
34 Display a short help text.
38 Display version infomation.
42 Enable verbose output.
44 =item B<-p> E<lt>prefixE<gt>
46 Define a prefix for destination files.
48 =item B<-T> E<lt>templateE<gt>
50 Define a compilation template.
52 =item B<-t> E<lt>templateE<gt>
54 Define a generic template.
58 Input files, that are subject for renaming.
64 B<arename.pl> is a tool that is able to rename audio files by looking at
65 a file's tagging information, from which it will assemble a consistent
66 destination file name. The format of that filename is configurable for the
67 user by the use of template strings.
69 B<arename.pl> currently supports two widely used audio formats, namely
70 MPEG Layer3 and ogg vorbis. The format, that B<arename.pl> will assume
71 for each input file is determined by the file's filename-extension
72 (I<.mp3> vs. I<.ogg>). The extension check is case-insensitive.
74 By default, B<arename.pl> will refuse to overwrite destination files,
75 if the file in question already exists. You can force overwriting by
76 supplying the B<-f> option.
80 B<arename.pl> uses up to two configuration files. As for most programs,
81 the script will try to read a configuration file, that is located in the
82 user's I<home directory>. In addition to that, it will try to load I<local>
83 configuration files, if it finds appropriately named files in the
90 per-user global configuration file.
92 =item B<./.arename.local>
94 per-directory local configuration file.
100 The format of the aforementioned files is pretty simple.
101 It is parsed line by line. Empty lines, lines only containing whitespace
102 and lines, whose first non whitespace character is a hash character (I<#>)
105 Each line consists of one or two parts. If there are two parts,
106 they are separated by whitespace. The first part of the line will be used
107 as the identifier of a setting (eg. I<verbose>). The second part (read: the
108 rest of the line) is used as the value of the setting. (No quoting, or whatsoever
111 If a line consists of only one part, that means the setting is switched on.
113 =head2 Configuration file example
115 # switch on verbosity
118 # the author is crazy! use a sane template by default. :-)
119 template &artist - &album (&year) - &tracknumber. &tracktitle
123 The following settings are supported in all configuration files:
127 =item B<comp_template>
129 Defines a template to use with files that provide a compilation tag
130 (for 'various artist' CDs, for example). This setting can still be
131 overwritten by the B<-T> command line option. (default value:
132 I<va/&album/&tracknumber - &artist - &tracktitle>)
134 =item B<default_year>
136 Defines a default year, for files, that lack this information.
137 (default value: I<undefined>)
141 Defines a prefix for destination files. This setting can still be
142 overwritten by the B<-p> command line option. (default value: I<.>)
146 Tagging information strings may contain slashes, which is a pretty bad
147 idea on most filesystems. Therefore, you can define a string, that replaces
148 slashes with the value of this setting. (default value: I<_>)
152 Defines a template to use with files that do not provide a compilation tag
153 (or where the compilation tag and the artist tag are exactly the same).
154 This setting can still be overwritten by the B<-T> command line option.
155 (default value: I<&artist[1]/&artist/&album/&tracknumber - &tracktitle>)
159 This defines the width, to which the tracknumber field is padded with zeros
160 on the left. (default value: I<2>)
164 Switches on verbosity by default. (default value: I<off>)
168 =head1 TEMPLATE FORMAT
170 B<arename.pl>'s templates are quite simple, yet powerful.
172 At simplest, a template is just a fixes character string. However, that would
173 not be exactly useful. So, the script is able to expand certain expressions
174 with information gathered from the file's tagging information.
176 The expressions can have two slightly different forms:
180 =item B<&>I<identifier>
184 =item B<&>I<identifier>B<[>I<length>B<]>
186 The "complex" form. The I<length> argument in square brackets defines the
187 maximum length, to which the expression should be expanded.
191 =head2 Available expression identifiers
193 The data, that is expanded is derived from tagging information in
194 the audio files. For I<.ogg> files, the tag checking B<arename.pl> does
195 is case insensitive and the first matching tag will be used.
209 For I<.ogg> this is filled with information found in the 'albumartist' tag.
210 For I<.mp3> this is filled with information from the id3v2 TPE2 frame.
211 If the mp3 file only provides a id3v1 tag, this is not supported.
215 The number of the position of the track on the disc. Obviously. However, this
216 can be in the form of '12' or '12/23'. In the second form, only the part left
217 of the slash is used. The tracknumber is a little special, as you can defined
218 to what width it should be padded with zeros on the left (see I<tnpad> setting
219 in L<arename(1)/SETTINGS>).
227 Year (id3v1), TYER (id3v2) or DATE tag (.ogg).
233 L<Ogg::Vorbis::Header(3)> and L<MP3::Tag(3)>.
237 Frank Terbeck E<lt>ft@bewatermyfriend.orgE<gt>,
244 Frank Terbeck <ft@bewatermyfriend.org>, All rights reserved.
246 Redistribution and use in source and binary forms, with or without
247 modification, are permitted provided that the following conditions
250 1. Redistributions of source code must retain the above
251 copyright notice, this list of conditions and the following
253 2. Redistributions in binary form must reproduce the above
254 copyright notice, this list of conditions and the following
255 disclaimer in the documentation and/or other materials
256 provided with the distribution.
258 THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
259 WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
260 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
261 DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS OF THE
262 PROJECT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
263 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
264 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
265 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
266 OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
267 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
268 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
274 %defaults, %methods, %opts,
275 $dryrun, $comp_template, $force, $prefix,
276 $sepreplace, $template, $tnpad, $verbose
278 my ($NAME, $VERSION) = ( 'arename.pl', 'v0.3' );
280 sub apply_defaults { #{{{
283 foreach my $key (keys %defaults) {
284 if (!defined $datref->{$key}) {
286 print " -!- Setting ($key) to \"$defaults{$key}\".\n";
288 $datref->{$key} = $defaults{$key};
294 my ($file, $datref, $ext) = @_;
297 apply_defaults($datref);
300 print " -!- Artist : \"" .
301 (defined $datref->{artist} ? $datref->{artist} : "-.-")
303 print " -!- Compilation: \"" .
304 (defined $datref->{compilation} ? $datref->{compilation} : "-.-")
306 print " -!- Album : \"" .
307 (defined $datref->{album} ? $datref->{album} : "-.-")
309 print " -!- Tracktitle : \"" .
310 (defined $datref->{tracktitle} ? $datref->{tracktitle} : "-.-")
312 print " -!- Tracknumber: \"" .
313 (defined $datref->{tracknumber} ? $datref->{tracknumber} : "-.-")
315 print " -!- Year : \"" .
316 (defined $datref->{year} ? $datref->{year} : "-.-")
320 if (defined $datref->{compilation}
321 && $datref->{compilation} ne $datref->{artist}) {
328 $newname = expand_template($t, $datref);
329 if (!defined $newname) {
333 $newname = $prefix . '/' . $newname . '.' . $ext;
335 if (file_eq($newname, $file)) {
336 print " -!- ($file)\n would stay the way it is, skipping.\n";
340 if (-e $newname && !$force) {
341 print " -!- ($newname) exists.\n use '-f' to force overwriting.\n";
345 ensure_dir(dirname($newname));
347 print " -!- mv '$file' \\\n" .
351 xrename($file, $newname);
355 sub ensure_dir { #{{{
356 # think: mkdir -p /foo/bar/baz
364 if ($wantdir =~ '^/') {
370 @parts = split(/\//, $wantdir);
371 foreach my $part (@parts) {
381 : $sofar . "/" . $part
386 if ($dryrun || $verbose) {
387 print " -!- mkdir \"$sofar\"\n";
390 mkdir($sofar) or die " -!- Could not mkdir($sofar).\n" .
397 sub expand_template { #{{{
398 my ($template, $datref) = @_;
408 foreach my $tag (@tags) {
411 while ($template =~ m/&$tag(\[(\d+)\]|)/) {
413 if (defined $2) { $len = $2; }
415 if (!defined $datref->{$tag} || $datref->{$tag} eq '') {
416 warn " -!- $tag not defined, but required by template. Giving up.\n";
421 $token = substr($datref->{$tag}, 0, $len);
423 if ($tag eq 'tracknumber') {
425 if ($datref->{$tag} =~ m/^([^\/]*)\/.*$/) {
428 $val = $datref->{$tag};
430 $token = sprintf "%0" . $tnpad . "d", $val;
432 $token = $datref->{$tag};
435 if ($token =~ m!/!) {
437 print " -!- Found directory seperator in token.\n";
438 print " -!- Replacing with \"$sepreplace\".\n";
440 $token =~ s!/!$sepreplace!g;
442 $template =~ s/&$tag(\[(\d+)\]|)/$token/;
453 if (!-e $f0 || !-e $f1) {
454 # one of the two doesn't even exist, can't be the same then.
458 @stat0 = stat $f0 or die "Could not stat($f0): $!\n";
459 @stat1 = stat $f1 or die "Could not stat($f1): $!\n";
461 if ($stat0[0] == $stat1[0] && $stat0[1] == $stat1[1]) {
462 # device and inode are the same. same file.
469 sub process_mp3 { #{{{
471 my ($mp3, %data, $info);
473 $mp3 = MP3::Tag->new($file);
476 print " -!- Failed to open \"$file\".\n -!- Reason: $!\n";
482 if (!exists $mp3->{ID3v1} && !exists $mp3->{ID3v2}) {
483 print " -!- No tag found. Ignoring.\n";
488 if (exists $mp3->{ID3v2}) {
489 ($data{artist}, $info) = $mp3->{ID3v2}->get_frame("TPE1");
490 ($data{compilation}, $info) = $mp3->{ID3v2}->get_frame("TPE2");
491 ($data{album}, $info) = $mp3->{ID3v2}->get_frame("TALB");
492 ($data{tracktitle}, $info) = $mp3->{ID3v2}->get_frame("TIT2");
493 ($data{tracknumber}, $info) = $mp3->{ID3v2}->get_frame("TRCK");
494 ($data{year}, $info) = $mp3->{ID3v2}->get_frame("TYER");
495 } elsif (exists $mp3->{ID3v1}) {
496 print " -!- Only found ID3v1 tag.\n";
497 $data{artist} = $mp3->{ID3v1}->artist;
498 $data{album} = $mp3->{ID3v1}->album;
499 $data{tracktitle} = $mp3->{ID3v1}->title;
500 $data{tracknumber} = $mp3->{ID3v1}->track;
501 $data{year} = $mp3->{ID3v1}->year;
506 arename($file, \%data, 'mp3');
509 sub process_ogg { #{{{
511 my ($ogg, %data, @tags);
513 $ogg = Ogg::Vorbis::Header->load($file);
516 print " -!- Failed to open \"$file\".\n -!- Reason: $!\n";
520 @tags = $ogg->comment_tags;
522 foreach my $tag (@tags) {
523 my ($realtag, $value);
525 $tag =~ m/^ALBUM$/i ||
526 $tag =~ m/^ARTIST$/i ||
527 $tag =~ m/^TITLE$/i ||
528 $tag =~ m/^TRACKNUMBER$/i ||
529 $tag =~ m/^DATE$/i ||
530 $tag =~ m/^ALBUMARTIST$/i
533 $value = join(' ', $ogg->comment($tag));
534 if ($tag =~ m/^ALBUM$/i) {
536 } elsif ($tag =~ m/^ARTIST$/i) {
538 } elsif ($tag =~ m/^TITLE$/i) {
539 $realtag = 'tracktitle';
540 } elsif ($tag =~ m/^TRACKNUMBER$/i) {
541 $realtag = 'tracknumber';
542 } elsif ($tag =~ m/^DATE$/i) {
544 } elsif ($tag =~ m/^ALBUMARTIST$/i) {
545 $realtag = 'compilation';
547 die "This should not happen. Report this BUG. ($tag, $value)";
550 if (!defined $data{$realtag}) {
551 $data{$realtag} = $value;
555 arename($file, \%data, 'ogg');
558 sub process_warn { #{{{
561 warn " -!- No method for handling \"$file\".\n";
565 my ($file, $desc) = @_;
570 if (!open($fh, "<$file")) {
571 warn "Failed to read $desc ($file).\n -!- Reason: $!\n";
575 print "Reading \"$file\"...\n";
577 while (my $line = <$fh>) {
581 if ($line =~ m/^\s*#/ || $line =~ m/^\s*$/) {
586 my ($key,$val) = split(/\s+/, $line, 2);
588 if ($key eq 'template') {
590 } elsif ($key eq 'comp_template') {
591 $comp_template = $val;
592 } elsif ($key eq 'default_year') {
593 $defaults{year} = $val;
594 } elsif ($key eq 'sepreplace') {
595 $sepreplace = (defined $val ? $val : "");
596 } elsif ($key eq 'tnpad') {
598 } elsif ($key eq 'verbose') {
600 } elsif ($key eq 'prefix') {
603 warn "$file,$lnum: invalid line '$line'.\n";
611 print " -!- Read $desc.\n -!- $count valid items.\n";
616 # a rename() replacement, that implements renames across
617 # filesystems via File::copy() + unlink().
618 # This assumes, that source and destination directory are
619 # there, because it stat()s them, to check if it can use
621 my ($src, $dest) = @_;
622 my (@stat0, @stat1, $d0, $d1, $cause);
625 $d1 = dirname($dest);
626 @stat0 = stat $d0 or die "Could not stat($d0): $!\n";
627 @stat1 = stat $d1 or die "Could not stat($d1): $!\n";
629 if ($stat0[0] == $stat1[0]) {
631 rename $src, $dest or goto err;
634 copy($src, $dest) or goto err;
636 unlink $src or goto dir;
642 die " -!- Could not rename($src, $dest);\n" .
643 " -!- Reason: $cause(): $!\n";
651 if (!getopts('dfhVvp:T:t:', \%opts)) {
652 if (exists $opts{t} && !defined $opts{t}) {
653 die " -t *requires* a string argument!\n";
654 } elsif (exists $opts{T} && !defined $opts{T}) {
655 die " -T *requires* a string argument!\n";
656 } elsif (exists $opts{p} && !defined $opts{p}) {
657 die " -p *requires* a string argument!\n";
659 die " Try $NAME -h\n";
664 if (defined $opts{h}) {
665 print " Usage:\n $NAME [-d,-f,-h,-V,-v,-p <prefix>,-[Tt] <template>] FILE(s)...\n\n";
666 print " -d Go into dryrun mode.\n";
667 print " -f Overwrite files if needed.\n";
668 print " -h Display this help text.\n";
669 print " -V Display version infomation.\n";
670 print " -v Enable verbose output.\n";
671 print " -p <prefix> Define a prefix for destination files.\n";
672 print " -T <template> Define a compilation template.\n";
673 print " -t <template> Define a generic template.\n";
678 if (defined $opts{V}) {
679 print " $NAME $VERSION\n";
692 $comp_template = "va/&album/&tracknumber - &artist - &tracktitle";
693 $template = "&artist[1]/&artist/&album/&tracknumber - &tracktitle";
696 # reading config file(s) {{{
698 my $rc = $ENV{HOME} . "/.arenamerc";
699 my $retval = rcload($rc, "arename.pl configuration");
701 die "Error(s) in \"$rc\". Aborting.\n";
702 } elsif ($retval > 0) {
703 warn "Error opening configuration; using defaults.\n";
706 if (-r "./.arename.local") {
707 $rc = "./.arename.local";
708 $retval = rcload($rc, "local configuration");
710 die "Error(s) in \"$rc\". Aborting.\n";
711 } elsif ($retval > 0) {
712 warn "Error opening local configuration.\n";
719 # let cmd line options overwrite {{{
722 die "No input files. See: $NAME -h\n";
725 if (defined $opts{f}) {
729 if (defined $opts{p}) {
733 if (defined $opts{t}) {
734 $template = $opts{t};
737 if (defined $opts{T}) {
738 $comp_template = $opts{T};
741 if (defined $opts{d}) {
745 if (defined $opts{v}) {
752 # process what's left on the commandline aka. main() {{{
754 '.mp3$' => \&process_mp3,
755 '.ogg$' => \&process_ogg
759 print "+++ We are on a dry run!\n";
763 print "+++ Running verbose.\n";
766 if ($dryrun || $verbose) {
770 foreach my $file (@ARGV) {
772 print "Processing: $file\n";
774 warn " -!- Refusing to handle symbolic links ($file).\n";
778 warn " -!- Can't read \"$file\": $!\n";
782 foreach my $method (sort keys %methods) {
783 if ($file =~ m!$method!i) {
784 $methods{$method}->($file);