X-Git-Url: https://diplodocus.org/git/flac-archive/blobdiff_plain/6aa2468770481c0e17a41760600b8a4eaff7a846..fe8996f09b7acb90ff10342f05deeb6c038af045:/flac2mp3 diff --git a/flac2mp3 b/flac2mp3 index 2231b1f..842f0cd 100755 --- a/flac2mp3 +++ b/flac2mp3 @@ -8,27 +8,152 @@ B - transcode FLAC file to MP3 files =head1 SYNOPSIS -B I +B [B<--lame-options> I] [B<-j> I] [B<-q>] [B<-v>] I [...] =head1 DESCRIPTION -B transcodes the FLAC file I to MP3 files. I is -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. +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 { - my $min = shift; - my $sec = shift; - my $hun = shift; - return "$min:$sec.$hun"; + return sprintf('%02d:%02d.%02d', @_); } sub get_decode_args { @@ -48,10 +173,10 @@ sub get_decode_args { if (defined($next)) { if ($next->[2] == 0) { if ($next->[1] == 0) { - push(@$arg, '--until=' . tformat($next->[0] - 1, 59, 99)); + push(@$arg, '--until=' . tformat($next->[0] - 1, 59, 74)); } else { push(@$arg, '--until=' . tformat($next->[0], $next->[1] - 1, - 99)); + 74)); } } else { push(@$arg, '--until=' . tformat($next->[0], $next->[1], @@ -61,6 +186,11 @@ sub get_decode_args { push(@args, $arg); } + # If no cue sheet, stick a dummy in here. + if (@args == 0) { + @args = ([]); + } + return @args; } @@ -68,69 +198,189 @@ sub get_decode_args { # 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 @titles; + my $discnum; + my $track; - open(TAGS, '-|', 'metaflac', '--export-vc-to=-', $fn); + open(TAGS, '-|', 'metaflac', '--export-vc-to=-', $fn) + or die("open(metaflac --export-vc-to=- $fn): $!"); while () { chomp; ($tag, $value) = split(/=/, $_, 2); - if (/^ARTIST=/) { + if (/^ARTIST=/i) { $artist = $value; - } elsif (/^ALBUM=/) { + } elsif (/^ALBUM=/i) { $album = $value; - } elsif (/^DATE=/) { + } elsif (/^DATE=/i) { $date = $value; - } elsif (/TITLE=/) { - push(@titles, $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($fn): $!"); + 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, @titles); + 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; - my $artist = shift; - my $album = shift; - my $date = shift; - my $track = 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; + } - my $outfile = sprintf("$artist ($album) \%02s $title.mp3", $track); + $outfile = sprintf("$artist ($album) \%02s $title.mp3", $track); $outfile =~ s/\//_/g; - $outfile =~ s/:/_/g; - $outfile =~ s/'/_/g; - $outfile =~ s/"/_/g; - # XXX + arg('--tt', \$title); + arg('--ta', \$artist); + arg('--tl', \$album); + arg('--ty', \$date); + arg('--tn', \$track); + + $skip_arg ||= ''; $until_arg ||= ''; - system("flac -cd $skip_arg $until_arg '$fn' | lame --preset standard --tt '$title' --ta '$artist' --tl '$album' --ty '$date' --tn $track - '$outfile'"); + run_or_die(join(' ', "flac $flac_options -cd $skip_arg $until_arg '$fn'", + " | lame $lame_options $title $artist $album $date $track", + " - '$outfile'")); } MAIN: { - my $fn = shift or pod2usage(); - my @args = get_decode_args($fn); - my ($artist, $album, $date, @titles) = get_tags($fn); + 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)"; - for my $i (0..$#titles) { - flac2mp3($fn, $titles[$i], $artist, $album, $date, $i + 1, - @{$args[$i]}); + # 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 .