#!/usr/bin/perl -w
#
# id3fs - a FUSE-based filesystem for browsing audio metadata
# Copyright (C) 2010  Ian Beckwith <ianb@erislabs.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use Getopt::Long qw(Configure);
use ID3FS::DB;
use ID3FS::Fuse;
use POSIX;

use vars qw($me);
$me=($0=~/(?:.*\/)?(.*)/)[0];

our $VERSION="1.00";
my $verbose=0;
my $help=0;
my $dbpath=undef;
my $tagdepth=undef;

Configure(qw(bundling no_ignore_case));
my $optret=GetOptions(
    "verbose|v"    => sub { $verbose++; },
    "help|h"       => \$help,
    "database|f=s" => \$dbpath,
    "t|tagdepth=s" => \$tagdepth,
    "o=s"          => sub {;}, # silently drop options passed by mount
    );

usage() if(scalar(@ARGV) != 2 || !$optret || $help);

my $source=shift;
my $mountpoint=shift;

my $db=ID3FS::DB->new($me, $verbose, 0, $source, $dbpath);
exit unless($db);

my $fuse=ID3FS::Fuse->new($db, $source, $mountpoint, $verbose, $tagdepth);

# disassociate from terminal
unless($verbose)
{
    my $pid=fork();
    if(defined($pid))
    {
	exit if($pid);   # parent
	POSIX::setsid(); # child
    }
    else
    {
	print "$me: couldn't drop terminal: $!\n";
    }
}

$fuse->run();

sub usage
{
    die("Usage: $me [-vh] [-f <dbfile>] [-t <tagdepth>] [--] <sourcedir> <mountpoint>\n".
	" -t|--tagdepth=NUM\tMaximum number of tags in expression (default: 6)\n" .
	" -f|--database=FILE\tPath to database file\n" .
	" -v\t\t\tVerbose (repeat for more verbosity)\n".
	" -h\t\t\tThis help\n".
	" --\t\t\tEnd of options\n");
}

__END__

=head1 NAME

id3fsd - FUSE filesystem for browsing id3 tags

=head1 SYNOPSIS

B<id3fsd> [B<-vh>] S<B<[-f >I<dbfile>]> [B<-->] I<SOURCEDIR> I<MOUNTPOINT>

=head1 DESCRIPTION

id3fsd provides a browsable filesystem of your music files, organised
into sub-directories by id3 tags (or flac/ogg comments).

id3fsd allows you to construct boolean queries from a tag folksonomy
such as:

  goth/AND/decade/1980s/
  postrock/AND/NOT/rating/terrible/
  thrash/OR/rapmetal/AND/NOT/wears-a-red-hat/
  prog/AND/decade/1970s/OR/psychedelia/AND/decade/1960s/
  location/sweden/AND/screamo/AND/postrock/

Multiple tags can be stored in the genre tag, separated by commas.
An index should first be created with L<id3fs-index(1)>, then id3fsd
can mount the files in I<SOURCEDIR> on the directory I<MOUNTPOINT>.

If not explicitly specified (with B<-f>), the index is searched for
at I<SOURCEDIR>/B<.id3fs>.

The resulting filesystem is read-only. Tags appear as directories,
and files appear as symlinks to the actual files in I<SOURCEDIR>.

=head1 QUICKSTART

To mount a view of F<~/music> on F<~/tags>:

  $ mkdir ~/tags
  $ id3fs-index ~/music
  $ id3fsd ~/music ~/tags

You may need to be in the I<fuse> group, or be root.

=head1 OPTIONS

=over 4

=item I<SOURCEDIR>

Directory containing actual audio files and database file F<.id3fs>
(unless otherwise specified with B<-f>).

=item I<MOUNTPOINT>

Directory to mount the id3fs view of the files.

=item S<B<-t >I<NUM>> | S<B<--tagdepth=>I<NUM>>

Maximum number of tags in an expression. A query with many ANDs and
NOTs can get quite slow. This option allows a cutoff before things get
too slow, as well as providing some eventual limit when processes
recurse into the filesystem. The default is 6.

