#!/usr/local/bin/perl -w
# $Id: mp3id,v 1.7 2003/12/12 06:30:10 ianb Exp $
# Ian Beckwith <ianb@nessie.mcc.ac.uk>
# 20031107

use strict;
use MP3::Tag;
use Term::ReadLine;

######################################################################
# Declarations of globals (far too many, but at least they are
# documented below)

# constants
use vars qw($BOTH $V1 $V2 $NEITHER);
use vars qw($SILENT $TERSE $VERBOSE);
use vars qw($REPLACE $ADD);
use vars qw($DELETE $NODELETE);
use vars qw($DEL_NONE $DEL_ALL $DEL_PIC $DEL_EXT $DEL_BINARY $DEL_EMPTY
			$DEL_FRAME $DEL_TRACKNUM $DEL_ARTIST $DEL_ALBUM $DEL_TRACK 
			$DEL_IMPORT $DEL_YEAR $DEL_GENRE $DEL_COMMENT);
use vars qw($EXTRACT_NONE $EXTRACT_ALL $EXTRACT_PIC 
			$EXTRACT_BINARY $EXTRACT_FRAME);
use vars qw($PROMPT_NONE $PROMPT_ALL $PROMPT_PIC $PROMPT_FRAME
			$PROMPT_TRACKNUM $PROMPT_ARTIST $PROMPT_ALBUM $PROMPT_TRACK
			$PROMPT_YEAR $PROMPT_GENRE $PROMPT_COMMENT);
use vars qw($SYNC_NONE $SYNC_ONE $SYNC_TWO $SYNC_BEST 
			$SYNC_COMP $SYNC_COMPSTRICT);

# vars
use vars qw($me $ver $volume $volumeset $extended $raw $doneopts
			$donesomething $term);
use vars qw($delete @delete @importedfiles $importfile $extract
			@extract $prompt $sync %newframes %newpics);
use vars qw($newtracknum $newartist $newalbum $newtrack $newgenre
			$newyear $newcomment);

######################################################################
# Initialisation 

# Constants

# enums
($BOTH,$V1,$V2,$NEITHER)=(0,1,2,3);
($SILENT,$TERSE,$VERBOSE)=(0,1,2);
($REPLACE,$ADD)=(0,1);
($DELETE,$NODELETE)=(0,1);

# flags
($DEL_NONE,$DEL_ALL,$DEL_PIC,$DEL_EXT,$DEL_BINARY,$DEL_EMPTY,$DEL_FRAME,
 $DEL_TRACKNUM,$DEL_ARTIST,$DEL_ALBUM,$DEL_TRACK,$DEL_IMPORT,
 $DEL_YEAR,$DEL_GENRE,$DEL_COMMENT)=
	(0,1,2,4,8,16,32,64,128,256,512,1024,2048,4096,8192);
($EXTRACT_NONE,$EXTRACT_ALL,$EXTRACT_PIC,$EXTRACT_BINARY,
 $EXTRACT_FRAME)=(0,1,2,4,8);
($PROMPT_NONE,$PROMPT_ALL,$PROMPT_PIC,$PROMPT_FRAME,$PROMPT_TRACKNUM,
 $PROMPT_ARTIST,$PROMPT_ALBUM,$PROMPT_TRACK,$PROMPT_YEAR,$PROMPT_GENRE,
 $PROMPT_COMMENT)=(0,1,2,4,8,16,32,64,128,256,512);
($SYNC_NONE,$SYNC_ONE,$SYNC_TWO,$SYNC_BEST,$SYNC_COMP,
 $SYNC_COMPSTRICT)=(0,1,2,4,8,16);

# Globals (far too many)

# Program basename for messages
$me=($0=~/(?:.*\/)?(.*)/)[0];

# ID3 version to use
$ver=$BOTH;

# How verbose to be
$volume=$VERBOSE;

# Whether verbosity has been set by user
$volumeset=0;

# show extended tags
$extended=0;

# show all id3v2 tags as extended
$raw=0;

# "--" cmdline opt
$doneopts=0;

# unless passed filenames or [-Ff] => usage()
$donesomething=0;

# term object for prompting
$term=new Term::ReadLine "$me";

# flags
$delete=$DEL_NONE;
$extract=$EXTRACT_NONE;
$prompt=$PROMPT_NONE;
$sync=$SYNC_NONE; # copy/compare

# main -i id3 import file
$importfile=undef;

# frames to delete
@delete=();
# frames to extract
@extract=();
# frames to add
%newframes=();
# pictures to add
%newpics=();

# new versions of basic tags supplied by -[ALNTYGC]
($newtracknum,$newartist,$newalbum,$newtrack,$newgenre,$newyear,$newcomment)=
	(undef,undef,undef,undef,undef,undef,undef);

######################################################################
# Statics (maintain state between function calls)

# currrent message for tprint()
my $curmsg="";

# used by initmime/exttomime/mimetoext
my $mimetoext=undef;
my $exttomime=undef;

# default for prompting
my $lasttracknum="00";

# used by printwrap
my $printwrapstr="";


######################################################################
# here be main()
{
	my @importedfiles=();
	while($_=shift)
	{
		my $filepath=$_;
		my $file=($filepath=~/(?:.*\/)?(.*)/)[0];
		
		if((/^-(.+)/) && (!$doneopts))
		{
			do_parseopts($1,\@ARGV);
			next;
		}
		$donesomething=1;
		
		my $mp3=MP3::Tag->new($filepath);
		
		unless(defined($mp3))
		{
			warn("$me: cannot load $file\n");
			next;
		}
		$mp3->get_tags;
		
		# delete/replace/extract/print etc. could probably all
		# be done in one big loop, but it would be even messier
		
		if($delete != $DEL_NONE)
		{
			do_delete($mp3,$file);
		}
		
		if(defined($importfile))
		{
			my @files=do_import($mp3,$file);
			if(@files)
			{
				push(@importedfiles,@files);
			}
			# assume they want to import same data to every file, so dont:
			#$importfile=undef;
		}
		
		if(defined($newtracknum) || defined($newartist) || defined($newalbum) || 
		   defined($newtrack)    || defined($newgenre)  || defined($newyear)  ||
		   defined($newcomment))
		{
			do_basicreplace($mp3);
		}
		
		if(keys(%newframes) || keys(%newpics))
		{
			do_addreplaceframes($mp3);
		}   
		
		if($prompt != $PROMPT_NONE)
		{
			do_prompt($file,$mp3);
		}
		
		if($extract != $EXTRACT_NONE)
		{
			do_extract($mp3,$filepath);
		}
		
		my $tagsdeleted=$NEITHER;
		if($delete & $DEL_EMPTY)
		{
			my $tagsdeleted=do_delempty($mp3);
			if(($tagsdeleted == $ver) || $tagsdeleted == $BOTH)
			{
				next;
			}
		}
		
		# aka copy & compare (-c)
		if($sync != $SYNC_NONE)
		{
			do_sync($file,$mp3);
		}
		
		unless($volume==$SILENT)
		{
			do_print($mp3,$file);
		}

	} # end while
	
	if(($delete & $DEL_IMPORT) && @importedfiles)
	{
		# get rid of dups
		my %files=map { $_=>1 } @importedfiles;
		@importedfiles=sort(keys(%files));
		foreach my $f (@importedfiles)
		{
			unless(unlink($f)) { warn("$me: could not remove $f: $!\n"); }
		}
	}
	unless($donesomething)
	{
		usage();
	}
	
} # end 'main' scope

######################################################################

# All do_* functions implement top level actions
# and are called from the main code, with the exception
# of do_printframetypes, which is called from do_parseopts

