[gfvs] cdda backend



Hey Alex,

(Sorry to continue bombarding you with super long mails and patches)

Here's a cdda:// backend for gvfs. Before going into details (and some
minor, probably easily fixable, architectural issues I've identified)
it's probably useful to answer the question "Why a cdda backend?" since
that question was asked here

http://mail.gnome.org/archives/gnome-vfs-list/2005-August/msg00001.html
http://mail.gnome.org/archives/gnome-vfs-list/2005-August/msg00022.html

It seems the main objections to that new cdda module for gnome-vfs were

 - would pull gstreamer into the client apps 

 - seeking didn't work

 - file manager not a good UI for ripping songs; better to launch
   the cd player

However, that ignores several fundamental problems

 - libcdio / libparanoia isn't the API application programmers
   should use (however, gio is)

 - nasty locking issues when doing things decentralized (if it's
   up to the software to do the raw device access it might do
   uninformed things like using O_EXCL)

 - duplication in every app (extraction, meta data retrieval)

So I wrote the attached cdda module because with gio/gvfs I think it
actually makes sense have a cdda:// backend (also, and sorry for
sounding like a cargo cult follower, to have feature parity with Mac OS
X and Windows). And I also wanted to get a feel / help review the plugin
API before we freeze. 

First, a small feature list

 - Stateful mounts; yes, you actually "mount" the cdda gvfs filesystem
   driver; there's one instance of gvfsd-cdda per mount. You also have
   to mount the fs manually: gvfs-mount cdda://sr0

 - Seeking just works (try 'totem ~/.gvfs/CD\ Audio\ on\ sr0/Track\
   2.wav' and seek away)

 - Works with multiple clients at the same time as well as when
   a iso9660 filesystem is mounted by a kernel driver. There's seeking
   and it's a bit slow (can be alleviated once we introduce readahead
   in the GVfsBackend base class, yes?) but basically works very well

    http://people.freedesktop.org/~david/mixed-cdda-iso9660-copying.png
    (Copying two cdda tracks and four files from the iso9660 fs)

 - Puts RIFF tags in the .wav files containing metadata; though no
   metadata framework is currently hooked up (I tried experimentally
   hooking up s-j's libjuicer and it works very well; unfortunately
   that library is pulling in gtk, gnome-vfs, gconf... Ross?)

   (unfortunately #437499 shows these are just ignored. But that's
    fixable.)

 - Uses libcdio which I think is portable to Solaris and other
   non-Linux platforms too

 - See the actual patch for remaining TODO's / feature ideas