=item S<B<-f >I<FILE>> | S<B<--database=>I<FILE>>

Use database in I<FILE>. The default is I<SOURCEDIR>/B<.id3fs>.

=item B<-v>

Enable verbose operation. Repeat for more verbosity.  If verbose is
enabled, id3fsd does not detach from the terminal.

=item B<-h>

Show a short help message.

=item B<-->

End of options.

=back

=head1 FILESYSTEM LAYOUT

A path in the filesystem consists of a tag query expression, followed
by directories containing the matching files, arranged by artist and
album.

Example paths:

  /krautrock/AND/year/1971/
  /krautrock/AND/year/1971/Can
  /krautrock/AND/year/1971/Can/Tago Mago/01-Paperhouse.mp3
  /krautrock/AND/year/1971/Can/NOALBUM/Can - Oh Yeah (Live).mp3
  /krautrock/AND/year/1971/Can/TRACKS/01-Paperhouse.mp3
  /krautrock/AND/year/1971/Can/TRACKS/Can - Oh Yeah (Live).mp3
  /krautrock/AND/year/1971/NOARTIST/unknown-track.mp3
  /krautrock/AND/year/1971/TRACKS/01-Paperhouse.mp3

id3fs supports OR, AND and NOT. NOT has the highest precedence,
followed by AND, so F<foo/OR/NOT/bar/AND/baz> is parsed as
(foo OR ((NOT bar) AND baz)).

=head2 Tags

Tags are extracted from the B<genre> tag of audio files with
L<id3fs-index(1)>.

Within the genre frame/comment, tags are separated by commas.

Tags can have values, separated by a slash, eg I<metal/thrash>,
I<rating/5>. Certain special tags are automatically filled in from
other file metadata.

=head2 Special Tags

The following tags are automatically derived from other metadata in
the audio files:

=over 4

=item B<year>

Extracted from the B<year> or B<DATE> tag. If not found defaults to
B<year/UNKNOWN>.

=item B<decade>

Also extracted from the B<year> or B<DATE> tag. If not found
defaults to B<decade/UNKNOWN>.

=item B<v1genre>

If a mp3 file has an ID3V1.1 genre tag, its value is assigned to
v1genre.

=item B<audiotype>

Type of audio file (mp3, ogg, flac). Always set.

=back

=head2 Special Directories

=over 4

=item B<TRACKS>

All tracks that match the given tag expression, whether they have an
assigned artist and album or not.

=item B<NOARTIST>

Tracks matching the given expression that do not have an artist tag.

=item B<NOALBUM>

Tracks matching the given expression that do not have an album tag.

=item B<ALL>

This is a special directory in the root of the filesystem, that
provides access to all the indexed files, regardless of tags assigned.

=back

=head1 FUSE AND MOUNTING AUTOMATICALLY

For others to be able to view your id3fs mount(s), you need to set the
option B<user_allow_other> in F</etc/fuse.conf>.

To mount a filesystem automatically, put an entry in F</etc/fstab>
like:

  id3fsd#/source/dir    /mount/point    defaults   0   0

=head1 CAVEATS

Because id3fs offers a combinatorial explosion of views of your files,
processes recursing into the mount point will take a B<long> time to
traverse it.

Ensure your backups, cron jobs, F</etc/updatedb.conf>, etc. are
configured to exclude the mount point.

=head1 BUGS

Please report any found to ianb@erislabs.net

=head1 SEE ALSO

L<id3fs-index(1)>, L<http://fuse.sourceforge.net>

=head1 AUTHOR

Ian Beckwith <ianb@erislabs.net>

Many thanks to Aubrey Stark-Toller for help wrangling SQL.

=head1 AVAILABILITY

The latest version can be found at:

L<http://erislabs.net/ianb/projects/id3fs/>

=head1 COPYRIGHT

Copyright (C) 2010  Ian Beckwith <ianb@erislabs.net>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

=cut