# Handle parsing of command line options
# Too much messing with globals here
sub do_parseopts
{
	my($rest,$argv)=@_;
	my $opt;
	while((($opt,$rest)=popchar($rest)) && (defined($opt)))
	{
		if   ($opt eq "1") { $ver=$V1 ; next;}
		elsif($opt eq "2") { $ver=$V2 ; next;}
		elsif($opt eq "q") { $volume=$SILENT;  $volumeset=1; next; }
		elsif($opt eq "t") { $volume=$TERSE;   $volumeset=1; next; }
		elsif($opt eq "v") { $volume=$VERBOSE; $volumeset=1; next; }
		elsif($opt eq "x") { $extended=1; next; }
		elsif($opt eq "X") { $extended=1;$raw=1; next; }
		elsif($opt =~ /([fF])/)
		{
			my $verbose=0;
			if($1 eq "F") { $verbose=1; }
			$donesomething=1;
			do_printframetypes($verbose);
			next;
		}
		elsif($opt eq "d")
		{
			unless(defined($rest)) { usage(); }
			unless($volumeset) { $volume=$SILENT; }
			my @delargs=split(//,$rest);
			$rest=undef;
			while(my $delarg=shift(@delargs))
			{
				if   ($delarg eq "a") { $delete |= $DEL_ALL; }
				elsif($delarg eq "x") { $delete |= $DEL_EXT; }
				elsif($delarg eq "b") { $delete |= $DEL_BINARY; }
				elsif($delarg eq "p") { $delete |= $DEL_PIC; }
				elsif($delarg eq "N") { $delete |= $DEL_TRACKNUM; }
				elsif($delarg eq "A") { $delete |= $DEL_ARTIST; }
				elsif($delarg eq "L") { $delete |= $DEL_ALBUM; }
				elsif($delarg eq "T") { $delete |= $DEL_TRACK; }
				elsif($delarg eq "G") { $delete |= $DEL_GENRE; }
				elsif($delarg eq "Y") { $delete |= $DEL_YEAR; }
				elsif($delarg eq "C") { $delete |= $DEL_COMMENT; }
				elsif($delarg eq "z") { $delete |= $DEL_EMPTY; }
				elsif($delarg eq "i") { $delete |= $DEL_IMPORT; }
				elsif($delarg eq "f") {
					$delete |= $DEL_FRAME;
					if(defined($rest) && length($rest)) { push(@delete,$rest); $rest=undef; }
					else { push(@delete,shift(@$argv));  }
					unless($volumeset) { $volume=$SILENT; }
				}
				else { usage(); }
			}
			next;
		}
		elsif($opt eq "e")
		{
			unless(defined($rest)) { usage(); }
			unless($volumeset) { $volume=$SILENT; }
			my @exargs=split(//,$rest);
			$rest=undef;
			while(my $exarg=shift(@exargs))
			{
				if   ($exarg eq "a") { $extract |= $EXTRACT_ALL; }
				elsif($exarg eq "b") { $extract |= $EXTRACT_BINARY; }
				elsif($exarg eq "p") { $extract |= $EXTRACT_PIC; }
				elsif($exarg eq "f") {
					$extract |= $EXTRACT_FRAME;
					if(defined($rest) && length($rest)) { push(@extract,uc($rest)); $rest=undef; }
					else { push(@extract,uc(shift(@$argv)));  }
					unless($volumeset) { $volume=$SILENT; }
				}
				else { usage(); }
			}
			next;
		}
		elsif($opt eq "p")
		{
			unless(defined($rest)) { usage(); }
			unless($volumeset) { $volume=$SILENT; }
			my @promptargs=split(//,$rest);
			$rest=undef;
			while(my $arg=shift(@promptargs))
			{
				if   ($arg eq "a") { $prompt |= $PROMPT_ALL; }
				elsif($arg eq "A") { $prompt |= $PROMPT_ARTIST; }
				elsif($arg eq "L") { $prompt |= $PROMPT_ALBUM; }
				elsif($arg eq "T") { $prompt |= $PROMPT_TRACK; }
				elsif($arg eq "N") { $prompt |= $PROMPT_TRACKNUM; }
				elsif($arg eq "G") { $prompt |= $PROMPT_GENRE; }
				elsif($arg eq "Y") { $prompt |= $PROMPT_YEAR; }
				elsif($arg eq "C") { $prompt |= $PROMPT_COMMENT; }
				elsif($arg =~ /([Pp])/) {
					$prompt |= $PROMPT_PIC;
					unless($volumeset) { $volume=$SILENT; }
				}
				elsif($arg =~ /([Ff])/) {
					$prompt |= $PROMPT_FRAME;
					unless($volumeset) { $volume=$SILENT; }
				}
				else { usage(); }
			}
			next;
		}
		elsif($opt eq "c")
		{
			unless(defined($rest)) { usage(); }
			unless($volumeset) { $volume=$SILENT; }
			my @syncargs=split(//,$rest);
			$rest=undef;
			while(my $arg=shift(@syncargs))
			{
				if   ($arg eq "1") { $sync |= $SYNC_ONE; }
				elsif($arg eq "2") { $sync |= $SYNC_TWO; }
				elsif($arg eq "b") { $sync |= $SYNC_BEST; }
				elsif($arg eq "c") { $sync |= $SYNC_COMP; }
				elsif($arg eq "C") { $sync |= $SYNC_COMPSTRICT; }
				else { usage(); }
			}
			next;
		}
		elsif($opt =~ /([ar])/)
		{
			my $actioncmd=$1;
			unless(defined($rest)) { usage(); }
			my $dest=substr($rest,0,1);
			if($dest !~ /([fp])/) { usage(); }

			if(length($rest)>1) { $rest=substr($rest,1); }
			else { $rest=""; }

			my ($data,$action);
			if(length($rest)) { $data=$rest; $rest=undef; }
			else { $data=shift(@$argv); }
			unless(defined($data)) { usage(); }

			if   ($actioncmd eq "a") { $action=$ADD; }
			else { $action=$REPLACE; }

			if($dest eq "f") { $newframes{$data}=$action; }
			else { $newpics{$data}=$action; }

			unless($volumeset) { $volume=$SILENT; }
			return;
		}
		elsif($opt =~ /[iI]/)
		{
			if(defined($rest) && length($rest)) { $importfile=$rest; $rest=undef; }
			else { $importfile=shift(@$argv);  }
			unless($volumeset) { $volume=$SILENT; }
			if($opt eq "I") { $delete |= $DEL_IMPORT; }
			return;
		}
		elsif($opt eq "N")
		{
			if(defined($rest) && length($rest)) { $newtracknum=$rest; $rest=undef; }
			else { $newtracknum=shift(@$argv); }
			unless($volumeset) { $volume=$SILENT; }
			return;
		}
		elsif($opt eq "A")
		{
			if(defined($rest) && length($rest)) { $newartist=$rest; $rest=undef; }
			else { $newartist=shift(@$argv); }
			unless($volumeset) { $volume=$SILENT; } 
			return;
		}
		elsif($opt eq "L") {
			if(defined($rest) && length($rest)) { $newalbum=$rest; $rest=undef; }
			else { $newalbum=shift(@$argv); }
			unless($volumeset) { $volume=$SILENT; } 
			return;
		}
		elsif($opt eq "T") {
			if(defined($rest) && length($rest)) { $newtrack=$rest; $rest=undef; }
			else { $newtrack=shift(@$argv); }
			unless($volumeset) { $volume=$SILENT; } 
			return;
		}
		elsif($opt eq "G") {
			if(defined($rest) && length($rest)) { $newgenre=$rest; $rest=undef; }
			else { $newgenre=shift(@$argv); }
			unless($volumeset) { $volume=$SILENT; } 
			return;
		}
		elsif($opt eq "Y") {
			if(defined($rest) && length($rest)) { $newyear=$rest; $rest=undef; }
			else { $newyear=shift(@$argv); }
			unless($volumeset) { $volume=$SILENT; } 
			return;
		}
		elsif($opt eq "C") {
			if(defined($rest) && length($rest)) { $newcomment=$rest; $rest=undef; }
			else { $newcomment=shift(@$argv); }
			unless($volumeset) { $volume=$SILENT; } 
			return;
		}
		elsif($opt eq "h")  { usage(); }
		elsif($opt eq "-") { $doneopts=1; return; }
		else { usage(); }
	}
	return;
}

# delete requested data
sub do_delete
{
	my($mp3,$file)=@_;
	
	if($delete & $DEL_ALL)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->remove_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_tag;
		}
	}

	if($delete & $DEL_TRACKNUM)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->track("00");
			$mp3->{ID3v1}->write_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_frame("TRCK");
			$mp3->{ID3v2}->write_tag;
		}
	}

	if($delete & $DEL_ARTIST)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->artist(" ");
			$mp3->{ID3v1}->write_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_frame("TPE1");
			$mp3->{ID3v2}->remove_frame("TPE2");
			$mp3->{ID3v2}->write_tag;
		}
	}

	if($delete & $DEL_ALBUM)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->album(" ");
			$mp3->{ID3v1}->write_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_frame("TALB");
			$mp3->{ID3v2}->write_tag;
		}
	}

	if($delete & $DEL_TRACK)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->song(" ");
			$mp3->{ID3v1}->write_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_frame("TIT2");
			$mp3->{ID3v2}->write_tag;
		}
	}

	if($delete & $DEL_GENRE)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->genre(" ");
			$mp3->{ID3v1}->write_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_frame("TCON");
			$mp3->{ID3v2}->write_tag;
		}
	}

	if($delete & $DEL_YEAR)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->year(" ");
			$mp3->{ID3v1}->write_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_frame("TYER");
			$mp3->{ID3v2}->write_tag;
		}
	}

	if($delete & $DEL_COMMENT)
	{
		if((($ver==$V1) || ($ver == $BOTH)) &&
		   (exists($mp3->{ID3v1})))
		{
			$mp3->{ID3v1}->comment(" ");
			$mp3->{ID3v1}->write_tag;
		}
		if((($ver==$V2)||($ver == $BOTH)) &&
		   (exists($mp3->{ID3v2})))
		{
			$mp3->{ID3v2}->remove_frame("COMM");
			$mp3->{ID3v2}->write_tag;
		}
	}

	# if -df, check it exists
	if(($delete&$DEL_FRAME) &&
	   (exists($mp3->{ID3v2})))
	{
		for my $frame (@delete)
		{
			my ($info,$name)=$mp3->{ID3v2}->get_frame($frame);
			unless(defined($info))
			{
				warn("$me: $file: frame $frame not found\n");
			}
		}
	}

	# if we need to delete any ext frames
	if((($delete & $DEL_PIC) || ($delete & $DEL_EXT) ||
		($delete & $DEL_BINARY) || ($delete & $DEL_FRAME)) &&
	   ((($ver==$V2)||($ver == $BOTH)) && (exists($mp3->{ID3v2}))))
	{
		my $frameIDs_hash = $mp3->{ID3v2}->get_frame_ids;
		foreach my $frame (keys %$frameIDs_hash)
		{
			if((isext($frame)) &&
			   ($delete & $DEL_EXT) ||
			   (($delete & $DEL_PIC) && ($frame =~ /^APIC/i)) ||
			   (($delete & $DEL_BINARY) && (isbinary($mp3,$frame))) ||
			   (($delete & $DEL_FRAME) && (grep(($frame eq $_),@delete))))
			{
				$mp3->{ID3v2}->remove_frame($frame);
				$mp3->{ID3v2}->write_tag;
			}
		}
	}
}

# replace basic tags
# basic tags are those that have an ID3v1 equivalent
sub do_basicreplace
{
	my($mp3)=@_;
	
	if(defined($newtracknum) || 
	   defined($newartist)   || defined($newalbum)   || 
	   defined($newtrack)    || defined($newgenre)   ||
	   defined($newyear)     || defined($newcomment))
	{
		if(($ver==$V1) || ($ver == $BOTH))
		{
			unless(exists($mp3->{ID3v1}))
			{
				$mp3->new_tag("ID3v1");
			}
			my $id3v1=$mp3->{ID3v1};
			my ( $v1tracknum, $v1artist, $v1album, $v1track, $v1genre, $v1year, $v1comment)=
				($newtracknum,$newartist,$newalbum,$newtrack,$newgenre,$newyear,$newcomment);
			
			# doesnt change id3v1 if you pass empty string, so change it to " "
			if((defined($newtracknum)) && ($newtracknum=~/^\s*$/))
			{ $v1tracknum=" "; }
			if((defined($newartist))   && ($newartist=~/^\s*$/))
			{ $v1artist=" "; }
			if((defined($newalbum))    && ($newalbum=~/^\s*$/))
			{ $v1album=" "; }
			if((defined($newtrack))    && ($newtrack=~/^\s*$/))
			{ $v1track=" "; }
			if((defined($newgenre))    && ($newgenre=~/^\s*$/))
			{ $v1genre=" "; }
			if((defined($newyear))     && ($newyear=~/^\s*$/))
			{ $v1year=" "; }
			if((defined($newcomment))  && ($newcomment=~/^\s*$/))
			{ $v1comment=" "; }

			if(defined($v1tracknum)) { $id3v1->track($v1tracknum);  }
			if(defined($v1artist))   { $id3v1->artist($v1artist);   }
			if(defined($v1album))    { $id3v1->album($v1album);     }
			if(defined($v1track))    { $id3v1->song($v1track);      }
			if(defined($v1genre))    { $id3v1->genre($v1genre);     }
			if(defined($v1year))     { $id3v1->year($v1year);       }
			if(defined($v1comment))  { $id3v1->comment($v1comment); }
			$id3v1->write_tag;
		}
		if(($ver==$V2) || ($ver == $BOTH))
		{
			unless(exists($mp3->{ID3v2}))
			{
				$mp3->new_tag("ID3v2");
			}
			my $id3v2=$mp3->{ID3v2};
			if(defined($newtracknum)) { setframe($id3v2,"TRCK",$newtracknum); }
			if(defined($newartist))   { setframe($id3v2,"TPE1",$newartist); }
			if(defined($newalbum))    { setframe($id3v2,"TALB",$newalbum); }
			if(defined($newtrack))    { setframe($id3v2,"TIT2",$newtrack); }
			if(defined($newgenre))    { setframe($id3v2,"TCON",$newgenre); }
			if(defined($newyear))     { setframe($id3v2,"TYER",$newyear); }
			if(defined($newcomment))  { setframe($id3v2,"COMM",{ Language=>"ENG",
																 short=>$newcomment,
																 Text=>$newcomment}); }
			$id3v2->write_tag;
		}
	}
}

