]> diplodocus.org Git - flac-archive/commitdiff
fa-mp3cd - new tool to generate MP3 CD images - lightly tested
authorEric Gillespie <epg@pretzelnet.org>
Mon, 14 Mar 2022 05:27:09 +0000 (00:27 -0500)
committerEric Gillespie <epg@pretzelnet.org>
Sat, 19 Mar 2022 01:56:54 +0000 (20:56 -0500)
fa-mp3cd [new file with mode: 0755]
flac2mp3
t/mp3cd/check-directory.t [new file with mode: 0644]
t/mp3cd/plan.t [new file with mode: 0644]
tags.pl

diff --git a/fa-mp3cd b/fa-mp3cd
new file mode 100755 (executable)
index 0000000..72efdcf
--- /dev/null
+++ b/fa-mp3cd
@@ -0,0 +1,228 @@
+#!/usr/local/bin/perl
+
+=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/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;
index d82e61d..fd0b148 100755 (executable)
--- a/flac2mp3
+++ b/flac2mp3
@@ -58,40 +58,23 @@ use FindBin;
 require "$FindBin::Bin/tags.pl";
 epg::flac::archive::tags->import(
     qw[
 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 {
         mangle_for_file_name
         quote
         two_digits
     ]);
 
 sub flac2mp3 {
-    my $quoted_flac = quote(shift);
+    my $mp3 = shift;
+    my $flac = shift;
     my $tags = 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',
     # 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',
              '|',
              'lame',
              '--id3v2-only',
@@ -99,51 +82,41 @@ sub flac2mp3 {
              '--pad-id3v2-size', 0,
              '--preset standard',
              '--ta',
              '--pad-id3v2-size', 0,
              '--preset standard',
              '--ta',
-             quote($artist),
+             quote($tags->{artist}),
              '--tl',
              '--tl',
-             quote($album),
+             quote($tags->{album}),
              '--tn',
              '--tn',
-             quote($tracknumber),
+             quote($tags->{tracknumber}),
              '--tt',
              '--tt',
-             quote($title),
+             quote($tags->{title}),
              '--ty',
              '--ty',
-             quote($date),
+             quote($tags->{date}),
              '$pic_options',
              #(map { ('--tv', quote("TPE2=$_")) } @{$albumartist}),
              '$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 {
 }
 
 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.$$');
 
     }
     say('rm -f flac2mp3.cover.$$');
 
diff --git a/t/mp3cd/check-directory.t b/t/mp3cd/check-directory.t
new file mode 100644 (file)
index 0000000..0e628f5
--- /dev/null
@@ -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 (file)
index 0000000..3e3a6a8
--- /dev/null
@@ -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 (file)
--- a/tags.pl
+++ b/tags.pl
@@ -63,8 +63,11 @@ use warnings;
 use Exporter 'import';
 
 our @EXPORT_OK = qw[
 use Exporter 'import';
 
 our @EXPORT_OK = qw[
-    disc_tags
     read_tags
     read_tags
+    read_tags_metaflac
+    disc_tags
+    track_tags
+
     mangle_for_file_name
     quote
     two_digits
     mangle_for_file_name
     quote
     two_digits
@@ -114,6 +117,19 @@ sub read_tags {
     \%album, \@tracks
 }
 
     \%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;
 sub one_or_undef {
     my $name = shift;
     my $tags = shift;
@@ -201,6 +217,36 @@ sub disc_tags {
     $artist, $album_tag, $date, \@discnumber
 }
 
     $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
 sub mangle_for_file_name {
     my $fn = shift;
     $fn =~ s/[!,.?]//g;          # discard these punctuation marks