#!/usr/bin/perl -w use strict; package Tracks; use Class::Std; use Digest::SHA1; use Audio::FLAC::Header; use constant SECTOR_OFFSET => 150; my %tracks_for :ATTR( get => 'tracks' ); sub _get_tracks_from_cdinfo { my $device = shift; my @tracks; open my $CD_INFO, 'cd-info -q |' or die "Unable to run cd-info: $!"; while (<$CD_INFO>) { next unless /^\s*([0-9]+): \d\d:\d\d:\d\d (\d{6})/; my ($num, $sector) = ($1, $2); my $track = { number => $num, sector => $sector, }; # place leadout track (170) at index 0 $num != 170 ? $tracks[$num] = $track : $tracks[0] = $track; } close $CD_INFO; return @tracks; } sub _get_tracks_from_cdparanoia { my $device = shift; my @tracks; open my $CDP, 'cdparanoia -d ' . $device . ' -Q 2>&1 |' or die "Unable to run cdparanoia: $!"; while (<$CDP>) { if (m{ ^\s+(\d+)\. # track number \s+(\d+) # length \s+\[(\d\d:\d\d\.\d\d)\] # length (MSF) \s+(\d+) # start \s+\[(\d\d:\d\d\.\d\d)\] # start (MSF) }x) { my ($track, $length, $length_msf, $start, $start_msf) = ($1, $2, $3, $4, $5); $start_msf =~ s/\./:/; $tracks[$track] = { number => $track, sector => $start, msf => $start_msf, }; } elsif (m{TOTAL\s+(\d+)}) { my $total = $1; my $leadout = $total + $tracks[1]{sector}; $tracks[0] = { number => 170, sector => $leadout, }; } } close $CDP; return @tracks; } sub read_disc { my ($self, $device) = @_; $tracks_for{ident $self} = [ _get_tracks_from_cdparanoia($device) ]; } sub get_mbz_discid { my ($self) = @_; my @tracks = @{ $tracks_for{ident $self} }; return unless @tracks; my $sha1 = Digest::SHA1->new; $sha1->add(sprintf('%02X', $tracks[1]{number})); $sha1->add(sprintf('%02X', $tracks[-1]{number})); for my $i (0 .. 99) { my $offset = (defined $tracks[$i]{sector} ? ($tracks[$i]{sector} + SECTOR_OFFSET) : 0); $sha1->add(sprintf('%08X', $offset)); } my $digest = $sha1->b64digest; $digest =~ tr{+/=}{._-}; $digest .= '-'; ## why do we need to manually add this? return $digest; } sub get_cuesheet { my ($self) = @_; my @tracks = @{ $tracks_for{ident $self} }; my @cuesheet; push @cuesheet, qq{FILE "cdda.wav" WAVE}; for my $i (1 .. @tracks - 1) { my $track = $tracks[$i]; push @cuesheet, sprintf(' TRACK %02d AUDIO', $i); if ($i == 1 && $track->{sector} != 0) { push @cuesheet, ' INDEX 00 00:00:00'; } push @cuesheet, ' INDEX 01 ' . $track->{msf}; } return join('', map { "$_\n" } @cuesheet); } sub get_cdparanoia_span { my ($self) = @_; # use a msf start unless track 1 begins at sector return $tracks_for{ident $self}[1]{sector} == 0 ? '1-' : '00:00.00-'; } package main; use File::Temp qw{tempdir}; use File::Spec::Functions qw{catfile splitpath}; use File::Copy; use File::Path qw{mkpath}; use Getopt::Long qw{:config no_ignore_case no_auto_abbrev}; use Cwd; GetOptions( 'device|D=s' => \my $CD_DEVICE, 'output|o=s' => \my $OUTPUT_NAME, 'force|f' => \my $FORCE, ); # output file my (undef, $out_dir, $out_file) = splitpath($OUTPUT_NAME); # automatically add ".flac" $out_file .= '.flac' unless $out_file =~ /\.flac$/; # default to current directory $out_dir ||= getcwd; mkpath($out_dir) unless -e $out_dir; my $archive_flac = catfile($out_dir, $out_file); # check for file exist; default to not overwrite die "$archive_flac exists\nwill not overwrite (use --force to override this)\n" if -e $archive_flac && !$FORCE; # get the CD info $CD_DEVICE ||= '/dev/cdrom'; my $tracks = Tracks->new; $tracks->read_disc($CD_DEVICE); die "No tracks found; is there a CD in the drive?\n" unless @{ $tracks->get_tracks }; my $tempdir = tempdir(CLEANUP => 1); my $wav_file = catfile($tempdir, 'cdda.wav'); my $flac_file = catfile($tempdir, 'cdda.flac'); my $cue_file = catfile($tempdir, 'cdda.cue'); # rip my $span = $tracks->get_cdparanoia_span; system 'cdparanoia', '-d', $CD_DEVICE, $span, $wav_file; die "\nRipping canceled\n" if ($? & 127); # encode + cuesheet open my $CUE, "> $cue_file"; print $CUE $tracks->get_cuesheet; close $CUE; system 'flac', '-o', $flac_file, '--cuesheet', $cue_file, $wav_file; die "\nFLAC encoding canceled\n" if ($? & 127); # MusicBrainz discid metadata my $discid = $tracks->get_mbz_discid; # copy to permanent location copy($flac_file, $archive_flac); system 'metaflac', '--set-tag', "MBZ_DISCID=$discid", $archive_flac; print "Rip saved as $archive_flac\n"; system 'eject', $CD_DEVICE;