#!/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; use lib $FindBin::Bin; require 'flac2mp3'; require 'tags.pl'; epg::flac::archive::tags->import( qw[ track_tags read_tags_metaflac mangle_for_file_name quote two_digits ]); sub filename { my $tags = shift; mangle_for_file_name( join(' ', (map { two_digits($_) } @{$tags->{DISCNUMBER} // []}), (map { two_digits($_) } @{$tags->{TRACKNUMBER} // []}), @{$tags->{TITLE}}, ) ) . '.mp3'; } sub plan_flac { local $@; my $workdir = shift; my $path = shift; my $tags = shift; my %track_tags = eval { track_tags($tags) }; if ($@) { die("$@: $path") } ( tags => \%track_tags, flac => $path, filename => filename($tags), dir => join('/', $workdir, mangle_for_file_name(@{$tags->{ALBUM}})), ) } 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->{tags}); } 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;