]> diplodocus.org Git - flac-archive/commitdiff
Here's flac-archive, version 1.
authorepg <>
Fri, 2 Apr 2004 01:53:41 +0000 (01:53 +0000)
committerepg <>
Fri, 2 Apr 2004 01:53:41 +0000 (01:53 +0000)
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
fa-flacd [new file with mode: 0755]
fa-rip [new file with mode: 0755]
fa-tags [new file with mode: 0755]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..256aa06
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,62 @@
+# $Id$
+
+prefix=/usr/local
+exec_prefix=${prefix}
+bindir=${exec_prefix}/bin
+datadir=${prefix}/share
+mandir=${datadir}/man
+man1dir=${mandir}/man1
+catdir=${mandir}
+cat1dir=${catdir}/cat1
+
+INSTALL=       install
+INSTALL_DATA=  ${INSTALL} -m 444
+INSTALL_DIRECTORY=${INSTALL} -d -m 755
+INSTALL_SCRIPT=        ${INSTALL} -m 555
+POD2MAN=       pod2man -c '' -r ''
+ROFF=          nroff -Tascii -mandoc
+
+SCRIPTS=\
+       fa-flacd \
+       fa-rip \
+       fa-tags
+
+MAN=\
+       fa-flacd.1 fa-flacd.0
+
+all: ${SCRIPTS} ${MAN}
+
+fa-flacd.1: fa-flacd
+       ${POD2MAN} fa-flacd > $@
+
+.SUFFIXES: .1 .0
+.1.0:
+       ${ROFF} $< > $@
+
+clean:
+       rm -f ${MAN}
+
+install: ${SCRIPTS} ${bindir} ${man1dir} ${cat1dir} ${SCRIPTS} ${MAN}
+       ${INSTALL_SCRIPT} ${SCRIPTS} ${bindir}
+       ${INSTALL_DATA} fa-flacd.1 ${man1dir}
+       ln -sf fa-flacd.1 ${man1dir}/fa-rip.1
+       ln -sf fa-flacd.1 ${man1dir}/fa-tags.1
+       ${INSTALL_DATA} fa-flacd.0 ${cat1dir}
+       ln -sf fa-flacd.0 ${cat1dir}/fa-rip.0
+       ln -sf fa-flacd.0 ${cat1dir}/fa-tags.0
+
+${bindir}:
+       ${INSTALL_DIRECTORY} $@
+
+${man1dir}:
+       ${INSTALL_DIRECTORY} $@
+
+${cat1dir}:
+       ${INSTALL_DIRECTORY} $@
+
+uninstall:
+       cd ${bindir} && rm -f ${SCRIPTS}
+       cd ${man1dir} && rm -f ${MAN} fa-rip.1 fa-tags.1
+       cd ${cat1dir} && rm -f ${MAN} fa-rip.0 fa-tags.0
+
+.PHONY: all clean install uninstall
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..b3e01e6
--- /dev/null
+++ b/README
@@ -0,0 +1,6 @@
+$Id$
+
+Run perldoc fa-flacd for more information.
+
+flac-archive requires Perl >= 5.6, abcde and cd-discid, cdparanoia,
+and cdrdao.
diff --git a/fa-flacd b/fa-flacd
new file mode 100755 (executable)
index 0000000..72c8923
--- /dev/null
+++ b/fa-flacd
@@ -0,0 +1,316 @@
+#! /usr/bin/env perl
+
+# $Id$
+
+=head1 NAME
+
+B<fa-flacd>, B<fa-rip>, B<fa-tags> - archive CDs to single FLAC files
+
+=head1 SYNOPSIS
+
+B<fa-flacd> [B<-jv>]
+
+B<fa-rip>
+
+B<fa-tags> ID TRACKCOUNT OFFSET [OFFSET ...] LENGTH
+
+=cut
+
+use strict;
+use warnings;
+
+use File::Basename;
+use Getopt::Std; $Getopt::Std::STANDARD_HELP_VERSION = 1;
+use POSIX ':sys_wait_h';
+
+our $VERSION = 1;
+
+my $verbose;
+my @jobs;
+my @finished;
+
+sub verbose {
+    $verbose and map({ print(STDERR $_) } @_);
+}
+
+# Return the ARTIST, ALBUM, and DATE followed by a list of all the
+# lines in the file FN.
+sub get_tags {
+    my $fn = shift;
+    my $tag;
+    my $value;
+    my $artist;
+    my $album;
+    my $date;
+    my @tags;
+
+    verbose("Opening tags file $fn\n");
+    open(TAGS, $fn) or die("open($fn): $!");
+    while (<TAGS>) {
+        chomp;
+        push(@tags, $_);
+
+        ($tag, $value) = split(/=/, $_, 2);
+
+        if (/^ARTIST=/) {
+            $artist = $value;
+            verbose("ARTIST $artist from $fn\n");
+        } elsif (/^ALBUM=/) {
+            $album = $value;
+            verbose("ALBUM $album from $fn\n");
+        } elsif (/^DATE=/) {
+            $date = $value;
+            verbose("DATE $date from $fn\n");
+        }
+    }
+    close(TAGS);
+
+    return ($artist, $album, $date, @tags);
+}
+
+# Process the fa-rip output in the directory DIR.
+sub flac {
+    my $dir = shift;
+    my $artist;
+    my $album;
+    my $date;
+    my @tags;
+    my $status;
+
+    verbose("Renaming $dir/tags\n");
+    rename("$dir/tags", "$dir/using-tags")
+      or die("rename($dir/tags, $dir/using-tags): $!");
+
+    ($artist, $album, $date, @tags) = get_tags("$dir/using-tags");
+
+    verbose("mkdir($artist)\n");
+    -d $artist or mkdir($artist) or die("mkdir($artist): $!");
+
+    verbose("chdir($dir)\n");
+    chdir($dir) or die("chdir($dir): $!");
+
+    verbose("Running flac\n");
+    $status = system('flac', '-o', "../$artist/$album.flac-tmp",
+                     '--delete-input-file', '-V', '--cuesheet',
+                     'cue', '--no-padding', '--best',
+                     map({ ('-T', $_) } @tags),
+                     'wav');
+    if (WIFEXITED($status) and ($status = WEXITSTATUS($status)) != 0) {
+        die("flac: $status");
+    } elsif (WIFSIGNALED($status)) {
+        die("flac killed with signal ", WTERMSIG($status));
+    } elsif (WIFSTOPPED($status)) {
+        die("flac stopped with signal ", WSTOPSIG($status));
+    }
+
+    verbose("Cleaning up $dir\n");
+    unlink('using-tags') or die("unlink(using-tags): $!");
+    unlink('cue') or die("unlink(cue): $!");
+    rename('toc', "../$artist/$album.toc")
+      or die("rename(toc, ../$artist/$album.toc): $!");
+    rename('log', "../$artist/$album.log")
+      or die("rename(log, ../$artist/$album.log): $!");
+    chdir('..') or die("chdir(..): $!");
+    rmdir($dir) or die("rmdir($dir): $!");
+
+    rename("$artist/$album.flac-tmp", "$artist/$album.flac")
+      or die("rename($artist/$album.flac-tmp, $artist/$album.flac): $!");
+}
+
+sub reaper {
+    my $pid;
+
+    while (($pid = waitpid(0, WNOHANG)) > 0) {
+        push(@finished, [$pid, $?]);
+    }
+
+    $SIG{CHLD} = \&reaper;
+}
+
+sub newjob {
+    my $dir = shift;
+    my $pid;
+
+    $pid = fork();
+    if ($pid == -1) {
+        die("fork: $!");
+    } elsif ($pid == 0) {
+        $SIG{CHLD} = 'IGNORE';
+        open(STDERR, ">$dir/log") or die("open(STDERR, >$dir/log): $!");
+        flac($dir);
+        exit(0);
+    }
+
+    verbose("new job $pid for $dir\n");
+    return $pid;
+}
+
+sub deljob {
+    my $i = shift;
+    my $j;
+    my $pid;
+    my $status;
+
+    $pid = $finished[$i][0];
+    $status = WEXITSTATUS($finished[$i][1]);
+
+    verbose("$pid finished (");
+    if (WIFEXITED($status)) {
+        verbose('exited ', WEXITSTATUS($status));
+    } elsif (WIFSIGNALED($status)) {
+        verbose('signalled ', WTERMSIG($status));
+    } elsif (WIFSTOPPED($status)) {
+        verbose('stopped ', WSTOPSIG($status));
+    }
+    verbose(")\n");
+
+    for ($j = 0; $j <= $#jobs; $j++) {
+        $pid == $jobs[$j] and splice(@jobs, $j, 1) and last;
+    }
+
+    splice(@finished, $i, 1);
+}
+
+sub flacloop {
+    my $MAXJOBS = shift;
+    my $i;
+    my $j;
+
+
+    $SIG{CHLD} = \&reaper;
+    while (1) {
+        if (scalar(@jobs) <= $MAXJOBS) {
+            foreach $i (glob('*/tags')) {
+                push(@jobs, newjob(dirname($i))) <= $MAXJOBS or last;
+            }
+        }
+
+        for ($i = 0; $i <= $#finished; $i++) {
+            deljob($i);
+        }
+
+        verbose(scalar(@jobs), " jobs\n");
+        sleep(5);
+    }
+}
+
+MAIN: {
+    my %opts;
+
+    $opts{'j'} = 4;
+    $opts{'v'} = 0;
+    if (not getopts('j:v', \%opts)) {
+        print("usage: flacd [-jN -v]\n");
+        exit(2);
+    }
+
+    $verbose = $opts{'v'};
+
+    flacloop($opts{'j'});
+}
+
+\f
+__END__
+
+=head1 DESCRIPTION
+
+B<fa-flacd>, B<fa-rip>, and B<fa-tags> together comprise
+B<flac-archive>, a system for archiving audio CDs to single FLAC
+files.  B<fa-flacd> is the guts of the system.  It runs in the
+directory where the audio archives are stored, scanning for new CDs to
+encode and rename; it never exits.  B<fa-rip> generates the inputs for
+B<fa-flacd>: the ripped WAV file, Vorbis tags, and a cuesheet.
+B<fa-tags> is not meant to be run directly; B<fa-rip> uses it to
+generate the candidate Vorbis tags.
+
+All three programs expect to be run from the same directory.  They use
+that directory to manage directories named by artist and by disc ID.
+Intermediate files are written to the disc ID directory.  B<fa-flacd>
+processes the disc ID directories into per-album files in the artist
+directories.
+
+=head2 FA-FLACD
+
+B<fa-flacd> does not exit; it runs until the user kills it.  Every 5
+seconds it scans its current directory for directories with a file
+called "tags" and creates a processing job for each one.  The number
+of jobs B<fa-flacd> attempts to run is controlled by the B<-j> option
+and defaults to 4.  B<fa-flacd> will print diagnostic output when the
+B<-v> option is given.
+
+A processing job first renames the directory's "tags" file to
+"using-tags" so that B<ra-flacd> will not try to start another job for
+this directory.  This file is left as is when an error is encountered,
+so a new job will not be started until the user corrects the error
+condition and renames "using-tags" back to "tags".  Next, it encodes
+the "wav" file to a FLAC file, using the "cue" file for the cuesheet
+and "using-tags" for Vorbis tags.  Any diagnostic output is saved in
+the "log" file.  Finally, the "cue" and "log" files are moved to the
+artist directory (and named by album) and the ID directory is removed.
+
+=head2 FA-RIP
+
+B<fa-rip> uses L<cd-discid(1)> to retrieve the disc ID and track
+information.  It creates a directory named by ID for storage of its
+intermediate files.  It passes the L<cd-discid(1)> output as
+command-line arguments to B<fa-tags> in the background.  It then uses
+L<cdrdao(1)> to create the "cue" file in the background.  Finally, it
+execs ><cdparanoia(1)> to rip the CD to the "wav" file.
+
+In order for this CD to be processed by B<fa-flacd>, the user must
+create a "tags" file.  This is usually done by renaming one of the
+candidate-tags files and deleting the others.
+
+=head2 FA-TAGS
+
+B<fa-tags> uses L<cddb-tool(1)> (from the B<abcde> package) to
+populate candidate-tags files.  These are numbered in the order of
+entries read from CDDB, e.g. candidate-tags-1, candidate-tags-2, etc.
+B<fa-tags> also creates candidate-tags-0, which has the correct fields
+for this CD (including correct number of TITLE= lines), but with all
+fields blank.
+
+B<fa-tags> expects the output of L<cd-discid(1)> as command-line
+arguments.  That is, the disc ID, number of tracks, list of track
+offsets, and total length of the CD in seconds.
+
+=head1 ENVIRONMENT
+
+=over 4
+
+=item CDDBURL
+
+B<fa-tags> uses this to retrieve candidate Vorbis tags.  Defaults to
+"http://freedb.freedb.org/~cddb/cddb.cgi".
+
+=item CDDEV
+
+B<fa-rip> uses this to rip audio and save the cuesheet for a CD.  It
+makes some effort to check some common device names for FreeBSD,
+Linux, and NetBSD by default.
+
+=back
+
+=head1 AUTHORS
+
+Written by Eric Gillespie <epg@pretzelnet.org>.  B<fa-tags> contains
+code from B<abcde>, which bears the following notice:
+
+# Copyright (c) 1998-2001 Robert Woodcock <rcw@debian.org>
+# Copyright (c) 2003-2004 Jesus Climent <jesus.climent@hispalinux.es>
+# This code is hereby licensed for public consumption under either the
+# GNU GPL v2 or greater, or Larry Wall's Artistic license - your choice.
+
+B<flac-archive> is hereby licensed for public consumption under either
+the GNU GPL v2 or greater, or Larry Wall's Artistic license - your
+choice.
+
+=cut
+
+# Local variables:
+# cperl-indent-level: 4
+# perl-indent-level: 4
+# indent-tabs-mode: nil
+# End:
+
+# vi: set tabstop=4 expandtab:
diff --git a/fa-rip b/fa-rip
new file mode 100755 (executable)
index 0000000..55b2fb2
--- /dev/null
+++ b/fa-rip
@@ -0,0 +1,36 @@
+#! /usr/bin/env zsh
+
+# $Id$
+
+set -e
+setopt NULL_GLOB
+
+get_cddev () {
+    local raw
+    raw=$(sysctl -n kern.rawpartition > /dev/null | awk '{printf "%c",97+$0}')
+
+    for CDDEV in /dev/{cdroms/cdrom*,cdrom*,rcd*${raw},{a,}cd*c}; do
+        [[ -e ${CDDEV} ]] && return 0
+    done
+
+    return 1
+}
+
+if [[ -z ${CDDEV} ]]; then
+    if ! get_cddev; then
+        echo 'CDDEV environment variable not set, defaults did not work'
+        exit 2
+    fi
+fi
+
+discid=($(cd-discid ${CDDEV}))
+
+[[ -d $discid[1] ]] || mkdir $discid[1]
+cd $discid[1]
+
+eval fa-tags $discid &
+
+cdrdao read-toc --device ${CDDEV} --driver generic-mmc toc
+toc2cue toc cue &
+
+exec cdparanoia -d ${CDDEV} 1-$discid[2] wav
diff --git a/fa-tags b/fa-tags
new file mode 100755 (executable)
index 0000000..34eafb8
--- /dev/null
+++ b/fa-tags
@@ -0,0 +1,103 @@
+#! /usr/bin/env zsh
+
+# $Id$
+
+# Copyright (c) 1998-2001 Robert Woodcock <rcw@debian.org>
+# Copyright (c) 2003-2004 Jesus Climent <jesus.climent@hispalinux.es>
+# Copyright (c) 2004 Eric Gillespie <epg@pretzelnet.org>
+# This code is hereby licensed for public consumption under either the
+# GNU GPL v2 or greater, or Larry Wall's Artistic license - your choice.
+
+set -e
+
+if [[ -z ${CDDBURL} ]]; then
+    CDDBURL="http://freedb.freedb.org/~cddb/cddb.cgi"
+fi
+
+get_cddb_protocol () {
+    for CDDBPROTO in 5 4 3; do
+        if [[ \
+            $(cddb-tool stat $CDDBURL $USER $HOST $CDDBPROTO \
+            | sed 's/^\([0-9][0-9]*\).*/\1/;q') = '210' ]]; then
+            return 0
+        fi
+    done
+
+    echo "${CDDBURL} does not work"
+    exit 2
+}
+
+handle_read=$(<<EOF
+function rstrip_print(_s) {
+    sub(/[[:space:]]*\$/, "", _s)
+    print(_s)
+}
+
+BEGIN {
+    FS="="
+}
+
+/^DTITLE=/ {
+    split(\$2, a, /[[:blank:]]*\/[[:blank:]]*/)
+    rstrip_print("ARTIST=" a[1])
+    rstrip_print("ALBUM=" a[2])
+}
+
+/^DYEAR=/ {
+    rstrip_print("DATE=" \$2)
+}
+
+/^TTITLE[[:digit:]]+/ {
+    rstrip_print("TITLE=" \$2)
+}
+EOF
+)
+
+perl_handle_read=$(<<EOF
+(\$tag, \$value) = split(/=/, \$_, 2);
+
+if (/^DTITLE=/) {
+    (\$artist, \$album) = split(/\\s*\\/\\s*/, \$value);
+    print("ARTIST=\$artist\\n");
+    print("ALBUM=\$album\\n");
+
+} elsif (/^DYEAR=/) {
+    print("DATE=\$value\\n");
+
+} elsif (/^TTITLE\\d+/) {
+    print("TITLE=\$value\\n");
+}
+EOF
+)
+
+handle_queries () {
+    local F
+    local i=1
+
+    # First eat the status line.
+    read F
+
+    while read -A F; do
+        [[ $F = '.' ]] && break
+        cddb-tool read $CDDBURL $CDDBPROTO $USER $HOST $F[1] $F[2] \
+            | nawk $handle_read > candidate-tags-$i
+# XXX The awk script has at least one problem; since we can't split to
+# exactly two components, we break on track names with equal signs.
+# Use Perl instead:
+#        cddb-tool read $CDDBURL $CDDBPROTO $USER $HOST $F[1] $F[2] \
+#            | perl -lne $handle_read > candidate-tags-$i
+        i=$(( $i + 1 ))
+    done
+}
+
+get_cddb_protocol
+(cddb-tool query $CDDBURL $CDDBPROTO $USER $HOST "$@" | handle_queries) &
+
+cat > candidate-tags-0 <<EOF
+ARTIST=
+ALBUM=
+DATE=
+EOF
+for i in $(jot $2 1); do
+    echo 'TITLE=' >> candidate-tags-0
+done