Some issues I've encounter with gvfs; some are probably small bugs; the
last one is more on an architectural level and probably needs addressing
before freezing..

 1. The GVfs FUSE daemon never class close() on my fs backend when
    it's done reading.. this leads to leaks. Have you seen this?

 2. I have to set GMountSpec in try_mount(); it's not enough just
    setting it in do_mount(). Bug?

 3. Once mounted, there's no way for the backend to change it's display
    name. I think I understand why you did this; it's because of the
    FUSE daemon right? E.g. you want to ensure that there are no
    name collisions to make it easier to figure out what directories
    to return in ~/.gvfs ...

    Anyway, the reason we want this is that initially we probably want
    the display name "Audio Disc" and if we (much later) acquire meta
    data for the disc we want to change it to "Tori Amos - Silent
    all these years" or whatever the title is.

    (same applies for the icon (think cover art); I haven't tested if
     this doesn't work)

 4. There's very little in way of documentation so it was really
    confusing to start writing the backend. Once I got the hang of
    it, it was a smooth ride though.

 5. There needs to be a way to associate a GDaemonMount to an existing
    GVolume object (stemming from the HAL volume monitor). This is
    not a problem inherent to the CDDA backend; it applies to any
    backend that provides a user space file system driver for a block
    device (e.g. NTFS, secure paranoid vfat etc.).

The first problems are probably easily fixable; the last one is more
problematic. Here's how I envision it will work

User inserts audio disc into drive. The HAL volume monitor notices this
and creates a GVolume object representing the unmounted audio disc. The
mount() method on the HAL volume monitor for said GVolume will simply do
the equivalent of

     $ gvfs-mount cdda://sr0

since it knows there a cdda:// backend already. So when this happens the
gvfs-cdda backend is started for sr0 and as a result a GDaemonMount is
created in the process space of all gvfs clients using volume monitoring
(via GDaemonVolumeMonitor). Now, I think all we need to do is

 o  Make the GHalVolume live object for sr0 note that the
    new GDaemonMount object was created and associate with it (e.g.
    take a weak ref and emit "changed". Future calls into get_mount()
    for the GHalVolume will return that ref).

 o  Have a way for the gvfsd-cdda backend to tell that the created
    GDaemonMount objects representing it needs to make get_volume()
    on them return a reference to the GHalVolume object.

This all sounds complicated but I'm not sure it really is. I think all
we need to do is to have some kind of tags we can decorate the
GHalVolume and GDaemonVolume objects so they can find each other.
Anyway, you probably have some ideas I haven't thought about...

      David

Index: configure.ac
===================================================================
--- configure.ac	(revision 1041)
+++ configure.ac	(working copy)
@@ -78,6 +78,28 @@
 
 AC_SEARCH_LIBS(login_tty, util, [AC_DEFINE([HAVE_LOGIN_TTY],[],[Whether login_tty is available])])
 
+dnl **************************************************
+dnl *** Check if we should build with CDDA backend ***
+dnl **************************************************
+AC_ARG_ENABLE(cdda, [  --disable-cdda           build without CDDA backend])
+msg_cdda=no
+CDDA_LIBS=
+CDDA_CFLAGS=
+
+if test "x$enable_cdda" != "xno"; then
+  PKG_CHECK_EXISTS(libcdio_paranoia >= 0.78.2, msg_cdda=yes)
+
+  if test "x$msg_cdda" == "xyes"; then
+    PKG_CHECK_MODULES(CDDA, libcdio_paranoia)
+    AC_DEFINE(HAVE_CDDA, 1, [Define to 1 if CDDA is going to be built])
+  fi
+fi
+
+AC_SUBST(CDDA_LIBS)
+AC_SUBST(CDDA_CFLAGS)
+
+AM_CONDITIONAL(USE_CDDA, [test "$msg_cdda" = "yes"])
+
 dnl **********************
 dnl *** Check for FUSE ***
 dnl **********************
@@ -100,6 +122,33 @@
 
 AM_CONDITIONAL(USE_FUSE, [test "$msg_fuse" = "yes"])
 
+dnl **********************
+dnl *** Check for HAL ***
+dnl **********************
+AC_ARG_ENABLE(hal, [  --disable-hal           build without HAL support])
+msg_hal=no
+have_hal_fast_init=no
+HAL_LIBS=
+HAL_CFLAGS=
+
+if test "x$enable_hal" != "xno"; then
+  PKG_CHECK_EXISTS(hal >= 0.5.9 dbus-glib-1, msg_hal=yes)
+
+  if test "x$msg_hal" == "xyes"; then
+    PKG_CHECK_MODULES(HAL, hal >= 0.5.9 dbus-glib-1)
+    AC_DEFINE(HAVE_HAL, 1, [Define to 1 if HAL is available])
+    AC_CHECK_LIB(hal, libhal_get_all_devices_with_properties, have_hal_fast_init=yes)
+    if test "x$have_hal_fast_init" == "xyes"; then
+      AC_DEFINE(HAVE_HAL_FAST_INIT, 1, [Define to 1 if libhal_get_all_devices_with_properties is available])
+    fi
+  fi
+fi
+
+AC_SUBST(HAL_LIBS)
+AC_SUBST(HAL_CFLAGS)
+
+AM_CONDITIONAL(USE_HAL, [test "$msg_hal" = "yes"])
+
 dnl ==========================================================================
 dnl Samba 3.0
 
@@ -228,6 +277,7 @@
 common/Makefile
 client/Makefile
 daemon/Makefile
+hal/Makefile
 programs/Makefile
 test/Makefile
 po/Makefile.in
@@ -236,6 +286,8 @@
 echo 
 echo "gvfs configuration summary:"
 echo "
-	Samba support:	      $msg_samba
-	FUSE support:         $msg_fuse
+	Samba support:	              $msg_samba
+	FUSE support:                 $msg_fuse
+        CDDA support:                 $msg_cdda
+	Use HAL for volume monitor:   $msg_hal (with fast init path: $have_hal_fast_init)
 "
Index: daemon/Makefile.am
===================================================================
--- daemon/Makefile.am	(revision 1041)
+++ daemon/Makefile.am	(working copy)
@@ -30,10 +30,10 @@
 
 mountdir = $(sysconfdir)/gvfs/mounts
 
-libexec_PROGRAMS=gvfsd gvfsd-ftp gvfsd-sftp gvfsd-trash
+libexec_PROGRAMS=gvfsd gvfsd-ftp gvfsd-sftp gvfsd-trash gvfsd-cdda
 
-mount_in_files = ftp.mount.in sftp.mount.in trash.mount.in
-mount_DATA = ftp.mount sftp.mount  trash.mount
+mount_in_files = ftp.mount.in sftp.mount.in trash.mount.in cdda.mount.in
+mount_DATA = ftp.mount sftp.mount trash.mount cdda.mount
 
 if HAVE_SAMBA
 mount_in_files += smb.mount.in smb-browse.mount.in
@@ -173,3 +173,17 @@
 	-DBACKEND_TYPES='"trash", G_VFS_TYPE_BACKEND_TRASH,'
 
 gvfsd_trash_LDADD = $(libraries)
+
+gvfsd_cdda_SOURCES = \
+	gvfsbackendcdda.c gvfsbackendcdda.h \
+	daemon-main.c daemon-main.h \
+	daemon-main-generic.c 
+
+gvfsd_cdda_CPPFLAGS = \
+	-DBACKEND_HEADER=gvfsbackendcdda.h \
+	-DDEFAULT_BACKEND_TYPE=cdda \
+	-DMAX_JOB_THREADS=1 \
+	$(CDDA_CFLAGS) \
+	-DBACKEND_TYPES='"cdda", G_VFS_TYPE_BACKEND_CDDA,'
+
+gvfsd_cdda_LDADD = $(libraries) $(CDDA_LIBS)
--- /dev/null	2007-12-13 08:58:08.580097562 -0500
+++ daemon/gvfsbackendcdda.c	2007-12-16 17:26:02.000000000 -0500
@@ -0,0 +1,750 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* GVFS cdrom audio file system driver
+ * 
+ * Copyright (C) 2007 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General
+ * Public License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: David Zeuthen <davidz redhat com>
+ */
+
+/* NOTE: since we link the libcdio libs (GPLv2) into our process space
+ * the combined work is GPLv2. This source file, however, is LGPLv2+.
+ */
+
+#include <config.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+
+#include <glib/gstdio.h>
+#include <glib/gi18n.h>
+#include <gio/gio.h>
+
+#include "gvfsbackendcdda.h"
+#include "gvfsjobopenforread.h"
+#include "gvfsjobread.h"
+#include "gvfsjobseekread.h"
+#include "gvfsjobqueryinfo.h"
+#include "gvfsjobenumerate.h"
+
+#define DO_NOT_WANT_PARANOIA_COMPATIBILITY
+#include <cdio/paranoia.h>
+
+/* TODO:
+ *
+ * - GVFS integration
+ *   - Need to unmount ourselves when the backing media is removed
+ *   - Need to have some way of making our resulting GDaemonMount object (in the client address space)
+ *     be associated with the GVolume (probably stems from the HAL volume monitor).. both ways
+ *   - g_vfs_backend_set_display_name() needs to work post mount
+ *
+ * - Metadata
+ *   - Use CD-Text to read metadata from the physical disc
+ *     - http://en.wikipedia.org/wiki/CD-Text
+ *     - libcdio can do this
+ *   - Use Musicbrainz to read metadata from the net
+ *     - libmusicbrainz appear to be a dead-end: http://musicbrainz.org/doc/libmusicbrainz
+ *     - Need to provide some UI for configuring musicbrainz; think proxy, local server, 
+ *       lockdown (secure facilities don't want us to randomly connect to the Interwebs)
+ *   - Ideally use libjuicer for all this
+ *     - however it is currently private to sound-juicer and brings in GTK+, gnome-vfs, gconf...
+ *   - Use metadata for file names and display_name of our Mount (using g_vfs_backend_set_display_name())
+ *   - Also encode metadata in the WAV header so transcoding to Vorbis or MP3 Just Works(tm)
+ *     - This is already done; see create_header() in this file
+ *
+ * - Scratched discs / error conditions from paranoia
+ *   - Need to handle this better...
+ *
+ * - Sector cache? Might be useful to maintain a cache of previously read sectors
+ */
+
+/*--------------------------------------------------------------------------------------------------------------*/
+
+struct _GVfsBackendCdda
+{
+  GVfsBackend parent_instance;
+
+  char *device_path;
+  cdrom_drive_t *drive;
+};
+
+G_DEFINE_TYPE (GVfsBackendCdda, g_vfs_backend_cdda, G_VFS_TYPE_BACKEND);
+
+static void
+g_vfs_backend_cdda_finalize (GObject *object)
+{
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (object);
+
+  //g_warning ("finalizing %p", object);
+
+  g_free (cdda_backend->device_path);
+
+  if (G_OBJECT_CLASS (g_vfs_backend_cdda_parent_class)->finalize)
+    (*G_OBJECT_CLASS (g_vfs_backend_cdda_parent_class)->finalize) (object);
+}
+
+static void
+g_vfs_backend_cdda_init (GVfsBackendCdda *cdda_backend)
+{
+  GVfsBackend *backend = G_VFS_BACKEND (cdda_backend);
+  GMountSpec *mount_spec;
+
+  //g_warning ("initing %p", cdda_backend);
+
+  g_vfs_backend_set_display_name (backend, "cdda");
+  // TODO: HMM: g_vfs_backend_set_user_visible (backend, FALSE);  
+
+  mount_spec = g_mount_spec_new ("cdda");
+  g_vfs_backend_set_mount_spec (backend, mount_spec);
+  g_mount_spec_unref (mount_spec);
+}
+
+static void
+do_mount (GVfsBackend *backend,
+	  GVfsJobMount *job,
+	  GMountSpec *mount_spec,
+	  GMountSource *mount_source,
+	  gboolean is_automount)
+{
+  char *display_name;
+  const char *host;
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (backend);
+  GError *error = NULL;
+  GMountSpec *cdda_mount_spec;
+
+  host = g_mount_spec_get (mount_spec, "host");
+  //g_warning ("host=%s", host);
+  if (host == NULL)
+    {
+      g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, _("No drive specified"));
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      return;
+    }
+
+  cdda_backend->device_path = g_strdup_printf ("/dev/%s", host);
+
+  cdda_backend->drive = cdio_cddap_identify (cdda_backend->device_path, 0, NULL);
+  if (cdda_backend->drive == NULL)
+    {
+      g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, 
+                   _("Cannot find drive %s"), cdda_backend->device_path);
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      return;
+    }
+
+  if (cdio_cddap_open (cdda_backend->drive) != 0)
+    {
+      cdio_cddap_close (cdda_backend->drive);
+      g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, 
+                   _("Drive %s does not contain audio files"), cdda_backend->device_path);
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      return;
+    }
+
+  display_name = g_strdup_printf (_("CD Audio on %s"), host);
+  g_vfs_backend_set_display_name (backend, display_name);
+  g_free (display_name);
+  g_vfs_backend_set_icon_name (backend, "media-optical-audio");
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+  cdda_mount_spec = g_mount_spec_new ("cdda");
+  g_mount_spec_set (cdda_mount_spec, "host", host);
+  g_vfs_backend_set_mount_spec (backend, cdda_mount_spec);
+  g_mount_spec_unref (cdda_mount_spec);
+
+  //g_warning ("mounted %p", cdda_backend);
+}
+
+static gboolean
+try_mount (GVfsBackend *backend,
+           GVfsJobMount *job,
+           GMountSpec *mount_spec,
+           GMountSource *mount_source,
+           gboolean is_automount)
+{
+  const char *host;
+  GError *error = NULL;
+  GMountSpec *cdda_mount_spec;
+
+  /* TODO: Hmm.. apparently we have to set the mount spec in
+   * try_mount(); doing it in mount() won't work.. 
+   */
+
+  host = g_mount_spec_get (mount_spec, "host");
+  //g_warning ("tm host=%s", host);
+  if (host == NULL)
+    {
+      g_set_error (&error, G_IO_ERROR, G_IO_ERROR_FAILED, _("No drive specified"));
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      g_error_free (error);
+      return TRUE;
+    }
+
+  cdda_mount_spec = g_mount_spec_new ("cdda");
+  g_mount_spec_set (cdda_mount_spec, "host", host);
+  g_vfs_backend_set_mount_spec (backend, cdda_mount_spec);
+  g_mount_spec_unref (cdda_mount_spec);
+  return FALSE;
+}
+
+
+static void
+do_unmount (GVfsBackend *backend,
+            GVfsJobUnmount *job)
+{
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (backend);
+
+  if (cdda_backend->drive != NULL)
+    {
+      //g_warning ("closed drive %p", backend);
+      cdio_cddap_close (cdda_backend->drive);
+    }
+
+  //g_warning ("unmounted %p", backend);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+}
+
+/* returns -1 if we couldn't map */
+static int
+get_track_num_from_name (GVfsBackendCdda *cdda_backend, const char *filename)
+{
+  int n;
+  char *basename;
+
+  basename = g_path_get_basename (filename);
+  if (sscanf (basename, "Track %d.wav", &n) == 1)
+    {
+      g_free (basename);
+      return n;
+    }
+
+  return -1;
+}
+
+typedef struct {
+  cdrom_paranoia_t *paranoia;
+
+  long size;           /* size of file being read */
+  long header_size;    /* size of the header */
+  long content_size;   /* size of content after the header */
+
+  long cursor;         /* cursor into the file being read */
+
+  long first_sector;   /* first sector of raw PCM audio data */
+  long last_sector;    /* last sector of raw PCM audio data */
+  long sector_cursor;  /* sector we're currently at */
+
+  char *header;        /* header payload */
+
+  /* These following two fields are used for caching the last read sector. This
+   * is to avoid seeking back if fewer bytes than whole sector is requested.
+   */
+  long buf_at_sector_num;                     /* the sector that is cached */
+  char buf_at_sector[CDIO_CD_FRAMESIZE_RAW];  /* the data of the sector */
+
+} ReadHandle;
+
+static void
+free_read_handle (ReadHandle *read_handle)
+{
+  if (read_handle->paranoia != NULL)
+    cdio_paranoia_free (read_handle->paranoia);
+  g_free (read_handle->header);
+  g_free (read_handle);
+}
+
+static char *
+create_header (GVfsBackendCdda *cdda_backend, long *header_size, long content_size)
+{
+  char *artist;
+  char *title;
+  const char *software;
+  size_t artist_len;
+  size_t title_len;
+  size_t software_len;
+  char *header;
+  char *ptr;
+  int var;
+
+  /* See http://www.saettler.com/RIFFMCI/riffmci.html for the spec.
+   *
+   * artist -> IART
+   * title -> INAM
+   * track_number -> ?? (TODO: work with GStreamer people on coordinate with the wavparse plugin)
+   *
+   * software -> ISFT
+   */
+
+
+  //artist = g_strdup ("Homer Simpson");
+  //title = g_strdup ("Simpsons Jail House Rock");
+
+  /* TODO: fill in from metadata */
+  artist = NULL;
+  title = NULL;
+  software = "gvfs-cdda using libcdio " CDIO_VERSION;
+
+  artist_len = 0;
+  title_len = 0;
+
+  /* ensure even length and include room for the chunk */
+  if (artist != NULL)
+    artist_len = 2 * ((strlen (artist) + 2) / 2) + 8;
+  if (title != NULL)
+    title_len = 2 * ((strlen (title) + 2) / 2) + 8;
+  software_len = 2 * ((strlen (software) + 2) / 2) + 8;
+
+  *header_size = 44;
+  *header_size += 12; /* for LIST INFO */
+  *header_size += artist_len;
+  *header_size += title_len;
+  *header_size += software_len;
+  header = g_new0 (char, *header_size);
+
+  ptr = header;
+  memcpy (ptr, "RIFF", 4); ptr += 4;
+  var = content_size + *header_size - 8;
+  memcpy (ptr, &var, 4); ptr += 4;
+  memcpy (ptr, "WAVE", 4); ptr += 4;
+
+  memcpy (ptr, "fmt ", 4); ptr += 4;
+  var = 16;
+  memcpy (ptr, &var, 4); ptr += 4;
+  var = 1;
+  memcpy (ptr, &var, 2); ptr += 2;
+  var = 2;
+  memcpy (ptr, &var, 2); ptr += 2;
+  var = 44100;
+  memcpy (ptr, &var, 4); ptr += 4;
+  var = 44100 * 2 * 2;
+  memcpy (ptr, &var, 4); ptr += 4;
+  var = 4;
+  memcpy (ptr, &var, 2); ptr += 2;
+  var = 16;
+  memcpy (ptr, &var, 2); ptr += 2;
+
+  memcpy (ptr, "LIST", 4); ptr += 4;
+  var = 4 + artist_len + title_len + software_len;
+  memcpy (ptr, &var, 4); ptr += 4;
+  memcpy (ptr, "INFO", 4); ptr += 4;
+
+  if (artist != NULL)
+    {
+      memcpy (ptr, "IART", 4);
+      var = artist_len - 8;
+      memcpy (ptr + 4, &var, 4);
+      strncpy (ptr + 8, artist, artist_len); 
+      ptr += artist_len;
+    }
+
+  if (title != NULL)
+    {
+      memcpy (ptr, "INAM", 4);
+      var = title_len - 8;
+      memcpy (ptr + 4, &var, 4);
+      strncpy (ptr + 8, title, title_len); 
+      ptr += title_len;
+    }
+
+  memcpy (ptr, "ISFT", 4);
+  var = software_len - 8;
+  memcpy (ptr + 4, &var, 4);
+  strncpy (ptr + 8, software, software_len); 
+  ptr += software_len;
+
+  memcpy (ptr, "data", 4); ptr += 4;
+  memcpy (ptr, &content_size, 4); ptr += 4;
+
+  g_free (artist);
+  g_free (title);
+
+  return header;
+}
+
+static void
+do_open_for_read (GVfsBackend *backend,
+                  GVfsJobOpenForRead *job,
+                  const char *filename)
+{
+  int track_num;
+  GError *error;
+  ReadHandle *read_handle;
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (backend);
+
+  //g_warning ("open_for_read (%s)", filename);
+
+  read_handle = g_new0 (ReadHandle, 1);
+
+  track_num = get_track_num_from_name (cdda_backend, job->filename);
+  if (track_num == -1)
+    {
+      error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, 
+                           _("No such file %s on drive %s"), job->filename, cdda_backend->device_path);
+      g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+      free_read_handle (read_handle);
+      return;
+    }
+
+
+  read_handle->first_sector = cdio_cddap_track_firstsector (cdda_backend->drive, track_num);
+  read_handle->last_sector = cdio_cddap_track_lastsector (cdda_backend->drive, track_num);
+  read_handle->sector_cursor = -1;
+
+  read_handle->cursor = 0;
+  read_handle->buf_at_sector_num = -1;
+  read_handle->content_size  = ((read_handle->last_sector - read_handle->first_sector) + 1) * CDIO_CD_FRAMESIZE_RAW;
+
+  read_handle->header = create_header (cdda_backend, &(read_handle->header_size), read_handle->content_size);
+  read_handle->size = read_handle->header_size + read_handle->content_size;
+
+  read_handle->paranoia = cdio_paranoia_init (cdda_backend->drive);
+  cdio_paranoia_modeset (read_handle->paranoia, PARANOIA_MODE_DISABLE);
+
+  g_vfs_job_open_for_read_set_can_seek (job, TRUE);
+  g_vfs_job_open_for_read_set_handle (job, GINT_TO_POINTER (read_handle));
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+}
+
+/* We have to pass in a callback to paranoia_read, even though we don't use it */
+static void 
+paranoia_callback (long int inpos, paranoia_cb_mode_t function)
+{
+}
+
+
+static void
+do_read (GVfsBackend *backend,
+         GVfsJobRead *job,
+         GVfsBackendHandle handle,
+         char *buffer,
+         gsize bytes_requested)
+{
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (backend);
+  ReadHandle *read_handle = (ReadHandle *) handle;
+  int bytes_read;
+  long skip_bytes;
+  char *readbuf;
+  long desired_sector;
+  int bytes_to_copy;
+  long cursor_in_stream;
+
+  //g_warning ("read (%"G_GSSIZE_FORMAT") (@ %ld)", bytes_requested, read_handle->cursor);
+
+  /* header */
+  if (read_handle->cursor < read_handle->header_size)
+    {
+      skip_bytes = read_handle->cursor;
+      bytes_read = read_handle->header_size - read_handle->cursor;
+      readbuf = read_handle->header + skip_bytes;
+      goto read_data_done;
+    }
+
+  /* EOF */
+  if (read_handle->cursor >= read_handle->size)
+    {
+      skip_bytes = 0;
+      bytes_read = 0;
+      readbuf = NULL;
+      goto read_data_done;
+    }
+
+  cursor_in_stream = read_handle->cursor - read_handle->header_size;
+
+  desired_sector = cursor_in_stream / CDIO_CD_FRAMESIZE_RAW + read_handle->first_sector;
+
+  if (desired_sector == read_handle->buf_at_sector_num)
+    {
+      /* got it cached */
+
+      /* skip some bytes */
+      skip_bytes = cursor_in_stream - (desired_sector - read_handle->first_sector) * CDIO_CD_FRAMESIZE_RAW;
+      readbuf = read_handle->buf_at_sector + skip_bytes;
+      bytes_read = CDIO_CD_FRAMESIZE_RAW - skip_bytes;
+
+      //g_warning ("read from cache for cursor @ %ld", read_handle->buf_at_sector_num);
+    }
+  else
+    {
+      /* first check that we're at the right sector */
+      if (desired_sector != read_handle->sector_cursor)
+        {
+          cdio_paranoia_seek (read_handle->paranoia, desired_sector, SEEK_SET);
+          read_handle->sector_cursor = desired_sector;
+          //g_warning ("seeking cursor to %ld", read_handle->sector_cursor);
+        }
+      
+      /* skip some bytes */
+      skip_bytes = cursor_in_stream - (read_handle->sector_cursor - read_handle->first_sector) * CDIO_CD_FRAMESIZE_RAW;
+      //g_warning ("advanced cursor to %ld", read_handle->sector_cursor);
+      
+      readbuf = (char *) cdio_paranoia_read (read_handle->paranoia, paranoia_callback);
+
+      if (readbuf == NULL)
+        {
+          g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR,
+                            g_io_error_from_errno (errno),
+                            _("Error from paranoia on drive %s"), cdda_backend->device_path);
+          return;
+        }
+
+      read_handle->buf_at_sector_num = read_handle->sector_cursor;
+      memcpy (read_handle->buf_at_sector, readbuf, CDIO_CD_FRAMESIZE_RAW);
+
+      read_handle->sector_cursor++;
+
+      readbuf += skip_bytes;
+      bytes_read = CDIO_CD_FRAMESIZE_RAW - skip_bytes;
+
+
+    }
+  
+ read_data_done:
+
+  bytes_to_copy = bytes_read;
+  if (bytes_requested < bytes_read)
+    bytes_to_copy = bytes_requested;
+
+  read_handle->cursor += bytes_to_copy;
+  cursor_in_stream = read_handle->cursor - read_handle->header_size;
+
+
+  if (bytes_to_copy > 0 && readbuf != NULL)
+    memcpy (buffer, readbuf, bytes_to_copy);
+
+  g_vfs_job_read_set_size (job, bytes_to_copy);
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+}
+
+static void
+do_seek_on_read (GVfsBackend *backend,
+		 GVfsJobSeekRead *job,
+		 GVfsBackendHandle handle,
+		 goffset    offset,
+		 GSeekType  type)
+{
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (backend);
+  ReadHandle *read_handle = (ReadHandle *) handle;
+  long new_offset;
+
+  //g_warning ("seek_on_read (%d, %d)", (int)offset, type);
+
+  switch (type)
+    {
+    default:
+    case G_SEEK_SET:
+      new_offset = offset;
+      break;
+    case G_SEEK_CUR:
+      new_offset = read_handle->cursor + offset;
+      break;
+    case G_SEEK_END:
+      new_offset = read_handle->size + offset;
+      break;
+    }
+
+  if (new_offset < 0 || new_offset >= read_handle->size)
+    {
+      g_vfs_job_failed (G_VFS_JOB (job), G_IO_ERROR,
+			G_IO_ERROR_FAILED,
+			_("Error seeking in stream on drive %s"), cdda_backend->device_path);
+    }
+  else
+    {
+      read_handle->cursor = new_offset;
+      
+      g_vfs_job_seek_read_set_offset (job, offset);
+      g_vfs_job_succeeded (G_VFS_JOB (job));
+    }
+  
+}
+
+static void
+do_close_read (GVfsBackend *backend,
+	       GVfsJobCloseRead *job,
+	       GVfsBackendHandle handle)
+{
+  ReadHandle *read_handle = (ReadHandle *) handle;
+
+  //g_warning ("close ()");
+
+  free_read_handle (read_handle);
+  
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+}
+
+static void
+set_info_for_track (GVfsBackendCdda *cdda_backend, GFileInfo *info, int track_num)
+{
+  char *header;
+  long first;
+  long last;
+  long header_size;
+  long content_size;
+  GIcon *icon;
+
+  first = cdio_cddap_track_firstsector (cdda_backend->drive, track_num);
+  last = cdio_cddap_track_lastsector (cdda_backend->drive, track_num);
+  content_size = (last - first + 1) * CDIO_CD_FRAMESIZE_RAW;
+
+  header = create_header (cdda_backend, &header_size, content_size);
+  g_free (header);
+
+  //g_warning ("size=%ld for track %d", size, track_num);
+
+  g_file_info_set_file_type (info, G_FILE_TYPE_REGULAR);
+  g_file_info_set_content_type (info, "audio/x-wav");
+  g_file_info_set_size (info, header_size + content_size);
+  icon = g_themed_icon_new ("audio-x-generic");
+  g_file_info_set_icon (info, icon);
+  g_object_unref (icon);
+
+}
+
+static void
+do_query_info (GVfsBackend *backend,
+	       GVfsJobQueryInfo *job,
+	       const char *filename,
+	       GFileQueryInfoFlags flags,
+	       GFileInfo *info,
+	       GFileAttributeMatcher *matcher)
+{
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (backend);
+  int track_num;
+  GError *error;
+
+  //g_warning ("get_file_info (%s)", filename);
+
+  if (strcmp (filename, "/") == 0)
+    {
+
+      GIcon *icon;
+      
+      g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY);
+      g_file_info_set_content_type (info, "inode/directory");
+      g_file_info_set_size (info, 0);
+      icon = g_themed_icon_new ("folder");
+      g_file_info_set_icon (info, icon);
+      g_object_unref (icon);
+    }
+  else
+    {
+      g_file_info_set_name (info, filename);
+      g_file_info_set_display_name (info, filename);
+      
+      track_num = get_track_num_from_name (cdda_backend, filename);
+      if (track_num == -1)
+        {
+          error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("No such file"));
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          return;
+        }
+
+      if (track_num > cdda_backend->drive->tracks)
+        {
+          error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("The file does not exist"));
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          return;
+        }
+
+      if (! cdio_cddap_track_audiop (cdda_backend->drive, track_num))
+        {
+          error = g_error_new (G_IO_ERROR, G_IO_ERROR_NOT_FOUND, _("The file does not exist or isn't an audio track"));
+          g_vfs_job_failed_from_error (G_VFS_JOB (job), error);
+          return;
+        }
+
+      set_info_for_track (cdda_backend, info, track_num);
+    }
+  
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+}
+
+static void
+do_enumerate (GVfsBackend *backend,
+              GVfsJobEnumerate *job,
+              const char *filename,
+              GFileAttributeMatcher *matcher,
+              GFileQueryInfoFlags flags)
+{
+  GVfsBackendCdda *cdda_backend = G_VFS_BACKEND_CDDA (backend);
+  GFileInfo *info;
+  GList *l;
+  int n;
+
+  //g_warning ("enumerate (%s)", filename);
+
+  l = NULL;
+
+  //g_warning ("there are %d tracks", drive->tracks);
+  
+  for (n = 1; n <= cdda_backend->drive->tracks; n++)
+    {
+      char *name;
+
+      /* not audio track */
+      if (! cdio_cddap_track_audiop (cdda_backend->drive, n))
+        continue;
+      
+      info = g_file_info_new ();
+
+      name = g_strdup_printf ("Track %d.wav", n);
+      g_file_info_set_name (info, name);
+      g_file_info_set_display_name (info, name);
+      g_free (name);
+
+      set_info_for_track (cdda_backend, info, n);
+      
+      l = g_list_append (l, info);
+    }
+
+  g_vfs_job_succeeded (G_VFS_JOB (job));
+
+  g_vfs_job_enumerate_add_infos (job, l);
+  g_list_foreach (l, (GFunc) g_object_unref, NULL);
+  g_list_free (l);
+
+  g_vfs_job_enumerate_done (job);
+}
+
+static void
+g_vfs_backend_cdda_class_init (GVfsBackendCddaClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GVfsBackendClass *backend_class = G_VFS_BACKEND_CLASS (klass);
+  
+  gobject_class->finalize = g_vfs_backend_cdda_finalize;
+
+  backend_class->try_mount = try_mount;
+  backend_class->mount = do_mount;
+  backend_class->unmount = do_unmount;
+  backend_class->open_for_read = do_open_for_read;
+  backend_class->read = do_read;
+  backend_class->seek_on_read = do_seek_on_read;
+  backend_class->close_read = do_close_read;
+  backend_class->query_info = do_query_info;
+  backend_class->enumerate = do_enumerate;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]