Index: /trunk/lib/Ripper.pm
===================================================================
--- /trunk/lib/Ripper.pm	(revision 25)
+++ /trunk/lib/Ripper.pm	(revision 26)
@@ -30,5 +30,5 @@
 
     # rip
-    my $span = $self->tracks->get_cdparanoia_span;
+    my $span = $self->_get_cdparanoia_span;
     system 'cdparanoia', '-d', $self->device, $span, $wav_file;
     die "\nRipping canceled\n" if ($? & 127);
@@ -42,5 +42,5 @@
 
     # MusicBrainz discid metadata
-    my $discid = $self->tracks->get_mbz_discid;
+    my $discid = $self->tracks->get_musicbrainz_discid;
 
     # copy to permanent location
@@ -49,4 +49,10 @@
 }
 
+sub _get_cdparanoia_span {
+    my ($self) = @_;
+    # use a msf start unless track 1 begins at sector
+    return $self->tracks->tracks->[1]{sector} == 0 ? '1-' : '00:00.00-';
+}
+
 # module return
 1;
Index: /trunk/lib/Tracks.pm
===================================================================
--- /trunk/lib/Tracks.pm	(revision 25)
+++ /trunk/lib/Tracks.pm	(revision 26)
@@ -2,6 +2,19 @@
 
 use Moose;
+use Audio::FLAC::Header;
+use IO::Lines;
+use IO::File;
 use Digest::SHA1;
 
+# conversion factors
+use constant FRAMES_PER_SECOND => 75;
+use constant SECONDS_PER_MINUTE => 60;
+
+# see also http://flac.sourceforge.net/format.html#cuesheet_track
+# 588 samples/frame = 44100 samples/sec / 75 frames/sec
+use constant SAMPLES_PER_FRAME => 588;
+
+# XXX: does this every vary?
+# or is the lead-in always 88200 samples (88200 / 588 = 150) i.e. 2 seconds?
 use constant SECTOR_OFFSET => 150;
 
@@ -68,4 +81,48 @@
 }
 
+sub parse_cuesheet {
+    my ($handle) = @_;
+
+    my @tracks;
+    my $track;
+    while (<$handle>) {
+        if (/TRACK (\d\d)/) {
+            $track = int($1);
+        } elsif (/INDEX 01/) {
+            my ($m,$s,$f) = /INDEX 01 (\d\d):(\d\d):(\d\d)/;
+            my $sector = ($m * SECONDS_PER_MINUTE * FRAMES_PER_SECOND) + ($s * FRAMES_PER_SECOND) + $f;
+            $tracks[$track] = {
+                number => $track,
+                sector => $sector,
+                msf    => "$m:$s:$f",
+            };
+        } elsif (/lead-out/) {
+            my ($total_samples) = /lead-out \d+ (\d+)/;
+            $tracks[0] = {
+                number => 170,
+                sector => $total_samples / SAMPLES_PER_FRAME,
+            };
+        }
+    }
+    return @tracks;
+}
+
+sub read_flac {
+    my ($self, $file) = @_;
+
+    my $flac = Audio::FLAC::Header->new($file);
+    my $cuesheet_lines = $flac->cuesheet;
+    my $CUE = IO::Lines->new($cuesheet_lines);
+
+    $self->tracks([ parse_cuesheet($CUE) ]);
+}
+
+sub read_cue {
+    my ($self, $file) = @_;
+
+    my $CUE = $file eq '-' ? \*STDIN : IO::File->new($file, '<');
+    $self->tracks([ parse_cuesheet($CUE) ]);
+}
+
 sub has_tracks {
     my $self = shift;
@@ -73,5 +130,6 @@
 }
 
-sub get_mbz_discid {
+# https://musicbrainz.org/doc/Disc_ID_Calculation
+sub get_musicbrainz_discid {
     my ($self) = @_;
 
@@ -96,4 +154,20 @@
 }
 
+sub get_musicbrainz_tocdata {
+    my ($self) = @_;
+    my @tracks = @{ $self->tracks };
+    # this is a CD TOC suitable for submitting to MusicBrainz as a CD Stub
+    # http://musicbrainz.org/doc/XML_Web_Service#Submitting_a_CDStub
+    return (
+        # first track number
+        $tracks[1]{number},
+        # last track number
+        $tracks[-1]{number},
+        # last frame (sector?)
+        $tracks[0]{sector} + SECTOR_OFFSET,
+        # start frame for each track
+        map { $_->{sector} + SECTOR_OFFSET } @tracks[1 .. @tracks - 1],
+    );
+}
 
 sub get_cuesheet {
@@ -113,10 +187,4 @@
 }
 
-sub get_cdparanoia_span {
-    my ($self) = @_;
-    # use a msf start unless track 1 begins at sector
-    return $self->tracks->[1]{sector} == 0 ? '1-' : '00:00.00-';
-}
-
 # module return
 1;
