Developer Forums | About Us | Site Map
Search  
HOME > TUTORIALS > SERVER SIDE CODING > PERL TUTORIALS > CULTURED PERL: FUN WITH MP3 AND PERL, PART 2


Sponsors





Useful Lists

Web Host
site hosted by netplex

Online Manuals

Cultured Perl: Fun with MP3 and Perl, Part 2
By Teodor Zlatanov - 2004-04-08 Page:  1 2 3 4 5 6

At last, the main loop

For each file given on the command line (in @ARGV), I get the ID3 tag and create it if necessary. For files that can't abide an ID3 tag for various reasons, I print an informative message and skip them. There is a difference here between files that are simply not in existence -- for instance, a directory name given as an MP3 file -- and files that are not accessible, such as a file with insufficient permissions.

Listing 7. Get the ID3 tag

foreach my $file (@ARGV)
{
 my $tag = get_tag($file, 1);

 unless (defined $tag)
 {
  if (-r $file && -f $file)
  {
   print "Could not get a tag from file $file, skipping";
  }
  else
  {
   print "Nonexistent file $file, skipping";
  }
  next;
 }
... the rest of this loop is explained later ...
}

The %discs_of_interest hash is a copy of %discs. I tried using modules for approximate (fuzzy) string matching to narrow the selection of disks that are interesting. For instance, I tried matching the album name fuzzily (with 50% to 90% precision), and no setting worked well. The problem is that some words like "love" are very common, while other words like "U2" are too short. There may be a good algorithm to narrow choices, and I've left the %discs_of_interest hash in place in case that algorithm comes about, but it seems from my personal experience that the best thing to do is let the human brain pick an option in 0.01 seconds. Sometimes, trying to solve a problem with a computer is simply not as efficient as a few million years of evolution.0

Now comes the while(1) loop. This is an endless loop that reflects how the user often will cycle between choices until he's made only one. I could have written this loop with variable controls, but using next() and last() in an endless loop seemed more natural.

I get a single album with the following loop:

Listing 8. Choosing only one album

my @chosen = ();

# do the following unless only one album is selected
if (1 == scalar keys %discs_of_interest)
{
 @chosen = (keys %discs_of_interest)[0];
}
else
{
 # get the ask4discurls special format back from %olddiscinfo
 my %ask4discurls_special_hash;

 foreach (keys %discs)
 {
  $ask4discurls_special_hash{$_} = $olddiscinfo{$_};
 }

 do
 {
  print_tag_info($file, $tag);
  print "Choose a single album or none (to skip file) from the current list\n";
  @chosen = $cddb->ask4discurls(\%ask4discurls_special_hash);
 } while (scalar @chosen > 1);
};

last if scalar @chosen == 0;
next if scalar @chosen != 1;
my $disc = $discs{$chosen[0]};

If only one album exists anyhow, I just take it. Otherwise, I get the lists of disks of interest using the ask4discurls() function. Note that I print out the file tag info before that question with print_tag_info(), so the user is reminded of the file's information. Users are naturally forgetful, so every shortcut and reminder the programmer can offer them is appreciated. Users are also imperfect, so I don't assume that just because I told them to pick one album, that they did so. With a GUI, similar selection rules can be enforced on listboxes -- but in the text interface autotag.pl has, input validation has to be done this way. Actually, that's not entirely true: There are some CPAN modules that can help here, but the scale and scope of autotag.pl did not seem to merit a text-mode UI framework.

If no album was chosen, I skip to the next file.

Now, $disc contains the album that is specifically applicable to the current file the user is examining.

Listing 9. The track number is hard to find

my $track_number_guess = guess_track_number($file, $tag);

my $tracks = $disc->{trackinfo};
my $track_number;
do
{
 # ask the user for the track number, while trying to be helpful
 print_tag_info($file, $tag, "Old tag");
 $cddb->outstd($disc);
 $track_number =
  read_line(
   sprintf(
    'Choose a track number 1 - %d, 0 to quit, -1 to select another album: ',
    scalar @$tracks),
   $track_number_guess);
} while (not defined $track_number ||
  	     $track_number < -1 ||
	     $track_number > scalar @$tracks);

# cycle to the album selection again if the user wants to select another album
next if $track_number == -1;

Another endless loop: Until the user has picked a suitable track number, I simply can't go on. The track number is like Moby Dick to autotag.pl's Ahab. It must be found, or else all this work is meaningless. I print out the tag info again to remind the user what it is we're talking about, then print out the disk information with the outstd() function from WebService::FreeDB.

The default track number the user can enter is guessed from the file name or from the previously existing track. This is always just a suggestion, but if the user wants to accept it he can just hit Enter. Now that's service.

When the track number is found, and it's good, the tagging of the MP3 file is done:

Listing 10. The tagging is done

# if the user selected a track...
if ($track_number > 0)
{
 my $new_tag = make_tag_from_freedb($disc, $track_number);
 print_tag_info($file, $new_tag, "New tag info") if defined $new_tag;
 # do this if the new tag was created, DRYRUN was not specified, and the
 # user says YES
 if ($new_tag
     && !$config->DRYRUN()
     && read_yes_no(
"Apply new tag (you'll get a chance to modify it)?", 1))
 {
  my $modify_tags = read_yes_no("Modify tag elements?", 0);
  # copy each new element (but don't overwrite valid old ones)
  foreach my $element (keys %$new_tag)
  {
   my $old_tag_element = $tag->{$element} || '';

   if ($modify_tags)
   {
    # the user can press Up Arrow to get the old tag element
    $term->addhistory($old_tag_element);
    $new_tag->{$element} =
     read_line("New value of $element (was '$old_tag_element'): ",
	 $new_tag->{$element});

    # put the artist and album $new_tag changes back in $disc so the
    # next file can also use them

    if (exists $info2freedb{$element})
    {
     $disc->{$info2freedb{$element}} = $new_tag->{$element};
    }
   }

   $tag->{$element} = $new_tag->{$element};
  }
  set_tag ($file, $tag);
 }				# if apply_new_tag...
}				# if $track_number > 0
last;

First of all, recall I'm in an endless while(1) loop here. The last() at the end means that if I got this far, I should exit the loop.

I start with the make_tag_from_freedb() function to make an ID3 tag from the FreeDB tag. This is hidden into a function because it's not a straightforward mapping.

Given the new tag and a final "yes" from the user, I proceed to the tagging of the file. The user now has a chance to modify each individual tag element. Each choice there is stored in the input object history (read the Term::Readline documentation for details). That way, the user can press Up Arrow and grab the old inputs instead of having to retype them. Finally, and this really makes the user's life easier when tagging multiple files, I store the modified information that should persist back in the %$disc hash. Thus, if a user modifies the artist of an album, on the next file the modified name will now be the default name.



View Cultured Perl: Fun with MP3 and Perl, Part 2 Discussion

Page:  1 2 3 4 5 6 Next Page: Usage

First published by IBM developerWorks


Copyright 2004-2017 GrindingGears.com. All rights reserved.
Article copyright and all rights retained by the author.