#!/usr/bin/perl # vim: ts=2:sw=2:expandtab:cindent use 5; use strict; use Getopt::Long; use Encode; use Encode::Guess qw(utf8); use constant ID3FLG_RMVBYBDYCHG => 64; use constant ID3FLG_RMVBYTAGCHG => 128; my %tags; my @order = qw(TPE1 TCON TALB TPOS TRCK TIT2 TLEN TSRC); my %hdflag = (TLEN => [ID3FLG_RMVBYBDYCHG, 0]); sub help() { print < \&help, 'create|c!' => \$create, 'encode|e=s' => \$encode, 'artist|a=s' => \$tags{TPE1}, 'genre|g=s' => \$tags{TCON}, 'album|A=s' => \$tags{TALB}, 'disc|d=s' => \$tags{TPOS}, 'track|n=s' => \$tags{TRCK}, 'title|t=s' => \$tags{TIT2}, 'length|l=s' => \$tags{TLEN}, 'isrc|i=s' => \$tags{TSRC}, ) || help; help unless $ARGV[0]; sub toencstr($) { local $SIG{__WARN__} = sub {}; my $str = shift; return undef unless $str; if ($str =~ /[[:^print:]]/) { if ($encode eq 'utf16le') { Encode::from_to($str, 'guess', 'UTF-16-LE'); $str = chr(1) . "\xff\xfe" . $str; } elsif ($encode eq 'utf16be') { Encode::from_to($str, 'guess', 'UTF-16'); $str = chr(1) . $str; } elsif ($encode eq 'utf8') { Encode::from_to($str, 'guess', 'utf8'); $str = chr(3) . $str; } else { die 'No suitable encoding for non ascii'; } } else { $str = chr(0) . $str; } return $str } sub tosyncsafeint($) { my $val = shift; my $str = ''; for (0 .. 3) { $str = chr($val % 128) . $str; $val >>= 7; } return $str; } sub fromsyncsafeint($) { my $str = shift; my $val = 0; $val = ($val << 7) + $_ foreach unpack 'cccc', $str; return $val; } sub totagframe($@) { my $tagname = shift; my $tagvalue = shift; my $hdflag = shift || [0, 0]; my $frame; $frame .= $tagname; $frame .= tosyncsafeint length $tagvalue; $frame .= chr($hdflag->[0]) . chr($hdflag->[1]); $frame .= $tagvalue; return $frame; } sub getfiletag($) { my $file = shift; return 0 unless -r $file; open FILE, '<', $file; binmode FILE; my $buf; read FILE, $buf, 3; if ($buf ne 'ID3') { close FILE; return 0 if $buf ne 'ID3'; } seek FILE, 6, 0; read FILE, $buf, 4; my $tagsize = fromsyncsafeint $buf; if ($create) { close FILE; return $tagsize; } my $tags = {}; read FILE, $buf, $tagsize; close FILE; while (ord(substr $buf, 0, 1)) { my $tagname = substr $buf, 0, 4; my $tagsize = fromsyncsafeint substr $buf, 4, 4; my $tagflag = [unpack 'cc', substr $buf, 8, 2]; my $tagbody = substr $buf, 10, $tagsize; substr $buf, 0, 10 + $tagsize, ''; $tags->{$tagname} = [$tagbody, $tagflag] unless ($tagflag->[0] & ID3FLG_RMVBYTAGCHG); } return $tagsize, $tags; } sub calctagsize($$$) { my $tagbody = shift; my $file = shift; my $filetagsize = shift; my $taglen = length $tagbody; return $taglen unless -e $file; return $filetagsize if $taglen + 10 <= $filetagsize; $filetagsize = 2048 - (((-s $file) + 10) % 2048); $filetagsize += 2048 while $taglen + 10 > $filetagsize; return $filetagsize; } my $file = $ARGV[0]; my ($filetagsize, $oldtag) = getfiletag $file; foreach (keys %tags) { if ($tags{$_}) { $tags{$_} = [toencstr($tags{$_}), $hdflag{$_}]; } else { delete $tags{$_}; } } $tags{$_} ||= $oldtag->{$_} foreach keys %$oldtag; my $tagbody; foreach (@order, keys %tags) { if ($tags{$_}) { $tagbody .= totagframe $_, @{$tags{$_}}; $tags{$_} = undef; } } my $taghead = 'ID3' . chr(3) . chr(0) . chr(0); my $tagsize = calctagsize $tagbody, $file, $filetagsize; $taghead .= tosyncsafeint $tagsize; $tagbody .= chr(0) x ($tagsize - length $tagbody) if $tagsize > length $tagbody; my $tagdata = $taghead . $tagbody; if ($filetagsize == length $tagbody) { open FILE, '+<', $file; binmode FILE; seek FILE, 0, 0; print FILE $tagdata; close FILE; } else { open TO, '>', $file . '.tmp'; binmode TO; print TO $tagdata; open FROM, '<', $file; binmode FROM; seek FROM, $filetagsize + 10, 0 if $filetagsize; print TO $_ while ; close TO; close FROM; rename $file . '.tmp', $file; }