Initial, mostly-working version of transcoding filesystem.
authorSteve McIntyre <steve@einval.com>
Wed, 16 Jun 2010 20:03:57 +0000 (21:03 +0100)
committerSteve McIntyre <steve@einval.com>
Wed, 16 Jun 2010 20:03:57 +0000 (21:03 +0100)
Single-threaded only, using a cache of output file sizes to make
readdir() work faster.

fuse-music.pl [new file with mode: 0755]

diff --git a/fuse-music.pl b/fuse-music.pl
new file mode 100755 (executable)
index 0000000..29ca5a1
--- /dev/null
@@ -0,0 +1,246 @@
+#!/usr/bin/perl
+
+use strict;
+use Filesys::Statvfs;
+use Fuse;
+use IO::File;
+use POSIX qw(ENOENT EROFS ENOSYS EEXIST EPERM EBUSY O_RDONLY O_RDWR O_APPEND O_CREAT);
+use Fcntl qw(O_RDWR O_WRONLY);
+use Audio::FLAC::Header;
+use DB_File;
+
+my %size_array;
+my $ogg_quality = 7;
+my $base_dir = "";
+my $size_cache_db = ".sizes";
+my $size_factor = 0.25;
+my $oggenc = "oggenc -q $ogg_quality -Q -o -";
+my $current_file;
+my $current_file_size = 0;
+my $current_file_in_use = 0;
+my $membuf;
+my $read_size = 32768;
+my $debug = 0;
+open(MEMORY,'+>', \$membuf)
+    or die "Can't open memory file: $!";
+
+sub fixup {
+    my $file = shift;
+    $file =~ s/\.ogg$/.flac/;
+    return $base_dir . $file
+}
+
+sub file_size {
+       my ($file) = shift;
+    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
+        $atime,$mtime,$ctime,$blksize,$blocks)
+        = lstat($file);
+    if (! $dev) {
+        return -$!;
+    }
+    return $size;
+}
+
+sub raw_audio_size {
+    my ($file) = shift;
+    my $flac = Audio::FLAC::Header->new($file) or return -$!;
+    my $info = $flac->info();
+    return (4 * $info->{TOTALSAMPLES});
+}
+
+sub encode_file {
+    my ($file) = shift;
+    my $bytes_read = 0;
+    my $buf;
+    if ($current_file_in_use) {
+        if (! (($current_file eq "") or ($current_file eq $file))) {
+            return -EBUSY();
+        }
+    }
+    $current_file = $file;
+    $current_file_size = 0;
+    $current_file_in_use = 1;
+    truncate(MEMORY, 0);
+    seek(MEMORY, 0, 0);
+    my $escaped_file = quotemeta($current_file);
+    if ($debug) {
+        print "Calling $oggenc $escaped_file |\n";
+    }
+    open(OGGREAD, "$oggenc $escaped_file |")
+        or print "Failed to open, $!\n";
+    while (($bytes_read = read(OGGREAD, $buf, $read_size)) > 0) {
+        $current_file_size += $bytes_read;
+        print MEMORY $buf;
+    }
+    if ($debug) {
+        print "Read $current_file_size bytes from oggenc\n";
+    }
+       close(OGGREAD);
+    $current_file_in_use = 0;
+    return 0;
+}
+
+sub x_getsize {
+    my ($file) = shift;
+    my $key = "ogg-$ogg_quality" . $file;
+    my $size = 0;
+    # Do we have the file size in the DB?
+    $size = $size_array{"$key"};
+    if ($size) {
+        if ($debug) {
+            print "$file: returning cached size $size from hash db\n";
+        }
+        return $size;
+    } else {
+        # Nope, we'll have to work it out
+        if ($debug) {
+            print "$file: don't have a cached size, encoding now\n";
+        }
+        my $ret = encode_file($file);
+        if ($ret != 0) {
+            return $ret;
+        }
+        $size_array{"$key"} = $current_file_size;
+        return $current_file_size;
+    }
+}
+
+sub x_getattr {
+       my ($file) = fixup(shift);
+    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
+        $atime,$mtime,$ctime,$blksize,$blocks)
+        = lstat($file);
+    if (! $dev) {
+        return -$!;
+    }
+    if ($file =~ /\.flac$/) {
+        $size = x_getsize($file);
+        if ($debug) {
+            print "ogg size $size bytes\n";
+        }
+    }
+       return ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
+            $atime,$mtime,$ctime,$blksize,$blocks);
+}
+
+sub x_getdir {
+       my ($dirname) = fixup(shift);
+       unless(opendir(DIRHANDLE,$dirname)) {
+               return -ENOENT();
+       }
+    my @outfiles;
+       my (@infiles) = readdir(DIRHANDLE);
+    foreach my $file(@infiles) {
+        if ($file =~ /\.flac$/) {
+            $file =~ s/\.flac$/.ogg/;
+        }
+        if ($file !~ $size_cache_db) {
+            push @outfiles, $file;
+        }
+    }
+       closedir(DIRHANDLE);
+       return (@outfiles, 0);
+}
+
+sub x_open {
+       my ($file) = fixup(shift);
+       my ($mode) = shift;
+    if ($mode & (O_RDWR|O_WRONLY)) {
+        return -EROFS();
+    }
+    if (! ($file =~ /\.flac$/)) {
+        return -$! unless sysopen(FILE,$file,$mode);
+        if ($debug) {
+            print "Non-flac, so reading normally\n";
+        }
+        close(FILE);
+        return 0;
+    }
+
+    return encode_file($file);
+}
+
+sub x_read {
+       my ($file,$bufsize,$off) = @_;
+       my $rv = -ENOSYS();
+    my $bytes_read;
+    $file = fixup($file);
+    if (! ($file =~ /\.flac$/)) {
+        -e $file or return -ENOENT();
+        my ($fsize) = -s $file;
+        open (FILE, '<', $file) or return $!;
+        if(seek(FILE,$off,SEEK_SET)) {
+            $bytes_read = read(FILE, $rv, $bufsize);
+            if ($debug) {
+                print "$bytes_read/$bufsize bytes of raw data read from file $file\n";
+            }
+        }
+        close(FILE);
+    } else {
+        if(seek(MEMORY,$off,SEEK_SET)) {
+            $bytes_read = read(MEMORY, $rv, $bufsize);
+            if ($debug) {
+                print "$bytes_read bytes of transcoded data read from file\n";
+            }
+        }
+    }
+       return $rv;
+}
+
+sub x_release {
+    my ($file) = fixup(shift);
+    if ($file eq $current_file) {
+        $current_file = "";
+        $current_file_size = 0;
+        truncate(MEMORY, 0);
+        seek(MEMORY, 0, 0);
+    }
+    return 0;
+}
+
+sub x_readonly {
+    return -EROFS();
+}
+
+sub err { return (-shift || -$!) }
+
+sub x_readlink { return readlink(fixup(shift)                 ); }
+
+sub x_statfs {
+       my($bsize, $frsize, $blocks, $bfree, $bavail,
+               $files, $ffree, $favail, $flag, $namemax) = Filesys::Statvfs::statvfs("$base_dir");
+       return ($namemax, $files, 0, $blocks, 0, $bsize);
+}
+       
+my ($mountpoint) = "";
+$base_dir = shift(@ARGV) if @ARGV;
+$mountpoint = shift(@ARGV) if @ARGV;
+if (!defined($base_dir) or !defined($mountpoint)) {
+    die "Need a base directory and a mountpoint!\n";
+}
+tie %size_array, 'DB_File', "$base_dir/$size_cache_db";
+Fuse::main(
+       mountpoint=>$mountpoint,
+       getattr =>"main::x_getattr",
+       readlink=>"main::x_readlink",
+       getdir  =>"main::x_getdir",
+       mknod   =>"main::x_readonly",
+       mkdir   =>"main::x_readonly",
+       unlink  =>"main::x_readonly",
+       rmdir   =>"main::x_readonly",
+       symlink =>"main::x_readonly",
+       rename  =>"main::x_readonly",
+       link    =>"main::x_readonly",
+       chmod   =>"main::x_readonly",
+       chown   =>"main::x_readonly",
+       truncate=>"main::x_readonly",
+       utime   =>"main::x_readonly",
+       write   =>"main::x_readonly",
+       open    =>"main::x_open",
+       release =>"main::x_release",
+       read    =>"main::x_read",
+       statfs  =>"main::x_statfs",
+       threaded=>0,
+       debug => $debug,
+    mountopts =>"allow_other",
+);