From: Eric Gillespie Date: Mon, 14 Mar 2022 05:27:09 +0000 (-0500) Subject: fa-mp3cd - new tool to generate MP3 CD images - lightly tested X-Git-Url: https://diplodocus.org/git/flac-archive/commitdiff_plain/92a7ddebe0c11576d91011b0ba58b6c2c057c9ed?ds=inline fa-mp3cd - new tool to generate MP3 CD images - lightly tested --- diff --git a/fa-mp3cd b/fa-mp3cd new file mode 100755 index 0000000..72efdcf --- /dev/null +++ b/fa-mp3cd @@ -0,0 +1,228 @@ +#!/usr/local/bin/perl + +=head1 NAME + +B - Make Universal Disk Format image of MP3 files from FLAC files + +=head1 SYNOPSIS + +B [B<-d> I] [B<-o> I] [B<-V> I] I [...] + +=head1 DESCRIPTION + +Each I argument is a single-track[1] FLAC file to be transcoded to MP3 +and stored in the UDF image. By default, place MP3 files in directories named +after the album title. Change with B<-dirname> options tracks after the +B<-dirname> go into that dirname. Restore default with B<-diralbum>.[2] + +B starts by validating all input files and planning the layout, then +looking into I for any files not in the plan, exiting with an +error if any are found. If files exist that are in the plan, assume this is a +rerun after crash and resume starting at the last file written. E.g. if +writing 100 files and crash after finishing file 98 but before starting file +99, go ahead and rewrite file 98. + +=head2 Notes + +=over + +=item 1. +Still considering restoring whole-disc FLAC file support. + +=item 2. +Not yet implemented. + +=back + +=head1 OPTIONS + +=over + +=item B<-d> I + +Path of the directory to write the UDF layout into. Default is the +current directory. + +=item B<-o> I + +Path of file to write UDF image to. No default: if not specified, B +prepares the I but makes no image. + +=item B<-V> I + +Passed through to B, which says: + +=item B<--no-resume> + +Assume files in I are up to date rather than assuming resume +after crash. + +=item B<--print-size> + +Run B to print out the estimated size (in kilobytes) of the image +as well as how much remains of the 650 MB allowed on CD-ROM. + +=over + +Specifies the volume ID (volume name or label) to be written into the master +block. There is space on the disc for 32 characters of information. + +=back + +Some systems may use this volume ID as the name of a mount point. + +=back + +=head1 AUTHORS + +Written by Eric Gillespie . + +=cut + +package epg::flac::archive::mp3::cd; + +use v5.12; +use warnings; +use FindBin; + +# TODO eval hack +eval { + require "$FindBin::Bin/flac2mp3"; + #require "$FindBin::Bin/tags.pl"; +}; +epg::flac::archive::tags->import( + qw[ + track_tags + read_tags_metaflac + mangle_for_file_name + quote + two_digits + ]); + +sub plan_flac { + my $workdir = shift; + my $path = shift; + my $tags = shift; + + my %track = track_tags($tags); + $track{flac} = $path; + $track{filename} = mangle_for_file_name( + join(' ', + (map { two_digits($_) } @{$track{discnumber}}), + two_digits($track{tracknumber}), + $track{title}, + )) . '.mp3'; + $track{dir} = join('/', $workdir, mangle_for_file_name($track{album})); + \%track +} + +sub plan { + my $workdir = shift; + map { plan_flac($workdir, @$_) } @_ +} + +# if @layout is partially on disk, return the portion that remains to be written +sub check_directory { + my $workdir = shift; + my $workdir_files = shift; + my @layout = @_; + + my @remaining; + my $last; + my %workdir_files = map { ($_, 1) } @$workdir_files; + for my $mp3 (@layout) { + my $path = join('/', $mp3->{dir}, $mp3->{filename}); + if (exists($workdir_files{$path})) { + delete($workdir_files{$path}); + $last = $mp3; + } else { + if (defined($last)) { + push(@remaining, $last); + undef($last); + } + push(@remaining, $mp3); + } + } + defined($last) && push(@remaining, $last); + if (%workdir_files > 0) { + die("unexpected files in $workdir: ", join(' ', sort(keys(%workdir_files)))) + } + + @remaining +} + +sub read_args { + my $workdir = '.'; # TODO + my $imagefile; # TODO + # TODO other flags + $workdir, $imagefile, @_ +} + +sub find_files { + my $path = shift; + # If we come across stupid file names embedded newlines, check_directory will complain about them. + open(my $fh, '-|', 'find', $path, '!', '-type', 'd') || die("find $path ! -type d: $!"); + my @files; + while (<$fh>) { + chomp; + push(@files, $_); + } + if (!close($fh)) { + if ($! == 0) { + die("find $path ! -type d exited $?") + } + die("close(find $path ! -type d): $!") + } + \@files +} + +sub main { + my ($workdir, $imagefile, @flac_files) = read_args(@_); + + # -no-resume would mean skipping preparing the work directory at all. + # So that's probably not the right name for the flag. + my @layout = check_directory( + $workdir, + find_files($workdir), + plan( + $workdir, + map { ["$_", read_tags_metaflac($_)] } @flac_files, + )); + for my $mp3 (@layout) { + say('mkdir -p ' . $mp3->{dir}); + epg::flac::archive::mp3::flac2mp3(join('/', $mp3->{dir}, $mp3->{filename}), $mp3->{flac}, $mp3); + } + + my @output; + if (defined($imagefile)) { + # Have to redirect it; won't accept -o with -reproducible-date! + #'-o', quote($imagefile) + @output = ('>', quote($imagefile)); + } else { + # "Print estimated filesystem size in multiples of the sector size (2048 bytes)" + # Overhead seems to be under 1MB so shoot for total of 649 MB of files. + @output = ('-print-size'); + } + # If I cared for fully reproducible image, set all file and directory timestamps: + # touch -d `git log -1 '--format=%cd' '--date=format:%FT%T'` + say(join(' ', + 'mkisofs', + @output, + #'-reproducible-date', `date '+%Y-%m-%d %H:%M:%S %z'`, + #'-reproducible-date', `git log -1 '--format=%ai'`, + #'-reproducible-date', quote('2022-03-11 23:37:11 -0600'), + '-J', + '-full-iso9660-filenames', + '-r', + '-udf', + quote($workdir), + )); + + 0 +} + +if (!caller) { + exit(main(@ARGV)) +} + +1; diff --git a/flac2mp3 b/flac2mp3 index d82e61d..fd0b148 100755 --- a/flac2mp3 +++ b/flac2mp3 @@ -58,40 +58,23 @@ use FindBin; require "$FindBin::Bin/tags.pl"; epg::flac::archive::tags->import( qw[ - disc_tags - read_tags + track_tags + read_tags_metaflac mangle_for_file_name quote two_digits ]); sub flac2mp3 { - my $quoted_flac = quote(shift); + my $mp3 = shift; + my $flac = shift; my $tags = shift; - my ($artist, $album, $date, $discnumber) = disc_tags(%$tags); - - # TODO resurrect whole-disc FLAC? - # 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. - # if track == None: - # track = 1 - # else: - # track = int(track) - - my $tracknumber = epg::flac::archive::tags::one(TRACKNUMBER => $tags); - my $title = epg::flac::archive::tags::one(TITLE => $tags); - # TODO restore PARTNUMBER and VERSION next time i need them - - say('metaflac --export-picture-to=flac2mp3.cover.$$', " $quoted_flac && pic_options=", '"--ti flac2mp3.cover.$$"'); - # This is an old TODO; what's wrong with --ty ? # TODO: Look at TDOR, TDRL, TDRC for date. say(join(' ', 'flac', '-cd', - $quoted_flac, + quote($flac), '|', 'lame', '--id3v2-only', @@ -99,51 +82,41 @@ sub flac2mp3 { '--pad-id3v2-size', 0, '--preset standard', '--ta', - quote($artist), + quote($tags->{artist}), '--tl', - quote($album), + quote($tags->{album}), '--tn', - quote($tracknumber), + quote($tags->{tracknumber}), '--tt', - quote($title), + quote($tags->{title}), '--ty', - quote($date), + quote($tags->{date}), '$pic_options', #(map { ('--tv', quote("TPE2=$_")) } @{$albumartist}), - (map { ('--tv', quote("TPOS=$_")) } @{$discnumber}), + (map { ('--tv', quote("TPOS=$_")) } @{$tags->{discnumber}}), '-', - quote( - mangle_for_file_name( - join(' ', - $artist, - $album, - (map { two_digits($_) } @{$discnumber}), - two_digits($tracknumber), - $title, - )) - . '.mp3' - ) - )); - say('unset pic_options'); -} - -sub read_tags_metaflac { - my $fn = shift; - open(my $fh, '-|', 'metaflac', '--no-utf8-convert', '--export-tags-to=-', $fn) || die("metalfac: $!"); - my @result = read_tags($fh); - if (!close($fh)) { - if ($! == 0) { - die("metaflac exited $?") - } - die("close(metaflac): $!") - } - @result + quote($mp3), + )) } sub main { - for my $fn (@_) { - my ($tags) = read_tags_metaflac($fn); - flac2mp3($fn, $tags); + for my $flac (@_) { + say('metaflac --export-picture-to=flac2mp3.cover.$$ ', quote($flac), ' && pic_options="--ti flac2mp3.cover.$$"'); + my %tags = track_tags(read_tags_metaflac($flac)); + flac2mp3( + mangle_for_file_name( + join(' ', + $tags{artist}, + $tags{album}, + (map { two_digits($_) } @{$tags{discnumber}}), + two_digits($tags{tracknumber}), + $tags{title}, + )) + . '.mp3', + $flac, + \%tags, + ); + say('unset pic_options'); } say('rm -f flac2mp3.cover.$$'); diff --git a/t/mp3cd/check-directory.t b/t/mp3cd/check-directory.t new file mode 100644 index 0000000..0e628f5 --- /dev/null +++ b/t/mp3cd/check-directory.t @@ -0,0 +1,95 @@ +use v5.12; +use warnings; + +use Test::More tests => 7; + +use Test::Differences; +use Test::Exception; + +require './tags.pl'; +require './fa-mp3cd'; + +my @Rising = ( + { title => ['Tarot Woman'] }, + { title => ['Run With the Wolf'] }, + { title => ['Starstruck'] }, + ); +for (my $i = 0; $i < @Rising; $i++) { + my $n = $i + 1; + $Rising[$i] = { + dir => './Rising', + flac => "$n.flac", + filename => "$n.mp3", + tracknumber => $n, + artist => 'Rainbow', + album => 'Rising', + date => '1976-05-17', + %{$Rising[$i]}, + }; +} + +eq_or_diff + \@Rising, + [epg::flac::archive::mp3::cd::check_directory('.', [], @Rising)], + 'empty work directory'; + +eq_or_diff + \@Rising, + [epg::flac::archive::mp3::cd::check_directory( + '.', + ['./Rising/1.mp3'], + @Rising)], + 'track 1 exists - plan to write 1-3'; + +eq_or_diff + [@Rising[1,2]], + [epg::flac::archive::mp3::cd::check_directory( + '.', + [ + './Rising/1.mp3', + './Rising/2.mp3', + ], + @Rising)], + 'track 1,2 exist - plan to write 2,3'; + +eq_or_diff + [$Rising[2]], + [epg::flac::archive::mp3::cd::check_directory( + '.', + [ + './Rising/1.mp3', + './Rising/2.mp3', + './Rising/3.mp3', + ], + @Rising)], + 'track 1-3 exist - plan to write 3'; + +throws_ok { + epg::flac::archive::mp3::cd::check_directory( + '.', + ['./unexpected1'], + @Rising) +} qr(unexpected files in .: ./unexpected1), + '1 unexpected file'; + +throws_ok { + epg::flac::archive::mp3::cd::check_directory( + '.', + ['./unexpected1', './unexpected2'], + @Rising) +} qr(unexpected files in .: ./unexpected1 ./unexpected2), + '2 unexpected files'; + +throws_ok { + epg::flac::archive::mp3::cd::check_directory( + '.', + [ + './unexpected', + './Rising/1.mp3', + './Rising/unexpected', + './Rising/2.mp3', + './l/o/l', + ], + @Rising) +} qr(unexpected files in .: ./Rising/unexpected ./l/o/l ./unexpected), + '3 unexpected files, 2 expected'; diff --git a/t/mp3cd/plan.t b/t/mp3cd/plan.t new file mode 100644 index 0000000..3e3a6a8 --- /dev/null +++ b/t/mp3cd/plan.t @@ -0,0 +1,107 @@ +use v5.12; +use warnings; + +use Test::More tests => 3; + +use Test::Differences; + +require './tags.pl'; +require './fa-mp3cd'; + +my @Rising = ( + { TITLE => ['Tarot Woman'] }, + { TITLE => ['Run With the Wolf'] }, + ); +for (my $i = 0; $i < @Rising; $i++) { + my $n = $i + 1; + $Rising[$i] = [ + "$n.flac", + { + TRACKNUMBER => [$n], + ARTIST => ['Rainbow'], + ALBUM => ['Rising'], + DATE => ['1976-05-17'], + %{$Rising[$i]}, + }]; +} + +eq_or_diff + [ + { + dir => './Rising', + flac => '1.flac', + filename => '01_Tarot_Woman.mp3', + artist => 'Rainbow', + album => 'Rising', + date => '1976-05-17', + discnumber => [], + tracknumber => 1, + title => 'Tarot Woman', + }, + { + dir => './Rising', + flac => '2.flac', + filename => '02_Run_With_the_Wolf.mp3', + artist => 'Rainbow', + album => 'Rising', + date => '1976-05-17', + discnumber => [], + tracknumber => 2, + title => 'Run With the Wolf', + }, + ], + [epg::flac::archive::mp3::cd::plan('.', @Rising)]; + +eq_or_diff + [ + { + dir => 'a/b/fakeal', + flac => 'file.flac', + filename => '01_faket.mp3', + artist => 'fakear', + album => 'fakeal', + date => 0, + discnumber => [], + tracknumber => 1, + title => 'faket', + } + ], + [ + epg::flac::archive::mp3::cd::plan( + 'a/b', + ['file.flac', { + ARTIST => ['fakear'], + ALBUM => ['fakeal'], + DATE => [0], + TRACKNUMBER => [1], + TITLE => ['faket'], + }], + )], + 'relative work directory'; + +eq_or_diff + [ + { + dir => '/a/b/fakeal', + flac => 'file.flac', + filename => '01_faket.mp3', + artist => 'fakear', + album => 'fakeal', + date => 0, + discnumber => [], + tracknumber => 1, + title => 'faket', + } + ], + [ + epg::flac::archive::mp3::cd::plan( + '/a/b', + ['file.flac', { + ARTIST => ['fakear'], + ALBUM => ['fakeal'], + DATE => [0], + TRACKNUMBER => [1], + TITLE => ['faket'], + }], + )], + 'absolute work directory'; diff --git a/tags.pl b/tags.pl index 2c04fb9..3475a56 100644 --- a/tags.pl +++ b/tags.pl @@ -63,8 +63,11 @@ use warnings; use Exporter 'import'; our @EXPORT_OK = qw[ - disc_tags read_tags + read_tags_metaflac + disc_tags + track_tags + mangle_for_file_name quote two_digits @@ -114,6 +117,19 @@ sub read_tags { \%album, \@tracks } +sub read_tags_metaflac { + my $fn = shift; + open(my $fh, '-|', 'metaflac', '--no-utf8-convert', '--export-tags-to=-', $fn) || die("metaflac: $!"); + my @result = read_tags($fh); + if (!close($fh)) { + if ($! == 0) { + die("metaflac exited $?") + } + die("close(metaflac): $!") + } + @result +} + sub one_or_undef { my $name = shift; my $tags = shift; @@ -201,6 +217,36 @@ sub disc_tags { $artist, $album_tag, $date, \@discnumber } +# TODO dedup with flac2mp3 +sub track_tags { + my $tags = shift; + # TODO probably ought to return hash too. then track_tags would augment + my ($artist, $album, $date, $discnumber) = disc_tags(%$tags); + + # TODO resurrect whole-disc FLAC? + # 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. + # if track == None: + # track = 1 + # else: + # track = int(track) + + my $tracknumber = epg::flac::archive::tags::one(TRACKNUMBER => $tags); + my $title = epg::flac::archive::tags::one(TITLE => $tags); + # TODO restore PARTNUMBER and VERSION next time i need them + + ( + artist => $artist, + album => $album, + date => $date, + discnumber => $discnumber, + tracknumber => $tracknumber, + title => $title, + ) +} + sub mangle_for_file_name { my $fn = shift; $fn =~ s/[!,.?]//g; # discard these punctuation marks