--- /dev/null
+#!/usr/local/bin/perl
+
+# TODO This is the one I've actually used. The other I hacked up and maybe improved?
+# but haven't tested
+
+=head1 NAME
+
+B<fa-mp3cd> - Make Universal Disk Format image of MP3 files from FLAC files
+
+=head1 SYNOPSIS
+
+B<fa-mp3cd> [B<-d> I<work-directory>] [B<-o> I<image-file>] [B<-V> I<VOLI>] I<file> [...]
+
+=head1 DESCRIPTION
+
+Each I<file> 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<fa-mp3cd> starts by validating all input files and planning the layout, then
+looking into I<work-directory> 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<work-directory>
+
+Path of the directory to write the UDF layout into. Default is the
+current directory.
+
+=item B<-o> I<image-file>
+
+Path of file to write UDF image to. No default: if not specified, B<fa-mp3cd>
+prepares the I<work-directory> but makes no image.
+
+=item B<-V> I<VOLI>
+
+Passed through to B<mkisofs(1)>, which says:
+
+=item B<--no-resume>
+
+Assume files in I<work-directory> are up to date rather than assuming resume
+after crash.
+
+=item B<--print-size>
+
+Run B<mkisofs(1)> 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 <epg@pretzelnet.org>.
+
+=cut
+
+package epg::flac::archive::mp3::cd;
+
+use v5.12;
+use warnings;
+use FindBin;
+
+# TODO eval hack
+eval { require "$FindBin::Bin/tags.pl" } ;
+epg::flac::archive::tags->import(
+ qw[
+ disc_tags
+ read_tags
+ mangle_for_file_name
+ quote
+ two_digits
+ ]);
+
+# 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 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;
+ my @flac_data = @_;
+ my @layout;
+ for my $path_tags (@flac_data) {
+ push(@layout, plan_flac($workdir, @$path_tags));
+ }
+ @layout
+}
+
+sub check_directory {
+ my $workdir = shift;
+ my $workdir_files = shift;
+ my @layout = @_;
+
+ # if @layout is partially on disk, return the portion that remains to be written
+
+ 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(' ', keys(%workdir_files)))
+ }
+
+ @remaining
+}
+
+sub read_args {
+ my $workdir = '.'; # TODO
+ my $imagefile; # TODO
+ # TODO other flags
+ $workdir, $imagefile, @_
+}
+
+# TODO dup from flac2mp3 but move to tags.pl
+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 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
+}
+
+# dedup with flac2mp3
+sub flac2mp3 {
+ my $mp3 = shift;
+ my $flac = shift;
+ my $tags = shift;
+ say(join(' ',
+ 'flac',
+ '-cd',
+ quote($flac),
+ '|',
+ 'lame',
+ '--id3v2-only',
+ '--id3v2-latin1',
+ '--pad-id3v2-size', 0,
+ '--preset standard',
+ '--ta',
+ quote($tags->{artist}),
+ '--tl',
+ quote($tags->{album}),
+ '--tn',
+ quote($tags->{tracknumber}),
+ '--tt',
+ quote($tags->{title}),
+ '--ty',
+ quote($tags->{date}),
+ '$pic_options',
+ #(map { ('--tv', quote("TPE2=$_")) } @{$albumartist}),
+ (map { ('--tv', quote("TPOS=$_")) } @{$tags->{discnumber}}),
+ '-',
+ quote($mp3),
+ ))
+}
+
+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});
+ flac2mp3(join('/', $mp3->{dir}, $mp3->{filename}), $mp3->{flac}, $mp3);
+ }
+
+ my @output_args;
+ if (defined($imagefile)) {
+ # Have to redirect it; won't accept -o with -reproducible-date!
+ #'-o', quote($imagefile)
+ @output_args = ('>', 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_args = ('-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',
+ #'-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),
+ @output_args,
+ ));
+
+ 0
+}
+
+if (!caller) {
+ exit(main(@ARGV))
+}
+
+1;