# Prompt for and alter data.
sub do_prompt
{
	my($file,$mp3)=@_;
	return if($prompt == $PROMPT_NONE);

	print "$file:\n";
	if(($prompt & $PROMPT_TRACKNUM) || ($prompt & $PROMPT_ALL))
	{
		my $newtracknum=prompt("Track number",++$lasttracknum);
	}
	if(($prompt & $PROMPT_ARTIST) || ($prompt & $PROMPT_ALL))
	{
		my $old;
		if((($ver == $V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
		{
			$old=$mp3->{ID3v1}->artist;
		}
		if((($ver == $V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
		{
			$old=$mp3->{ID3v2}->artist;
		}
		unless(defined($old)) { $old=""; }
		my $new=prompt("Artist",$old);
		if(defined($new))
		{
			if(($ver == $V1) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v1})) { $mp3->new_tag("ID3v1"); }
				$mp3->{ID3v1}->artist($new);
			}
			if(($ver == $V2) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
				setframe($mp3->{ID3v2},"TPE1",$new); 
			}
		}
	}
	if(($prompt & $PROMPT_ALBUM) || ($prompt & $PROMPT_ALL))
	{
		my $old;
		if((($ver == $V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
		{
			$old=$mp3->{ID3v1}->album;
		}
		if((($ver == $V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
		{
			$old=$mp3->{ID3v2}->album;
		}
		unless(defined($old)) { $old=""; }
		my $new=prompt("Album",$old);
		if(defined($new))
		{
			if(($ver == $V1) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v1})) { $mp3->new_tag("ID3v1"); }
				$mp3->{ID3v1}->album($new);
			}
			if(($ver == $V2) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
				setframe($mp3->{ID3v2},"TALB",$new); 
			}
		}
	}
	if(($prompt & $PROMPT_TRACK) || ($prompt & $PROMPT_ALL))
	{
		my $old;
		if((($ver == $V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
		{
			$old=$mp3->{ID3v1}->song;
		}
		if((($ver == $V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
		{
			$old=$mp3->{ID3v2}->song;
		}
		unless(defined($old)) { $old=""; }
		my $new=prompt("Track",$old);
		if(defined($new))
		{
			if(($ver == $V1) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v1})) { $mp3->new_tag("ID3v1"); }
				$mp3->{ID3v1}->song($new);
			}
			if(($ver == $V2) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
				setframe($mp3->{ID3v2},"TIT2",$new); 
			}
		}
	}
	if(($prompt & $PROMPT_YEAR) || ($prompt & $PROMPT_ALL))
	{
		my $old;
		if((($ver == $V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
		{
			$old=$mp3->{ID3v1}->year;
		}
		if((($ver == $V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
		{
			$old=($mp3->{ID3v2}->get_frame("TYER"))[0];
		}
		unless(defined($old)) { $old=""; }
		my $new=prompt("Year",$old);
		if(defined($new))
		{
			if(($ver == $V1) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v1})) { $mp3->new_tag("ID3v1"); }
				$mp3->{ID3v1}->year($new);
			}
			if(($ver == $V2) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
				setframe($mp3->{ID3v2},"TYER",$new); 
			}
		}
	}
	if(($prompt & $PROMPT_GENRE) || ($prompt & $PROMPT_ALL))
	{
		my $old;
		if((($ver == $V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
		{
			$old=$mp3->{ID3v1}->genre;
		}
		if((($ver == $V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
		{
			$old=($mp3->{ID3v2}->get_frame("TCON"))[0];
		}
		unless(defined($old)) { $old=""; }
		my $new=prompt("Genre",$old);
		if(defined($new))
		{
			if(($ver == $V1) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v1})) { $mp3->new_tag("ID3v1"); }
				$mp3->{ID3v1}->genre($new);
			}
			if(($ver == $V2) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
				setframe($mp3->{ID3v2},"TCON",$new); 
			}
		}
	}
	if(($prompt & $PROMPT_COMMENT) || ($prompt & $PROMPT_ALL))
	{
		my $old;
		if((($ver == $V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
		{
			$old=$mp3->{ID3v1}->comment;
		}
		if((($ver == $V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
		{
			$old=$mp3->{ID3v2}->get_frame("COMM");
			if(defined($old)) { $old=$old->{Text}; }
		}
		unless(defined($old)) { $old=""; }
		my $new=prompt("Comment",$old);
		if(defined($new))
		{
			if(($ver == $V1) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v1})) { $mp3->new_tag("ID3v1"); }
				$mp3->{ID3v1}->comment($new);
			}
			if(($ver == $V2) || ($ver == $BOTH))
			{
				unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
				setframe($mp3->{ID3v2},"COMM",{ Language=>"ENG",
												short=>$new,
												Text=>$new});
			}
		}
	}
	if($prompt & $PROMPT_PIC)
	{
		unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
		my $new=prompt("Picture filename");
		my $mode=$ADD;
		if(defined($new))
		{
			if($mp3->{ID3v2}->get_frame("APIC"))
			{
				my $reply=prompt("File already has picture, [a]dd or [R]eplace? (a/R)");
				unless(defined($reply)) { $reply='r'; }
				if($reply=~/^\s*[Rr]/)  { $mode=$REPLACE; }
			}
			setpic($mp3->{ID3v2},$new,$mode);
		}
	}

	if($prompt & $PROMPT_FRAME)
	{
		promptframe($mp3);
	}

	if((($ver == $V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
	{
		$mp3->{ID3v1}->write_tag;
	}
	if((($ver == $V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
	{
		$mp3->{ID3v2}->write_tag;
	}
}

# extract requested data
sub do_extract
{
	my($mp3,$file)=@_;

	if($extract&$EXTRACT_ALL)
	{
		dumptags(1,$mp3,$file,$ver);
	}
	elsif(($extract&$EXTRACT_PIC)    ||
		  ($extract&$EXTRACT_BINARY) ||
		  ($extract&$EXTRACT_FRAME))
	{
		my $frameIDs_hash = $mp3->{ID3v2}->get_frame_ids;
		my $countval="01";
		my $count=\$countval;
		foreach my $frame (sort(keys(%$frameIDs_hash)))
		{
			if((($extract&$EXTRACT_BINARY) && (isbinary($mp3,$frame))) ||
			   (($extract&$EXTRACT_PIC)    && ($frame=~/^APIC/i))      ||
			   (($extract&$EXTRACT_FRAME)  && (scalar(grep { $frame eq $_; } @extract))))
			{
				extractframe($mp3,$file,$frame,$count);
			}
		}
	}
}

# print out frames to screen
sub do_print
{
	my($mp3,$file)=@_;
	if($volume == $VERBOSE)
	{
		dumptags(0,$mp3,$file,$ver);
	}
	elsif($volume == $TERSE)
	{
		printterse($mp3,$file,$ver);
	}
}

# copy and compare frames
sub do_sync
{
	my ($file,$mp3)=@_;
	return if($sync==$SYNC_NONE);
	my $compare=(($sync&$SYNC_COMP) || ($sync&$SYNC_COMPSTRICT));
	if(!defined($mp3->{ID3v1}))
	{
		if($compare)
		{
			warn("$me: $file: compare: no ID3v1 tag\n");
			return;
		}
		else { $mp3->new_tag("ID3v1"); }
	}
	if(!defined($mp3->{ID3v2}))
	{
		if($compare)
		{
			warn("$me: $file: compare: no ID3v2 tag\n");
			return;
		}
		else { $mp3->new_tag("ID3v2"); }
	}

	my $v1=$mp3->{ID3v1};
	my $v2=$mp3->{ID3v2};

	my $v1artist=$v1->artist;
	my $v2artist=$v2->artist;

	if($compare)
	{
		compare($file,$v1artist,$v2artist,"artist",$sync);
	}
	else
	{
		my $bestartist=best($v1artist,$v2artist,$sync);
		$v1->artist($bestartist);
		setframe($v2,"TPE1",$bestartist);
	}
	
	my $v1album=$v1->album;
	my $v2album=$v2->album;

	if($compare)
	{
		compare($file,$v1album,$v2album,"album",$sync);
	}
	else
	{
		my $bestalbum=best($v1album,$v2album,$sync);
		$v1->album($bestalbum);
		setframe($v2,"TALB",$bestalbum);
	}
	
	my $v1tracknum=$v1->track;
	my $v2tracknum=$v2->track;

	if($compare)
	{
		compare($file,$v1tracknum,$v2tracknum,"tracknum",$sync);
	}
	else
	{
		my $besttracknum=best($v1tracknum,$v2tracknum,$sync);
		$v1->track($besttracknum);
		setframe($v2,"TRCK",$besttracknum);
	}
	
	my $v1track=$v1->song;
	my $v2track=$v2->song;

	if($compare)
	{
		compare($file,$v1track,$v2track,"track",$sync);
	}
	else
	{
		my $besttrack=best($v1track,$v2track,$sync);
		$v1->song($besttrack);
		setframe($v2,"TIT2",$besttrack);
	}
	
	my $v1genre=$v1->genre;
	my $v2genre=($v2->get_frame("TCON"))[0];

	if($compare)
	{
		compare($file,$v1genre,$v2genre,"genre",$sync);
	}
	else
	{
		my $bestgenre=best($v1genre,$v2genre,$sync);
		$v1->genre($bestgenre);
		setframe($v2,"TCON",$bestgenre);
	}
	
	my $v1year=$v1->year;
	my $v2year=($v2->get_frame("TYER"))[0];

	if($compare)
	{
		compare($file,$v1year,$v2year,"year",$sync);
	}
	else
	{
		my $bestyear=best($v1year,$v2year,$sync);
		$v1->year($bestyear);
		setframe($v2,"TYER",$bestyear);
	}
	
	my $v1comment=$v1->comment;
	my $v2comframe=$v2->get_frame("COMM");
	my $v2comment;
	if(defined($v2comframe))
	{
		$v2comment=$v2comframe->{Text};
	}

	if($compare)
	{
		compare($file,$v1comment,$v2comment,"comment",$sync);
	}
	else
	{
		my $bestcomment=best($v1comment,$v2comment,$sync);
		$v1->comment($bestcomment);
		setframe($v2,"COMM",{ Language =>"ENG",
							  short=>$bestcomment,
							  Text=>$bestcomment});
	}
	$v1->write_tag;
	$v2->write_tag;
}

# delete empty tags
sub do_delempty
{
	my($mp3)=@_;
	my $delver=$NEITHER;
	if((($ver==$V1) || ($ver == $BOTH)) && exists($mp3->{ID3v1}))
	{
		my $t=$mp3->{ID3v1};
		if(((isblank($t->track)) || ($t->track =~/^\s*0+\s*$/)) &&
			   (isblank($t->artist)) && (isblank($t->album)) && 
			   (isblank($t->song))   && (isblank($t->year))  && 
			   (isblank($t->genre))  && (isblank($t->comment)))
		{
			$t->remove_tag;
			$delver=$V1;
		}
	}
	if((($ver==$V2) || ($ver == $BOTH)) && exists($mp3->{ID3v2}))
	{
		my $v2=$mp3->{ID3v2};
		my $frameIDs_hash = $v2->get_frame_ids;
		my $delv2=1;

		# check only blank non-extended frames
		foreach my $f (grep { (!isext($_) ) } keys(%$frameIDs_hash))
		{
			my($info,undef)=$v2->get_frame($f);
			if($f =~/^COMM/i)
			{
				if(ref($info) &&
				   exists($info->{Text})  &&
				   defined($info->{Text}))
				{
					if($info->{Text}=~/\S/)
					{
						$delv2=0;
					}
				}
			}
			else
			{
				if((!ref($info) && ($info=~/\S/)))
				{
					$delv2=0;
				}
			}
		}

		# or any extended frames present
		if(scalar(grep { isext($_) } keys(%$frameIDs_hash)))
		{
			$delv2=0;
		}

		if($delv2)
		{
			$mp3->{ID3v2}->remove_tag;
			if($delver == $V1) { $delver=$BOTH; }
			else { $delver=$V2; }
		}
	}
	return $delver;
}

# import tag data from .id3 file
# returns a list of imported files.
sub do_import
{
	my ($mp3,$file)=@_;
	unless(open(I,$importfile))
	{
		warn("$me: cannot open import file $importfile: $!\n");
		return(());
	}
	my $filedata=join('',<I>);
	close(I);
	if((($ver==$V1 || $ver==$BOTH)) &&
	   ($filedata=~/^.*:ID3v1:$/m))
	{
		importv1($mp3,$file,$importfile,$filedata);
	}
	my @imported=($importfile);
	if((($ver==$V2 || $ver==$BOTH)) &&
	   ($filedata=~/^.*:ID3v2:$/m))
	{
		push(@imported,importv2($mp3,$file,$importfile,$filedata));
	}
	return(@imported);
}

# manipulate extended frames
sub do_addreplaceframes
{
	my($mp3)=@_;
	unless(exists($mp3->{ID3v2}))
	{
		$mp3->new_tag("ID3v2");
	}
	my $v2=$mp3->{ID3v2};
	while(my ($framestr,$mode)=each(%newframes))
	{
		my ($frame,$info)=parseframespec($v2,$framestr);
		if(defined($frame))
		{
			setframe($v2,$frame,$info,$mode);
		}
	}
	while(my ($pic,$mode)=each(%newpics))
	{
		setpic($v2,$pic,$mode);
	}
}

# print available frame types
sub do_printframetypes
{
	my $verbose=shift;
	my $filename="/tmp/mp3id.$$.mp3";
	unless((open(TMP,">$filename")) && (close TMP))
	{
		warn("$me: cannot show frame types: error creating temp file\n");
		return;
	}
	my $mp3=MP3::Tag->new($filename);
	unless(defined($mp3)) { warn("$me: printframetypes: internal error\n"); return; }
	my $v2=$mp3->new_tag("ID3v2");
	my $tags=$v2->supported_frames();
	my %simple; my %complex;
	while(my ($f,$long)=each(%$tags))
	{
		my($tags)=$v2->what_data($f);
		if(scalar(@$tags) == 1)
		{
			$simple{$f}=$long;
		}
		else
		{
			$complex{$f}={tags=>$tags,long=>$long};
		}
	}

	if($verbose)
	{
		print "Simple frame types:\n";
		for my $tag (sort keys(%simple))
		{
			my($fname,$res_inp)=$v2->what_data($tag);
			print "$tag: $simple{$tag}\n";
			
			if((exists ($res_inp->{Text})) &&
			   (defined($res_inp->{Text})))
			{
				print " ";
				if(exists($res_inp->{Text}{_FREE}))
				{
					print "Recommended";
				}
				else
				{
					print "Allowed";
				}
				print " values for field \"$tag\":\n";
				foreach my $k (keys(%{$res_inp->{Text}}))
				{
					next if ($k eq "_FREE");
					printwrap($k,", "," ");
				}
				printwrap("\n");
			}
		}
		print "\nComplex frame types:\n";
		for my $f (sort keys(%complex))
		{
			my($fname,$res_inp)=$v2->what_data($f);
			my $extra="";
			print "$f $complex{$f}{long}:\n";
			for my $tag (@{$complex{$f}{tags}})
			{
				if((exists($res_inp->{$tag})) && (defined($res_inp->{$tag})))
				{
					$extra .= " ";
					if(exists($res_inp->{$tag}{_FREE}))
					{
						$extra .= "Recommended";
					}
					else
					{
						$extra .= "Allowed";
					}
					$extra .= " values for field \"$tag\":\n";
					foreach my $k (keys(%{$res_inp->{$tag}}))
					{
						next if ($k eq "_FREE");
						$extra .= "   $k\n";
					}
				}
				printwrap($tag,", "," ");
			}
			printwrap("\n");
			if($extra ne "") { print $extra; $extra=""; }
		}
		print "\nFor more details see MP3::Tag::ID3v2-Data(1)\n";
	}
	else
	{
		print "Simple tags:\n";
		my $str="";
		for my $f (sort keys %simple)
		{
			printwrap($f,", ","");
		}
		printwrap("\n");
		print "Complex tags:\n";
		for my $f (sort keys(%complex))
		{
			my $delim=": ";
			printwrap($f,$delim,"");
			for my $tag (@{$complex{$f}{tags}})
			{
				printwrap($tag,$delim," ");
				$delim=", "
			}
			printwrap("\n");
		}
		print "\nFor more details do  \"$me -F | more\"\n";
	}
	unlink($filename);
}	

# dump tag data to stdout (if $dumping==0)
# or export file (if $dumping==1)
sub dumptags
{
	my($dumping,$mp3,$file,$ver)=@_;

	# make sure there is something to do
	my $ev1=exists($mp3->{ID3v1});
	my $ev2=exists($mp3->{ID3v2});
	if((($ver == $V1) && !$ev1) ||
	   (($ver == $V2) && !$ev2) ||
	   (($ver == $BOTH) && (!($ev1 || $ev2))))
	{
		return;
	}
	
	my $oldstdout=select;
	if($dumping)
	{
		my $filebase=$file;
		$filebase=~s/(?:.*\/)?(.*)\..*/$1/;
		my $dumpfile=$filebase.".id3";
		# unconditionally overwrite dump file
		unless(open(DUMP,">$dumpfile"))
		{
			warn("$file: cannot open output file $dumpfile, skipping\n");
			return;
		}
		print "$me: exporting tag data to $dumpfile\n" unless(($volume==$SILENT) && ($volumeset));
		select(DUMP);
	}

	my $found=0;
	if((exists($mp3->{ID3v1})) && ($ver==$V1 || $ver == $BOTH))
	{
		$found=1;
		my $v1=$mp3->{ID3v1};
		print "$file:ID3v1:\n";
		if(defined($v1->song))    { print "Title:    ", escape($v1->song),   "\n"; }
		if(defined($v1->artist))  { print "Artist:   ", escape($v1->artist), "\n"; }
		if(defined($v1->album))   { print "Album:    ", escape($v1->album),  "\n"; }
		if(defined($v1->track))   { print "Tracknum: ", escape($v1->track),  "\n"; }
		if(defined($v1->year))    { print "Year:     ", escape($v1->year),   "\n"; }
		if(defined($v1->genre))   { print "Genre:    ", escape($v1->genre),  "\n"; }
		if(defined($v1->comment)) { print "Comment:  ", escape($v1->comment),"\n"; }
	}

	if((exists($mp3->{ID3v2})) && ($ver==$V2 || $ver == $BOTH))
	{
		$found=1;
		my $v2=$mp3->{ID3v2};
 		my $v2year=($v2->get_frame("TYER"))[0];
		my $v2tracknum=($v2->get_frame("TRCK"))[0];
		my $v2genre=($v2->get_frame("TCON"))[0];

		my $v2comment=undef;
		my ($info,$name)=$v2->get_frame("COMM");
		if(defined($info) && ref($info) && exists($info->{Text}))
		{
			$v2comment=$info->{Text};
		}
		if(keys(%{$v2->get_frame_ids}))
		{
			print "$file:ID3v2:\n";
		}
		if(defined($v2->song))   { print "Title:    ", escape(scalar($v2->song)),   "\n"; }
		if(defined($v2->artist)) { print "Artist:   ", escape(scalar($v2->artist)), "\n"; }
		if(defined($v2->album))  { print "Album:    ", escape(scalar($v2->album)),  "\n"; }
		if(defined($v2tracknum)) { print "Tracknum: ", escape(scalar($v2tracknum)), "\n"; }
		if(defined($v2year))     { print "Year:     ", escape($v2year),             "\n"; }
		if(defined($v2genre))    { print "Genre:    ", escape($v2genre),            "\n"; }
		if(defined($v2comment))  { print "Comment:  ", escape($v2comment),          "\n"; }
		if($extended || $raw || $dumping)
		{
			my $frameIDs_hash = $v2->get_frame_ids;
			my $printedheader=0;
			my $countval="01";
			my $count=\$countval;
			foreach my $frame (sort(keys(%$frameIDs_hash)))
			{
				next unless(isext($frame) || ($raw && !$dumping));
				if(!$printedheader)
				{
					print "Extended ID3v2 frames:\n";
					$printedheader=1;
				}
				my ($info, $name) = $mp3->{ID3v2}->get_frame($frame);
				if (ref $info)
				{
					unless(defined($name))  { $name=""; }
					unless(defined($frame)) { $frame=""; }
					print escape("$frame ($name):"),"\n";
					while(my ($key,$val)=each %$info)
					{
						if($key =~ /_Data/i)
						{
							if($dumping)
							{
								my $mime=undef;
								if((exists($info->{"MIME type"})) && (defined($info->{"MIME type"})))
								{
									$mime=$info->{"MIME type"};
								}
								my $datafile=dumpdata($file,$mime,$val,$count);
								next unless defined($datafile);
								unless(($volume==$SILENT) && $volumeset)
								{
									# need to specify stdout because we have select()ed
									# DUMP at this point.
									print STDOUT escape("$file: $frame: saved data to $datafile"),"\n";
								}
								$val="file($datafile)";
							}
							else { $val="<data> (".length($val)." bytes)"; }
						}
						print "\t",escape("$key => $val"),"\n";
					}
				}
				else
				{
					unless(defined($info)) { $info=""; }
					unless(defined($frame)) { $frame=""; }
					unless(defined($name)) { $name=""; }
					print escape("$frame ($name):"),"\t",escape("$info"),"\n";
				}
			}
		}
	}
	if((!$found) && (!$dumping))
	{
		if   ($ver == $V1) { print("$file:no ID3v1 tag\n"); }
		elsif($ver == $V2) { print("$file:no ID3v2 tag\n"); }
		else               { print("$file:no ID3 tags\n");  }
	}
	if($dumping)
	{
		close(DUMP);
	}
	select($oldstdout);
}

# Prompt for an arbitrary frame
# (including complex frames)
sub promptframe
{
	my $mp3=shift;
	
	unless(exists($mp3->{ID3v2})) { $mp3->new_tag("ID3v2"); }
	my $fname=prompt("Frame Name");
	my %data=();
	return unless(defined($fname));
	my ($fields,$res_inp)=$mp3->{ID3v2}->what_data($fname);
	unless(defined($fields))
	{
		warn("$me: $fname: Unknown frame type\n");
		return;
	}

	my $oldtext=undef;
	my($oldinfo,$oldname)=$mp3->{ID3v2}->get_frame($fname);
	if(defined($oldinfo) && !ref($oldinfo))
	{
		$oldtext=$oldinfo;
	}
	
	if(scalar(@$fields) > 1)
	{
		print "Complex frame. Needed fields: ", join(", ",@$fields), "\n";
	}
	else { print "Simple frame.\n"; }

	for my $field (@$fields)
	{
		my $new;
		if($field=~/^_/) # data
		{
			my $dofile=prompt("Data field. Read from [F]ile, [p]rompt or [e]xisting tag? (F/p/t)");
			unless(defined($dofile)) { $dofile="f"; }
			if($dofile=~/^\s*[fF]/)
			{
				my $filename=prompt("Filename");
				if(defined($filename)) { $new=readfile($filename); }
				return unless(defined($new));
			}
			elsif($dofile=~/^\s*[Ee]/)
			{
				if(defined($oldinfo) && ref($oldinfo) &&
				   exists ($oldinfo->{$field}) &&
				   defined($oldinfo->{$field}))
				{
					$new=$oldinfo->{$field};
				}
				else
				{
					warn("$me: $fname: $field: no existing field\n");
					return;
				}
			}
			else
			{
				$new=prompt("Value");
			}
		}
		elsif((exists($res_inp->{$field})) &&
			  (defined($res_inp->{$field})))
		{
			my $res=$res_inp->{$field};
			my $free=0;
			$free=1 if exists($res->{_FREE});
			my $done=0;
			do
			{
				print "Choose a value for $field\n";
				my @allowedkeys;
				my @allowedvals;
				foreach my $k (sort keys %{$res})
				{
					next if ($k eq "_FREE");
					push(@allowedkeys,$k);
					push(@allowedvals,$res->{$k});
				}
				for(my $i=0;$i<scalar(@allowedkeys);$i++)
				{
					printf "  %2s: $allowedkeys[$i]\n",$i;
				}
				if($free)
				{
					print "   o: other (prompt)\n";
				}
				my $reply=prompt($field);
				if($reply =~ /^o$/i)
				{
					$new=prompt("Value");
					$done=1;
				}
				elsif(($reply =~ /^\d+$/) &&
					  ($reply >= 0) &&
					  ($reply <= $#allowedvals))
				{
					$new=$allowedvals[$reply];
					$done=1;
				}
				else
				{
					warn("$me: Invalid response \"$reply\"\n");
					return;
				}
			} while (!$done);
		}
		else
		{
			my $default="";
			if(defined($oldinfo)  &&
			   ref($oldinfo)      &&
			   exists ($oldinfo->{$field}) &&
			   defined($oldinfo->{$field}))
			{
				$default=$oldinfo->{$field};
			}
			elsif(defined($oldtext))
			{
				$default=$oldtext;
			}
			$new=prompt($field,$default);
		}
		unless(defined($new)) { $new=""; }
		$data{$field}=$new;
	}
	
	my $mode=$ADD;
	if($mp3->{ID3v2}->get_frame($fname))
	{
		my $promptmsg="Frame exists, [a]dd or [R]eplace? (a/R)";
		my $replacecmd=prompt($promptmsg);
		unless(defined($replacecmd)) { $replacecmd="r"; }
		if   ($replacecmd=~/^\s*[Rr]/) { $mode=$REPLACE; }
	}

	setframe($mp3->{ID3v2},$fname,\%data,$mode);
}

# prompts for data using Term::Readline
# $text = prompting text
# $default = default data displayed (press return to accept)
# returns users reply.s
sub prompt
{
	my($text,$default)=@_;
	unless(defined($text)) { $text=""; }
	my $reply=$term->readline("$text: ", $default);
	if(defined($reply) && ($reply=~/\S+/))
	{
		$term->addhistory($reply);
	}
	if($reply eq "") { return undef; }
	return $reply;
}

# adds/replaces pictures
sub setpic
{
	my($v2,$pic,$mode)=@_;
	unless(-r $pic)
	{
		warn("$me: $pic: file not found\n");
		return;
	}
	if($mode == $REPLACE)
	{
		$v2->remove_frame("APIC");
		$v2->write_tag;
	}
	my $ext='';
	if($pic=~/.*\.(.*)/)
	{
		$ext=$1;
	}
	my $mime=exttomime($ext);
	if($mime eq "")
	{
		$mime="application/octet-stream";
	}
	my $data=readfile($pic);
	unless(defined($data)) { warn("$me: cannot read $pic: $!\n"); return; }
	my $info={ "MIME type"    => $mime,
			   "Picture Type" => chr(0), # "Other"
			   "Description"  => $pic,
			   "_Data"        => $data };
	setframe($v2,"APIC",$info,$mode);
}

# single point to add/replace id3v2 frame data
sub setframe
{
	my($id3,$name,$new,$mode)=@_;
	unless(defined($mode)) { $mode=$REPLACE; }
	my $old=($id3->get_frame($name))[0];
	unless(defined($old)) { $old={}; }
	my @data;
	my $fieldcount=0;
	
	if(defined($new) && ref($new))
	{
		my ($framelist,$res_inp)=$id3->what_data($name);
		unless(defined($framelist))
		{
			warn("$me: $name: unknown frame type.\n");
			return;
		}
		for(my $i=0;$i<scalar(@$framelist);$i++)
		{
			my $f=$framelist->[$i];
			my $val;
			if(exists($new->{$f}) && defined($new->{$f}))
			{
				$val=$new->{$f};
				$fieldcount++;
			}
			elsif((exists($old->{$f})) && (defined($old->{$f})))
			{
				$val=$old->{$f};
				$fieldcount++;
			}
			else { $val=""; }

			# pass through $res_inp if necessary
			if((exists($res_inp->{$f}))  &&
			   (defined($res_inp->{$f})) &&
			   ($val ne ""))
			{
				my $res=$res_inp->{$f};
				my $free=0;
				$free=1 if exists($res->{_FREE});

				# first see if val is in keys $res
				if(exists($res->{$val}))
				{
					$val=$res->{$val};
				}
				# see if its in vals $res
				elsif(scalar(grep { $_ eq $val; } values(%{$res})))
				{
					; # pass it through unchanged
				}
				else
				{
					unless($free)
					{
						warn("$me: $name: Invalid data for element \"$f\": ",ord($val),"\n");
						return;
					}
				}
			}
			push(@data,$val);
		}
		my $listcount=scalar(@$framelist);
		if($fieldcount!=$listcount)
		{
			warn("$me: frame $name element count mismatch - wanted $listcount, got $fieldcount\n");
			return;
		}
	}
	else
	{
		if(defined($new))
		{
			# prepends '(C) ' to TCOP data, so remove it if there is one there
			if($name eq "TCOP") { $new =~ s/^\([Cc]\) ?//; }
		}
		else { $new=""; }
		push(@data,$new);
	}
	if($mode==$REPLACE)
	{
		$id3->remove_frame($name);
		$id3->write_tag;
	}
#	$id3->add_frame($name,0,@data);
# Some tags don't have encoding elements, so leave out encoding ie default.
	$id3->add_frame($name,@data);
	$id3->write_tag;
}

# attempts to extract the _data from a given frame to file
sub extractframe
{
	my ($mp3,$file,$frame,$count)=@_;
	return unless(exists($mp3->{ID3v2}));

	my $filebase=$file;
	$filebase=~s/(?:.*\/)?(.*)\..*/$1/;

	my ($info,undef)=$mp3->{ID3v2}->get_frame($frame);
	return unless(defined($info));

	if(!ref $info)
	{
		# not a complex frame, so can't have _Data
		warn("$me: $file: frame $frame does not have a binary attachment\n");
		return;
	}
	
	my $data=undef; my $mime=undef;
	my $datakey=undef;

	while(my ($key,$val)=each %$info)
	{
		if ($key =~ /mime/i) { $mime=$val; }
		
		if ($key =~ /^_/) { $data=$val; $datakey=$key }
	}

	unless(defined($datakey))
	{
		warn("$me: $file: frame $frame does not have a binary attachment\n");
		return;
	}
	
	if(defined($data))
	{
		my $datafile=dumpdata($file,$mime,$data,$count);
		print "$file: $frame: saved data to $datafile\n" if (($volume!=$SILENT) || !$volumeset);
	}
}

# Parse a frame specification. valid specs:
# -af <frame=data> -af <frame:key1=data:key2=:key3=file(file.dat)>
# -rf <frame=data> -[rR]f <frame:key1=data:key2=:key3=file(file.dat)>
# returns ($framename, $info), same as get_frame, but, er, backwards.
sub parseframespec
{
	my ($v2,$spec)=@_;
	my($frame,$info);
	my $supported_frames=$v2->supported_frames;
	# escape colons
	$spec=~s/\\:/\xfc/g;
	if($spec=~/(.*?):(.*)/)
	{
		$frame=$1;
		unless(exists($supported_frames->{ uc(substr($frame,0,4)) }))
		{
			warn("$me: unknown frame type $frame\n");
			return((undef,undef));
		}

		my($fdata,undef)=$v2->what_data($frame);
		if(scalar(@$fdata) <= 1)
		{
			warn("$me: $frame: expected simple frame data, got complex data\n");
			return((undef,undef));
		}

		$info={};
		my @tags=split(/:/,$2);
		for my $tag (@tags)
		{
			# unescape colons
			$tag=~s/\xfc/:/g;
			# handle backslashed consts (\0, etc)
			# first escape '\\'
			$tag=~s/\\\\/\xfb/g;
			# convert consts
			$tag=~s/\\(\d{1,3}?)/chr($1)/eg;
			# unescape \\
			$tag=~s/\xfb/\\/g;

			if($tag=~/(.*)=(.*)?/)
			{
				my($key,$val)=($1,$2);
				unless(defined($val)) { $val=""; }
				if($val=~/^file\((.*)\)/)
				{
					$val=readfile($1);
				}
				unless(defined($val)) { $val=""; }
				$info->{$key}=$val;
			}
			else
			{
				warn("$me: bad frame spec\n");
				return((undef,undef));
			}
		}
	}
	elsif($spec=~/(.*)=(.*)?/)
	{
		$frame=$1;
		$info=$2;
		unless(defined($info)) { $info=""; }
		unless(exists($supported_frames->{ uc(substr($frame,0,4)) }))
		{
			warn("$me: unknown frame type $frame\n");
			return((undef,undef));
		}
		my($fdata,undef)=$v2->what_data($frame);
		if(scalar(@$fdata) != 1)
		{
			warn("$me: $frame: expected complex frame data, got simple data\n");
			return((undef,undef));
		}
	}
	else
	{
		warn("$me: bad frame spec\n");
		return((undef,undef));
	}
	return(uc($frame),$info);
}

# import id3v1 data
sub importv1
{
	my ($mp3,$file,$importfile,$filedata)=@_;
	unless(exists($mp3->{ID3v1}))
	{
		$mp3->new_tag("ID3v1");
	}
	my $v1=$mp3->{ID3v1};
	my $line=0;
	while($filedata=~/^(.*)$/mg)
	{
		$_=$1;
		++$line;
		if   (/^.*:ID3v1:$/) {;} # skip
		elsif(/^Title:\s+(.*)/)    { my $d=$1; $v1->song($d);    }
		elsif(/^Artist:\s+(.*)/)   { my $d=$1; $v1->artist($d);  }
		elsif(/^Album:\s+(.*)/)    { my $d=$1; $v1->album($d);   }
		elsif(/^Tracknum:\s+(.*)/) { my $d=$1; $v1->track($d);   }
		elsif(/^Year:\s+(.*)/)     { my $d=$1; $v1->year($d);    }
		elsif(/^Genre:\s+(.*)/)    { my $d=$1; $v1->genre($d);   }
		elsif(/^Comment:\s+(.*)/)  { my $d=$1; $v1->comment($d); }
		elsif(/^.*:ID3v2:$/) { $v1->write_tag; return; } # done v1
		else
		{
			warn("$me: $file: error parsing ID3v1 import data from $importfile at line $line ($_)\n");
			$v1->write_tag;
			return 0;
		}
	}
	$v1->write_tag;
}

# import basic id3v2 frames
# returns a list of imported files
sub importv2
{
	my ($mp3,$file,$importfile,$filedata)=@_;
	unless(exists($mp3->{ID3v2}))
	{
		$mp3->new_tag("ID3v2");
	}
	my $v2=$mp3->{ID3v2};
	my $line=0;
	my $foundv2=0;
	while($filedata=~/^(.*)$/mg)
	{
		$_=$1;
		++$line;
		if(!$foundv2)
		{
			if   (/^.*:ID3v2:$/) { $foundv2=1;}
			next;
		}

		if   (/^Title:\s+(.*)/)    { setframe($v2,"TIT2",$1); }
		elsif(/^Artist:\s+(.*)/)   { setframe($v2,"TPE1",$1); }
		elsif(/^Album:\s+(.*)/)    { setframe($v2,"TALB",$1); }
		elsif(/^Tracknum:\s+(.*)/) { setframe($v2,"TRCK",$1); }
		elsif(/^Year:\s+(.*)/)     { setframe($v2,"TYER",$1); }
		elsif(/^Genre:\s+(.*)/)    { setframe($v2,"TCON",$1); }
		elsif(/^Comment:\s+(.*)/)  { setframe($v2,"COMM",{ Language=>"ENG",
														   short=>$1,
														   Text=>$1}); }
		elsif(/^Extended ID3v2 frames:$/)
		{
			my @ret=importext($mp3,$file,$importfile,$filedata);
			$v2->write_tag;
			return(@ret);
		}
		else
		{
			warn("$me: $file: error parsing ID3v2 import data from $importfile at line $line\n");
			$v2->write_tag;
			return(());
		}
	}
	$v2->write_tag;
	return(());
}

# import extended id3v2 frames
# returns a list of imported files
sub importext
{
	my ($mp3,$file,$importfile,$filedata)=@_;
	my $v2=$mp3->{ID3v2};
	my $line=0;
	my $foundext=0;
	my $lastframe=undef;
	my $lastdata=undef;
	my @imported=();
	while($filedata=~/^(.*)$/mg)
	{
		$_=$1;
		++$line;
		if(!$foundext)
		{
			if(/^Extended ID3v2 frames:$/) { $foundext=1; }
			next; 
		}

		# normal frame or header of complex frame
		if(/^(....\d*)\s+\(.*\):\s*(.*)?$/)
		{
			push(@imported,importframe($v2,$lastframe,$lastdata));
			$lastframe=$1;
			$lastdata=$2;
		}
		# part of complex frame
		elsif(/^\s+(.*)\s+=>\s*(.*)?/)
		{
			if((!defined($lastdata)) || (!ref($lastdata)))
			{
				$lastdata={};
			}
			my $thisdata=$2;
			unless(defined($thisdata)) { $thisdata=""; }
			$lastdata->{$1}=$thisdata;
		}
		else
		{
			warn("$me: $file: error parsing ID3v2 import data from $importfile at line $line\n");
			$v2->write_tag;
			return @imported;
		}
	}
	# take care of last line
	push(@imported,importframe($v2,$lastframe,$lastdata));
	$v2->write_tag;
	return @imported;
}

# import a single extended frame
# returns a list of imported files.
sub importframe
{
	my($tag,$frame,$data)=@_;
	return unless defined($frame);
	my @importedfiles=();
	if(defined($data))
	{
		if(ref($data))
		{
			foreach my $k (keys %$data)
			{
				if($data->{$k} =~/^\s*file\((.*)\)/)
				{
					$data->{$k}=readfile($1);
					if(!defined($data->{$k}))
					{
						warn("$me: cannot read data file $1: $!\n");
					}
					else
					{
						push(@importedfiles,$1);
					}
				}
			}
		}
	}
	else
	{
		$data="";
	}
	setframe($tag,$frame,$data,$REPLACE);
	return(@importedfiles);
}

# returns the best value of $one or $two, depending on $sync
sub best
{
	my ($one,$two,$sync)=@_;
	if(($sync&$SYNC_ONE) && defined($one))
	{
		return $one;
	}
	if(($sync&$SYNC_TWO) && defined($two))
	{
		return $two;
	}
	my $best="";
	if($sync&$SYNC_BEST)
	{
		if(defined($one) && length($one))
		{
			$best=$one;
		}
		if(defined($two) && length($two))
		{
			$best=$two;
		}
	}
	return $best;
}

# compares versions of tag data according to $sync
sub compare
{
	my ($file,$one,$two,$text,$sync)=@_;
	unless(defined($one))
	{
		warn("$me: $file: compare: ID3v1 $text tag not defined\n");
		return;
	}
	unless(defined($two))
	{
		warn("$me: $file: compare: ID3v2 $text tag not defined\n");
		return;
	}
	my $return=0;
	if(length($one)==0)
	{
		warn("$me: $file: compare: ID3v1 $text tag empty\n");
		$return=1;
	}
	if(length($two)==0)
	{
		warn("$me: $file: compare: ID3v2 $text tag empty\n");
		$return=1;
	}
	return if $return;
	unless($sync & $SYNC_COMPSTRICT)
	{
		# allow for id3v1 truncation
		$two=substr($two,0,length($one));
	}
	if($one ne $two)
	{
		warn("$me: $file: compare: ID3v1/v2 $text tag mismatch\n");
	}
}

# helper function used by do_print in terse mode
sub printterse
{
	my($mp3,$file,$ver)=@_;
	my $prefix="$file:ext:";
	if((exists($mp3->{ID3v1})) && ($ver==$V1 || $ver == $BOTH))
	{
		my $v1=$mp3->{ID3v1};
		print(join(':',$file,"id3v1",
				   def($v1->track),def($v1->artist),
				   def($v1->album),def($v1->song),
				   def($v1->genre),def($v1->year),
				   def($v1->comment)),"\n");
	}

	if((exists($mp3->{ID3v2})) && ($ver==$V2 || $ver == $BOTH))
	{
		my $v2=$mp3->{ID3v2};
		my $v2comment="";
		my ($info,$name)=$v2->get_frame("COMM");
		if(defined($info) && ref($info) && exists($info->{Text}))
		{
			$v2comment=$info->{Text};
		}
		print(join(':',$file,"id3v2",
				   def($v2->track),def($v2->artist),
				   def($v2->album),def($v2->song),
				   def(($v2->get_frame("TCON"))[0]), #genre
				   def(($v2->get_frame("TYER"))[0]), # year
				   $v2comment),"\n");

		if($extended || $raw)
		{
			my $frameIDs_hash = $v2->get_frame_ids;
			my $outerfirst=1;
			foreach my $frame (keys %$frameIDs_hash)
			{
				next unless(isext($frame) || $raw);
				my ($info, $name) = $mp3->{ID3v2}->get_frame($frame);
				if($outerfirst) { $outerfirst=0; } else { tprint($prefix,":"); }
				if (ref $info)
				{
					tprint($prefix,"$frame:<");
					my $innerfirst=1;
					while(my ($key,$val)=each %$info)
					{
						if($key =~ /_Data/i) { $val="<data>" }
						if($innerfirst) { $innerfirst=0; } else { tprint($prefix,":"); }
						tprint($prefix,"$key=$val"); 
					}
					tprint($prefix,">");
				}
				else
				{
					tprint($prefix,"$frame=$info");
				}
			}
			tprint($prefix,"\n"); 
		}
	}
}

# helper function used by printterse to handling pretty output
sub tprint
{
	my $LINELEN=75;
	my ($prefix,$msg)=@_;
	if($msg eq "\n")
	{
		if(length($curmsg)) { print "$prefix$curmsg\n"; }
		$curmsg="";
		return;
	}
	# let short msgs through to avoid > on a line by itself etc
	if(length($msg)<2)
	{
		$curmsg.=$msg;
		return;
	}

	if(length($curmsg)>$LINELEN)
	{
		print "$prefix$curmsg\n";
		$curmsg="";
		# to iterate is human...
		tprint($prefix,$msg);
		return;
	}

	if(length($msg)>$LINELEN)
	{
		if(length($curmsg)) {  print "$prefix$curmsg\n"; }
		$curmsg=$msg;
		return;
	}
	
	if((length($msg)+length($curmsg))>$LINELEN)
	{
		if($curmsg ne "") { print "$prefix$curmsg\n"; }
		$curmsg="";
		# ... to recurse, divine.
		tprint($prefix,$msg);
		return;
	}

	$curmsg .= $msg;
}

# helper function used by printframetypes to handle pretty output
sub printwrap
{
	my($str,$delim,$prefix)=@_;
	unless(defined($prefix)) { $prefix=""; }
	unless(length($printwrapstr)) { $printwrapstr=$prefix; }
	if(($str eq "\n") && (length($printwrapstr)))
	{
		print "$printwrapstr\n";
		$printwrapstr=$prefix;
		return;
	}
	if(length($printwrapstr)>75)
	{
		print $printwrapstr;
		print $delim if defined($delim);
		print "\n";
		$printwrapstr=$prefix;
	}
	if(($printwrapstr ne $prefix) && defined($delim)) { $str = $delim . $str; }
	$printwrapstr .= $str;
}

# dumps a data field to a file, attempting to use mime type for
# extension if applicable. returns name of dumpfile
sub dumpdata
{
	my($file,$mime,$data,$count)=@_;
	$file=~s/(?:.*\/)?(.*)\..*/$1/;
	$file .= ".$$count";
	$$count++;
	if(defined($mime))
	{
		my $ext=mimetoext($mime);
		if(defined($ext) && length($ext)) { $file .= ".$ext"; }
	}
	unless(open(DATAFILE,">$file"))
	{
		warn("$me: cannot open $file: $!\n");
		return undef;
	}
	print DATAFILE $data;
	unless(close(DATAFILE))
	{
		warn("$me: cannot close $file: $!\n");
		return undef;
	}
	return $file;
}

# reads the data from $file and returns it as a scalar or undef on failure
sub readfile
{
	my $file=shift;
	my $data=undef;
	my $block="";
	if(open(D,$file))
	{
		local $/=undef;
		$data=<D>;
		close(D);
	}
	else { warn("$me: $file: cannot read: $!\n"); }
	return $data;
}

# escape non printing characters
# converts eg chr(2) => "\2"
sub escape
{
	my $s=shift;
	$s=~s/([^[:print:]])/sprintf("\\%d",ord($1))/eg;
	return $s;
}

# converts mime type to filename extension
sub mimetoext
{
	my $mime=shift;
	$mime="application/octet-stream" unless defined($mime);
	# see after __DATA__ for mime data
	unless(defined($mimetoext))
	{
		($mimetoext,$exttomime)=initmime();
	}
	if(exists($mimetoext->{$mime})) { return $mimetoext->{$mime} };
	return($mimetoext->{"application/octet-stream"});
}

# converts filename extension to mime type
sub exttomime
{
	my $ext=shift;
	$ext="" unless defined($ext);
	$ext=lc($ext);
	# see after __DATA__ for mime data
	unless(defined($exttomime))
	{
		($mimetoext,$exttomime)=initmime();
	}
	if(exists($exttomime->{$ext})) { return $exttomime->{$ext} };
	return("application/octet-stream");
}

# called to initialise mime datastructures from data after __DATA__
sub initmime
{
	while(<DATA>)
	{
		chomp;
		next if(/^\s*$/);
		next if(/^\s*\#/);
		if(/^(\S+)\s+(.+)/)
		{
			my @exts=map lc, split(/\s+/,$2);
			$mimetoext->{$1}=$exts[0];
			foreach my $ext (@exts)
			{
				unless(exists($exttomime->{$ext}))
				{
					$exttomime->{$ext}=$1;
				}
			}
		}
		else
		{
			die("$me: initmime: internal error\n");
		}
	}
	return($mimetoext,$exttomime);
}

# returns 1 if frame has a binary element (begins with _, eg _Data).
sub isbinary
{
	my ($mp3,$frame)=@_;
	my ($info, $name) = $mp3->{ID3v2}->get_frame($frame);
	if (ref $info)
	{
		# tag starts with '_' => binary (probably)
		while(my ($key,$val)=each %$info)
		{
			if($key =~ /^_/i) { return 1; }
		}
	}
	return 0;
}

# returns 1 if $name is that of an extended tag
# (ie one with no id3v1 equivalent)
sub isext
{
	my $name=shift;
	if(($name eq "TRCK") || # Tracknumber
	   ($name eq "TPE1") || # artist
	   ($name eq "TPE2") || # alt artist
	   ($name eq "TALB") || # album
	   ($name eq "TIT2") || # title
	   ($name eq "TCON") || # genre (content type)
	   ($name eq "TYER") || # year
	   ($name eq "COMM"))   # comment
	{
		return 0;
	}
	return 1;
}

# returns 1 if $it is empty, just whitespace,
# or a number equal to 0 or 255 (to catch genre/tracknum)
sub isblank
{
	my $it=shift;
	if(!defined($it)) { return 1; }
	if($it=~/^\s*$/)  { return 1; }
	# year and genre
	if(($it=~/^\d+$/) && (($it==0) || ($it==255))) { return 1; }
	return 0;
}

# pops a character off $a
# returns ($a, $rest)
# used by do_parseopts
sub popchar
{
	my $a=shift;
	unless(defined($a)) { return((undef,undef)); }
	return($a=~/^(.)(.*)/);
}

# returns $it or the empty string("") if $it is undefined
sub def
{
	my $it=shift;
	if(!defined($it)) { return ""; }
	return $it;
}

# prints overly-compressed usage info
sub usage
{
	die("Usage:\n".
		"$me  -[12qtvxXh] -A <artist> -L <album> -T <track> -N <tracknum>\n".
		"        -G <genre> -Y <year> -C <comment> -ap <file.jpg> -rp <file.gif>\n".
		"        -p[aNALTGYCpPfF] -[iI] file.id3 -d[axbpzNALTGYC] -[fF] -df <frame>\n".
        "        -e[axbp] -ef <frame> -c[12bcC]\n".
		"        -af <frame=data> -af <frame:key1=value:key2=:key3=file(file.dat)>\n".
		"        -rf <frame=data> -rf <frame:key1=value:key2=:key3=file(file.dat)>\n".
        "        --  <file.mp3>...\n".
		"-1, -2         View/set ID3v1/ID3v2 only.\n".
		"-q,-t,-v       Quiet (default if modifying), Terse, Verbose (default)\n".
		"-x, -X         View extended ID3v2 frames. -X = show all as extended\n".
		"-A <artist>    Set Artist\n".
		"-L <album>     Set Album\n".
		"-T <track>     Set trackname (song title)\n".
		"-N <num>       Set track number\n".
		"-G <genre>     Set genre\n".
		"-Y <year>      Set year\n".
		"-C <comment>   Set comment\n".
		"-ap <picfile>  Add picture\n".
		"-rp <picfile>  Replace picture\n".
		"-[iI] <file.id3> import dumped(-ea) data from file.id3. -I=del imported files\n".
		"-c[12bcC]      -c[12b]=copy frames from ID3v1,ID3v2,best\n".
		"               -c[Cc]=(strict) compare\n".
        "-e[abp]        extract ID3v2 frames, a=all b=binary p=picture\n".
		"-ef <frame>    extract named ID3v2 frame\n".
		"-df <frame>    delete named ID3v2 frame\n".
        "-d[axbpzNALTGYC] delete a=all x=extended frames b=binary frames p=pictures\n".
		"                        N=tracknum A=artist L=album T=track G=genre\n".
		"                        Y=year C=comment z=empty tags\n".
		"-p[aNALTGYCpf]   prompt a=all p=picture f=frame\n".
		"                        N=tracknum A=artist L=album T=track G=genre\n".
		"                        Y=year C=comment\n".
		"-af <frame=data> add simple frame\n".
		"-rf <frame=data> replace simple frame\n".
		"-af <frame:key1=value:key2=:key3=file(file.dat)>  add complex frame\n".
		"-rf <frame:key1=value:key2=:key3=file(file.dat)>  replace complex frame\n".
		"-[fF] list available ID3v2 frame types. -F=more details\n".
        "-h             This help\n".
        "--             End of options\n".
		"\nSee the man page for mp3id(1) for more details\n");
}
		
=head1 NAME

mp3id - displays/edits id3 metadata (id3v1 and id3v2) in mp3 files

=head1 SYNOPSIS

B<mp3id> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<->[B<12qtvxXh>] B<-c>[B<12bcC>] B<--> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-A> I<E<lt>artistE<gt>> B<-L> I<E<lt>albumE<gt>> B<-T> I<E<lt>trackE<gt>> B<-N> I<E<lt>tracknumE<gt>>  I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-G> I<E<lt>genreE<gt>> B<-Y> I<E<lt>yearE<gt>> B<-C E<lt>commentE<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-ap> I<E<lt>file.jpgE<gt>> B<-rp> I<E<lt>file.gifE<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-p>[B<aNALTGYCpPfF>] B<->[B<iI>] I<E<lt>file.id3E<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-d>[B<axbpzNALTGYC>] B<-df> I<E<lt>frameE<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-e>[B<abp>] B<-ef> I<E<lt>frameE<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-af> I<E<lt>frame=dataE<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-af> I<E<lt>frame:key1=val:key2=:key3=file(file.dat)E<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-rf> I<E<lt>frame=dataE<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<-rf> I<E<lt>frame:key1=val:key2=:key3=file(file.dat)E<gt>> I<E<lt>file.mp3E<gt>...>

B<mp3id> B<->[B<Ff>]

=head1 DESCRIPTION

mp3id is a command-line viewer and editor for id3 metadata in mp3
files. ID3v1 and ID3v2 are both supported, including access to
extended ID3v2 metadata (pictures, lyrics, etc).

=head1 OPTIONS

Options are processed in order, so you could do eg:

 mp3id -v -A Artist -L Album -N 1 -T Foo foo.mp3 -N 2 -T Bar bar.mp3

Any options involving B<N>, B<A>, B<L>, B<T>, B<G>, B<Y> or B<C> refer
to trackB<N>umber, B<A>rtist, aB<L>bum, B<T>itle, B<G>enre, B<Y>ear or
B<C>omment, respectively.

=head2 General Options

=over 4

=item B<-1>, B<-2>

Show/modify ID3v1/ID3v2 tags only. By default it does both v1 and v2
tags.

=item B<-q>

Quiet (no output). This is the default if any changes are being made.

=item B<-t>

Terse output.

=item B<-v>

Verbose output. This is the default if no options are supplied.

=item B<-x>, B<-X>

View extended ID3v2 frames. B<-X> shows all ID3v2 frames as extended
frames, including the basic ones (artist, album, etc).

If terse output (B<-t>) is selected, an attempt to summarise the
extended frames as tersely as possible is made.

Extended frames are separated by B<:>. Simple extended frames are of
the form C<framename=value>.

Complex frames have a leading frame name, then a B<:>. The elements of
the frame are enclosed in angle brackets (B<E<lt>E<gt>>), separated by
B<:>, and are displayed in the form C<elementname=value>. The contents
of binary frames (eg B<_Data>) is displayed as C<E<lt>dataE<gt>>

For instance, one simple and one complex frame would be displayed as eg:

 filename:ext:TSIZ=2000:APIC:<Picture Type=Other:_Data=<data>:MIME type=image/gif:
 filename:ext:Description=file.gif>

=item B<-f>

Summarise available ID3v2 frame types.

=item B<-F>

Show a detailed listing of available ID3v2 frame types.. This is
several pages long, so you may wish to pipe it to a pager such as
L<more(1)>.

=item B<-h>

Shows a summary of command line options.

=item B<-->

Treat remaining command line arguments as files to operate on, not
options.

=back

=head2 Options to set basic metadata (artist, album, etc)

=over 4

=item B<-A> I<artist>

Set Artist

=item B<-L> I<album>

Set aLbum

=item B<-T> I<track>

Set Trackname (song title)

=item B<-N> I<num>

Set trackNumber

=item B<-G> I<genre>

Set Genre

=item B<-Y> I<year>

Set Year

=item B<-C> I<comment>

Set Comment

=back

=head2 Options to add/replace extended frames

To find out about available frame formats,
see L<MP3::Tag::ID3v2-Data(1)>.

For a quick reference, use B<mp3id -f>.

=over 4

=item B<-ap> I<picfile>

Add picture.

=item B<-rp> I<picfile>

Replace picture. This will only replace the first APIC frame. If there
is more than one APIC frame in the file, you will need to use the
B<-rf> option, below.

=item B<-af> I<'frame=data'>

Add a simple frame. The data field can be empty.

If necessary, you should enclose the frame spec in single quotes
(B<'>), especially if there are spaces or other characters that will
be interpreted by the shell.

=item B<-af> I<'frame:key1=value:key2=:key3=file(file.dat)'>

Add a complex frame. The format is the B<framename>, then a colon,
then a list of B<fieldname=value>, separated by colons. Field values
can be left blank.

If you want the contents of a file included in the field (useful for
binary B<_Data> fields), surround the I<filename> with B<file()>,
e.g. C<_Data=file(picture.gif)>.

If necessary, you should enclose the frame spec in single quotes
(B<'>), especially if there are spaces or other characters that will
be interpreted by the shell.

If all this is too much hassle, try B<-pf> to enter frame details
interactively, with prompting.

=item B<-rf> I<'frame=data'>

Replace a simple frame. The data field can be empty.

=item B<-rf> I<'frame:key1=value:key2=:key3=file(file.dat)'>

Replace or merge a complex frame.

The frame format is the same for adding frames (B<-af>, above).

If you omit any fields and the frame already exists, they will be
taken from the existing version of the frame, otherwise it is an error.

=back

=head2 Importing and Exporting data

When writing files containing pictures or binary frame data, the
filename is created from the basename of the file (ie with the
extension (eg .mp3) lopped off), then a 2 digit number to distinguish
multiple frames from the same file, then a guessed extension. If the
frame contains a B<MIME type> field, the extension will be guessed from
the MIME type, if not it will default to F<.bin>.

For instance, the contents of the _Data field of the first APIC frame
with MIME type C<image/jpeg> in F<foo.mp3> will be written to
F<foo.01.jpg>.

=over 4

=item B<-i> I<file.id3>, B<-I> I<file.id3>

Import data dumped with the B<-ea> option (see below) from from
file.id3.

B<-I> deletes the files you have imported, B<including> any files
included using the B<file()> syntax.

The format for the B<.id3> export file is essentially the same as that
displayed by B<mp3id -vx>, except that instead of using
B<E<lt>dataE<gt>> as a placeholder for binary frame data (B<_Data>
etc), it uses the B<file()> notation to include data from a file.  See
the B<-af> option above for more details on the B<file()> notation.

Not all fields need to be present in the import file, it will only
alter those mentioned in it.

=item B<-ea>

Dump (export) all id3 tag data to a file. The file will be have the
same basename as the input file, but with an extension of C<.id3>. For
example, C<mp3id -ea foo.mp3> creates the file F<foo.id3>.

This file can be edited then imported using the B<-i> option (see
above).

=item B<-eb>

Extract all binary data stored in frames.

Some frames (eg APIC, the frame for storing pictures) have elements
that begin with B<_>, usually C<_Data>. These elements are used for
storing binary data (eg picture data). This extracts the content of
the binary data element for each frame that contains one.

=item B<-ep>

Extract all pictures from file.

=item B<-ef> I<frame>

Extract named ID3v2 frame.

=back

=head2 Deleting data

=over 4

=item B<-df> I<frames>

Delete named frame

=item B<-da>

Delete id3 tags.

=item B<-dx>

Delete extended frames.

=item B<-db>

Delete binary frames (frames containing elements starting with B<_>,
eg B<_Data>, which contain binary data).

=item B<-dp>

Delete pictures. This will delete all APIC ID3v2 frames. If you only
want to delete the first picture, use:

 mp3id -df APIC file.mp3

=item B<-dz>

Delete id3 tags that contain no data.

=item B<-dN>

Delete trackNumber.

=item B<-dA>

Delete Artist.

=item B<-dL>

Delete aLbum

=item B<-dT>

Delete Track name (song title).

=item B<-dG>

Delete Genre.

=item B<-dY>

Delete Year.

=item B<-dC>

Delete Comment.

=back

=head2 Prompting

=over 4

=item B<-pa>

Prompt for all items.

=item B<-pN>

Prompt for track Number.

=item B<-pA>

Prompt for Artist.

=item B<-pL>

Prompt for aLbum.

=item B<-pT>

Prompt for Track name (song title).

=item B<-pG>

Prompt for Genre.

=item B<-pY>

Prompt for Year.

=item B<-pC>

Prompt for Comment.

=item B<-pp>

Prompt for a picture to add.

=item B<-pf>

Prompt for an arbitrary frame to add (simple or complex).

=back

=head2 Copying and Comparing frames

=over 4

=item B<-c1>

Copy data from ID3v1 tag to ID3v2 tag.

=item B<-c2>

Copy data from ID3v2 tag to ID3v1 tag.

=item B<-cb>

Copy data from best source. If an ID3v2 frame is set and non-empty,
sets the ID3v1 tag data to the same value, otherwise attempts to do
the reverse.

=item B<-cc>, B<-cC>

Compares ID3v1 and ID3v2 data. 

If B<-cc> is used, it allows for truncation caused by limited-length
ID3v1 tags, and only compares up to the length of the ID3v1 tag data.

B<-cC> is stricter, and complains if there is any difference between
the tags.

=back

=head1 EXAMPLES

=over 4

=item B<mp3id file.mp3>   

Display id3 tags in F<file.mp3> (same as B<mp3id -v file.mp3>).

=item B<mp3id -vx file.mp3>  

Display id3 tags in F<file.mp3>, including extended id3v2 tags.

=item B<mp3id -2 -da *.mp3>   

Delete all id3v2 tags in all mp3 files in current directory.

=item B<mp3id -A artist -T album -pN -pT *.mp3>   

In all files in the current directory, set the artist to B<artist>,
the album to B<album>, then for each file prompt for the tracknumber
and track title.

=item B<mp3id -c2 file.mp3>   

Copy id3v2 tag to id3v1 tag. Some data may be truncated due to
limitations of the id3v1 format. See L<MP3::Tag::ID3v1(3)> for
details on the lengths of id3v1 tag data.

=item B<mp3id -cc file.mp3>   

Compare id3v1 and id3v2 tags and complain if they don't match.

=item B<mp3id -ea file.mp3>   

Extract all tag data from F<file.mp3> and write it in
human-readable/editable format to F<file.id3>

=item B<mp3id -i file.id3 file.mp3>   

Import the tag data from F<file.id3> to F<file.mp3>

=item B<mp3id -af 'COMM:Language=ENG:short=:Text=Hello World' file.mp3>   

This is the long-winded way of adding a comment. This is equivalent to
B<mp3id -C "Hello World" file.mp3>.	

=item B<mp3id -ap pic.jpg file.mp3>   

Adds the picture F<pic.jpg> to file.mp3. If there is already an APIC
(picture) frame present, this will be added as (eg) APIC01.

=item B<mp3id -af 'APIC:MIME type=image/jpeg:Picture type=Other:Description=A picture:_Data=file(pic.jpg)' file.mp3>   

This is the long-winded way of adding a picture. It is equivalent to
the B<-ap> example above (except that B<-ap> sets the Description
field to the filename of the picture).

Note the use of B<file()> to import the picture data from F<pic.jpg>.

=item B<mp3id -- -filename.mp3>   

Views the tags in F<-filename>. The B<--> option stops mp3id from
treating B<-filename> as an option.

=item B<mp3id -f>   

View a summary of available ID3v2 tags and their associated data
elements.

=item B<mp3id -F E<verbar> less>   

View a detailed list of available ID3v2 tags and their associated data
elements, piping through L<less(1)> to show a page at a time.

=back

=head1 ACKNOWLEDGEMENTS

This program makes heavy use of the perl module L<MP3::Tag(3)>, by
Thomas Geffert.

=head1 BUGS

None known. Please report any found to ianb@nessie.mcc.ac.uk	

=head1 SEE ALSO    

L<mp3-archive-tools(1)>, L<MP3::Tag(3)>, L<MP3::Tag::ID3v2-Data(1)>,
L<mp3lint(1)>

=head1 AUTHOR

Ian Beckwith <ianb@nessie.mcc.ac.uk>

=head1 AVAILABILITY

mp3id is part of the mp3-archive-tools package.

The latest version can be found at:

B<http://nessie.mcc.ac.uk/~ianb/projects/mp3-archive-tools/>

=head1 COPYRIGHT

Copyright 2003 Ian Beckwith <ianb@nessie.mcc.ac.uk>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or (at
your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

=cut

__DATA__

########################################################################
# cut down and tweaked from debian mime-support 3.23-1 /etc/mime/types #
########################################################################

application/octet-stream						bin
application/andrew-inset						ez
application/cu-seeme							csm cu
application/dsptype								tsp
application/futuresplash						spl
application/hta									hta
application/mac-binhex40						hqx
application/mathematica							nb
application/msaccess							mdb
application/msword								doc dot
application/oda									oda
application/ogg									ogg
application/pics-rules							prf
application/pgp-keys							key
application/pdf									pdf
application/pgp-signature						pgp
application/postscript							ps ai eps
application/rss+xml								rss
application/rtf									rtf
application/smil								smi smil
application/wordperfect5.1						wp5
application/xhtml+xml							xhtml xht
application/zip									zip
application/vnd.cinderella						cdy
application/vnd.ms-excel						xls xlb
application/vnd.ms-pki.seccat					cat
application/vnd.ms-pki.stl						stl
application/vnd.ms-powerpoint					ppt pps pot
application/vnd.stardivision.calc				sdc
application/vnd.stardivision.draw				sda
application/vnd.stardivision.impress			sdd sdp
application/vnd.stardivision.math				smf
application/vnd.stardivision.writer				sdw vor
application/vnd.stardivision.writer-global		sgl
application/vnd.sun.xml.calc					sxc
application/vnd.sun.xml.calc.template			stc
application/vnd.sun.xml.draw					sxd
application/vnd.sun.xml.draw.template			std
application/vnd.sun.xml.impress					sxi
application/vnd.sun.xml.impress.template		sti
application/vnd.sun.xml.math					sxm
application/vnd.sun.xml.writer					sxw
application/vnd.sun.xml.writer.global			sxg
application/vnd.sun.xml.writer.template			stw
application/vnd.symbian.install					sis
application/vnd.wap.wbxml						wbxml
application/vnd.wap.wmlc						wmlc
application/vnd.wap.wmlscriptc					wmlsc
application/x-123								wk
application/x-apple-diskimage					dmg
application/x-bcpio								bcpio
application/x-bittorrent						torrent
application/x-cdf								cdf
application/x-cdlink							vcd
application/x-chess-pgn							pgn
application/x-cpio								cpio
application/x-csh								csh
application/x-debian-package					deb
application/x-director							dcr dir dxr
application/x-doom								wad
application/x-dms								dms
application/x-dvi								dvi
application/x-font								pfa pfb gsf pcf pcf.Z
application/x-futuresplash						spl
application/x-gnumeric							gnumeric
application/x-go-sgf							sgf
application/x-graphing-calculator				gcf
application/x-gtar								gtar tgz taz
application/x-hdf								hdf
application/x-httpd-php							phtml pht php
application/x-httpd-php-source					phps
application/x-httpd-php3						php3
application/x-httpd-php3-preprocessed			php3p
application/x-httpd-php4						php4
application/x-ica								ica
application/x-internet-signup					ins isp
application/x-iphone							iii
application/x-java-applet           			applet
application/x-java-archive						jar
application/x-java-jnlp-file					jnlp
application/x-java-serialized-object			ser
application/x-java-vm							class
application/x-javascript						js
application/x-kchart							chrt
application/x-killustrator						kil
application/x-kpresenter						kpr kpt
application/x-koan								skp skd skt skm
application/x-kspread							ksp
application/x-kword								kwd kwt
application/x-latex								latex
application/x-lha								lha
application/x-lzh								lzh
application/x-lzx								lzx
application/x-maker								frm maker frame fm fb book fbdoc
application/x-mif								mif
application/x-ms-wmz							wmz
application/x-ms-wmd							wmd
application/x-msdos-program						exe com bat dll
application/x-msi								msi
application/x-netcdf							nc
application/x-ns-proxy-autoconfig				pac
application/x-nwc								nwc
application/x-object							o
application/x-oz-application					oza
application/x-perl								pl pm
application/x-pkcs7-certreqresp					p7r
application/x-pkcs7-crl							crl
application/x-quicktimeplayer					qtl
application/x-shar								shar
application/x-shockwave-flash					swf swfl
application/x-sh								sh
application/x-stuffit							sit
application/x-sv4cpio							sv4cpio
application/x-sv4crc							sv4crc
application/x-tar								tar
application/x-tcl								tcl
application/x-tex								tex
application/x-tex-gf							gf
application/x-tex-pk							pk
application/x-texinfo							texinfo texi
application/x-trash								bak ~ % old sik
application/x-troff								troff t tr roff
application/x-troff-man							man
application/x-troff-me							me
application/x-troff-ms							ms
application/x-ustar								ustar
application/x-wais-source						src
application/x-wingz								wz
application/x-x509-ca-cert						crt
application/x-xfig								fig
audio/basic										au snd
audio/midi										midi mid kar
audio/mpeg										mp3 mpga mpega mp2
audio/prs.sid									sid
audio/x-aiff									aiff aif aifc
audio/x-gsm										gsm
audio/x-mpegurl									m3u
audio/x-ms-wma									wma
audio/x-ms-wax									wax
audio/x-pn-realaudio-plugin						rpm
audio/x-pn-realaudio							ra rm ram
audio/x-realaudio								ra
audio/x-scpls									pls
audio/x-sd2										sd2
audio/x-wav										wav
chemical/x-pdb									pdb
chemical/x-xyz									xyz
image/gif										gif
image/ief										ief
image/jpeg										jpg jpeg jpe
image/pcx										pcx
image/png										png
image/svg+xml									svg svgz
image/tiff										tiff tif
image/vnd.wap.wbmp								wbmp
image/x-cmu-raster								ras
image/x-coreldraw								cdr
image/x-coreldrawpattern						pat
image/x-coreldrawtemplate						cdt
image/x-corelphotopaint							cpt
image/x-djvu									djvu djv
image/x-icon									ico
image/x-jg										art
image/x-jng										jng
image/x-ms-bmp									bmp
image/x-photoshop								psd
image/x-portable-anymap							pnm
image/x-portable-bitmap							pbm
image/x-portable-graymap						pgm
image/x-portable-pixmap							ppm
image/x-rgb										rgb
image/x-xbitmap									xbm
image/x-xpixmap									xpm
image/x-xwindowdump								xwd
model/iges										igs iges
model/mesh										msh mesh silo
model/vrml										vrml wrl
text/comma-separated-values						csv
text/css										css
text/h323										323
text/html										htm html
text/iuls										uls
text/mathml										mml
text/plain									    txt asc text diff
text/richtext						   			rtx
text/rtf										rtf
text/scriptlet									sct wsc
text/texmacs									tm ts
text/tab-separated-values						tsv
text/vnd.sun.j2me.app-descriptor				jad
text/vnd.wap.wml								wml
text/vnd.wap.wmlscript							wmls
text/xml										xml xsl
text/x-c++hdr									h++ hpp hxx hh
text/x-c++src									c++ cpp cxx cc
text/x-chdr										h
text/x-csh										csh
text/x-csrc										c
text/x-java										java
text/x-moc										moc
text/x-pascal									p pas
text/x-pcs-gcd									gcd
text/x-server-parsed-html						shtml
text/x-setext									etx
text/x-sh										sh
text/x-tcl										tcl tk
text/x-tex										tex ltx sty cls
text/x-vcalendar								vcs
text/x-vcard									vcf
video/dl										dl
video/fli										fli
video/gl										gl
video/mpeg										mpg mpeg mpe
video/quicktime									qt mov
video/vnd.mpegurl								mxu
video/x-dv										dif dv
video/x-la-asf									lsf lsx
video/x-mng										mng
video/x-ms-asf									asf asx
video/x-ms-wm									wm
video/x-ms-wmv									wmv
video/x-ms-wmx									wmx
video/x-ms-wvx									wvx
video/x-msvideo									avi
video/x-sgi-movie								movie
x-conference/x-cooltalk							ice
# below here are duplicate extensions with different mime-types
x-world/x-vrml			   						vrml vrm wrl
application/x-redhat-package-manager			rpm
audio/wav										wav
audio/aiff										aiff
