#! /usr/bin/env perl # $Id$ =head1 NAME B - transcode FLAC file to MP3 files =head1 SYNOPSIS B [B<--lame-options> I] [B<-j> I] [B<-q>] [B<-v>] I [...] =head1 DESCRIPTION B transcodes the FLAC files I to MP3 files. I may be the kind of FLAC file B generates. That is, it contains a cue sheet, one TITLE tag per track listed therein, and ARTIST, ALBUM, and DATE tags. =cut package Jobs; use strict; use warnings; use Errno; use POSIX ':sys_wait_h'; sub newjob { my $f = shift; my $jobs = shift; my $debug = shift; my $pid; if (not $debug) { $pid = fork(); if (not defined($pid)) { die("fork: $!"); } } if ($debug or $pid == 0) { exit($f->()); } if ($pid == 0) { exit($f->()); } push(@$jobs, $pid); return $pid; } sub deljob { my $pid = shift; my $status = shift; my $jobs = shift; for (my $i = 0; $i <= $#$jobs; $i++) { if ($pid == $jobs->[$i]) { splice(@$jobs, $i, 1); last; } } return ($pid, $status); } sub run { my %o = @_; my $maxjobs = $o{'max-jobs'}; my $get_job = $o{'get-job'}; my $notify_start = $o{'notify-start'}; my $notify_finish = $o{'notify-finish'}; my @jobs; my $pid; # Call notifier function if given. sub call { my $f = shift or return; ref($f) eq 'CODE' or return; $f->(@_); } while (1) { if (@jobs < $maxjobs) { my $job; while (defined($job = $get_job->())) { $pid = newjob($job, \@jobs, $o{'debug'}); call($notify_start, $pid, @jobs); @jobs < $maxjobs or last; } # No jobs running and get-job returned undef; we're finished. if (@jobs == 0 and not defined($job)) { return; } } # Now running as many jobs as we can, block waiting for one to die. do { $pid = waitpid(-1, 0); } while ($pid == 0 or ($pid == -1 and ($!{ECHILD} or $!{EINTR}))); $pid == -1 and die("waitpid(-1): $!"); # Before starting more, see if any others have finished. do { call($notify_finish, deljob($pid, $?, \@jobs), @jobs); } while (($pid = waitpid(-1, WNOHANG)) > 0); if ($pid == -1) { $!{ECHILD} or $!{EINTR} or die("waitpid(-1): $!"); } } } ################################################################################ package main; use strict; use warnings; use POSIX ':sys_wait_h'; use Pod::Usage; use Getopt::Long qw(:config gnu_getopt no_ignore_case); my $flac_options; my $lame_options; my $quiet; my $verbose; sub run_or_die { my $command = shift; my $status; $verbose and print(STDERR "$command\n"); $status = system($command); if (WIFEXITED($status)) { if (($status = WEXITSTATUS($status)) != 0) { die("$command exited with status $status"); } } elsif (WIFSIGNALED($status)) { die("$command killed with signal ", WTERMSIG($status)); } elsif (WIFSTOPPED($status)) { die("$command stopped with signal ", WSTOPSIG($status)); } else { die("Major horkage on system($command): \$? = $? \$! = $!"); } } sub tformat { return sprintf('%02d:%02d.%02d', @_); } sub get_decode_args { my $fn = shift; my @l; open(F, '-|', 'metaflac', '--export-cuesheet-to=-', $fn); while () { /INDEX 01 (\d\d):(\d\d):(\d\d)$/ or next; push(@l, [$1, $2, $3]); } my @args; for my $i (0..$#l) { my $arg = ["--skip=" . tformat(@{$l[$i]})]; my $next = $l[$i+1]; if (defined($next)) { if ($next->[2] == 0) { if ($next->[1] == 0) { push(@$arg, '--until=' . tformat($next->[0] - 1, 59, 74)); } else { push(@$arg, '--until=' . tformat($next->[0], $next->[1] - 1, 74)); } } else { push(@$arg, '--until=' . tformat($next->[0], $next->[1], $next->[2] - 1)); } } push(@args, $arg); } # If no cue sheet, stick a dummy in here. if (@args == 0) { @args = ([]); } return @args; } # Return the ARTIST, ALBUM, and DATE tags followed by the TITLE tags # in the file FN. sub get_tags { my $fn = shift; my $artists = shift; my $titles = shift; my $tag; my $value; my $artist; my $album; my $date; my $discnum; my $track; open(TAGS, '-|', 'metaflac', '--export-vc-to=-', $fn) or die("open(metaflac --export-vc-to=- $fn): $!"); while () { chomp; ($tag, $value) = split(/=/, $_, 2); if (/^ARTIST=/i) { $artist = $value; } elsif (/^ALBUM=/i) { $album = $value; } elsif (/^DATE=/i) { $date = $value; } elsif (/^DISCNUMBER=/i) { $discnum = int($value); } elsif (/^ARTIST\[/i) { push(@$artists, $value); } elsif (/^TRACKNUMBER=/i) { $track = $value; # Intentionally don't match the = on this one, to support the # TITLE[1] .. TITLE[n] tag style. } elsif (/^TITLE/i) { push(@$titles, $value); } } close(TAGS) or die("close(metaflac --export-vc-to=- $fn): $?"); # If no TITLEs, stick a dummy in here. if (@$titles == 0) { push(@$titles, undef); } return ($artist, $album, $date, $discnum, $track); } sub arg { my $arg = shift; my $var = shift; if (defined($$var)) { $$var = "$arg '$$var'"; } else { $$var = '' } } sub flac2mp3 { my $fn = shift; my $title = (shift or 'unknown'); my $artist = (shift or 'unknown'); my $album = (shift or 'unknown'); my $date = (shift or 'unknown'); my $track = int(shift); my $skip_arg = shift; my $until_arg = shift; my @tmp; my $outfile; if ($quiet) { $flac_options = '--silent'; } else { $flac_options = ''; } if ($lame_options) { push(@tmp, $lame_options); } else { push(@tmp, '--preset standard'); } $quiet and push(@tmp, '--quiet'); $verbose and push(@tmp, '--verbose'); $lame_options = join(' ', @tmp); # We'll be putting these in single quotes, so we need to escape # any single quotes in the filename by closing the quote ('), # putting an escaped quote (\'), and then reopening the quote ('). for ($fn, $title, $artist, $album, $date) { defined and s/'/'\\''/g; } $outfile = sprintf("$artist ($album) \%02s $title.mp3", $track); $outfile =~ s/\//_/g; arg('--tt', \$title); arg('--ta', \$artist); arg('--tl', \$album); arg('--ty', \$date); arg('--tn', \$track); $skip_arg ||= ''; $until_arg ||= ''; run_or_die(join(' ', "flac $flac_options -cd $skip_arg $until_arg '$fn'", " | lame $lame_options $title $artist $album $date $track", " - '$outfile'")); } MAIN: { my $help; my $debug; my $maxjobs = 1; GetOptions( 'debug|X' => \$debug, 'jobs|j=i' => \$maxjobs, 'lame-options=s', \$lame_options, 'quiet|q' => \$quiet, 'verbose|v' => \$verbose, 'help|h|?' => \$help, ) or pod2usage(); $help and pod2usage(-exitstatus=>0, -verbose=>1); @ARGV > 0 or pod2usage(); my @jobs; for my $fn (@ARGV) { my @args = get_decode_args($fn); my (@artists, @titles); my ($artist, $album, $date, $discnum, $track) = get_tags($fn, \@artists, \@titles); # lame doesn't seem to support disc number. defined($discnum) and $album .= " (disc $discnum)"; # Stupid hack: only a single-track file should have the # TRACKNUMBER tag, so use it if set for the first pass through # the loop. At the end of the loop, we'll set $track for the # next run, so this continues to work for multi-track files. $track ||= 1; for my $i (0..$#titles) { push(@jobs, [$fn, $titles[$i], ($artists[$i] or $artist), $album, $date, $track, @{$args[$i]}]); $track = $i + 2; } } Jobs::run('max-jobs'=>$maxjobs, 'debug'=>$debug, 'get-job'=>sub { my $job = shift(@jobs) or return; return sub { flac2mp3(@$job) } }); } __END__ =head1 OPTIONS =over 4 =item B<--lame-options> I Pass I to B. This ends up being passed to the shell, so feel free to take advantage of that. You'll almost certainly have to put I in single quotes. =item B<-j> [B<--jobs>] I Run up to I jobs instead of the default 1. =item B<-q> [B<--quiet>] Suppress status information. This option is passed along to B and B. =item B<-v> [B<--verbose>] Print diagnostic information. This option is passed along to B and B. =back =head1 AUTHORS Written by Eric Gillespie . =cut # Local variables: # cperl-indent-level: 4 # perl-indent-level: 4 # indent-tabs-mode: nil # End: # vi: set tabstop=4 expandtab: