remove unnecessary code
[id3fs.git] / lib / ID3FS / Path.pm
1 # id3fs - a FUSE-based filesystem for browsing audio metadata
2 # Copyright (C) 2010  Ian Beckwith <ianb@erislabs.net>
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 package ID3FS::Path;
18
19 use strict;
20 use warnings;
21 use ID3FS::Path::Node;
22
23 our ($STATE_INVALID, $STATE_ROOT, $STATE_TAG, $STATE_TAGVAL,
24      $STATE_BOOLEAN, $STATE_ALBUMS, $STATE_TRACKLIST,
25      $STATE_FILE, $STATE_ALL)=(0..8);
26
27 our %priorities=( "OR" => 0, "AND" => 1, "NOT" => 2 );
28
29 our $PATH_ALLTRACKS= "TRACKS";
30 our $PATH_NOARTIST = "NOARTIST";
31 our $PATH_NOALBUM  = "NOALBUM";
32
33 our $ENABLE_FILTER = 1;
34
35 sub new
36 {
37     my $proto=shift;
38     my $class=ref($proto) || $proto;
39     my $self={};
40     bless($self,$class);
41
42     $self->{elements}=[];
43     $self->{db}=shift;
44     $self->{path}=shift;
45     $self->{verbose}=shift;
46     $self->{maxtagdepth}=shift;
47     $self->{curtagdepth}=0;
48     $self->{path} =~ s/\/\//\//g; # drop doubled slashes
49
50     $self->parse();
51 #    print "STATE: ", $self->state(), "\n";
52     return $self;
53 }
54
55 sub isdir
56 {
57     my($self)=@_;
58     if(($self->state() == $STATE_FILE) ||
59        ($self->state() == $STATE_INVALID))
60     {
61         return 0;
62     }
63     return 1;
64 }
65
66 sub isfile
67 {
68     my($self)=@_;
69     return($self->state() == $STATE_FILE);
70 }
71
72 sub isvalid
73 {
74     my($self)=@_;
75     return($self->state() != $STATE_INVALID);
76 }
77
78 sub dest
79 {
80     my($self, $mountpoint)=@_;
81     if($self->state() == $STATE_FILE)
82     {
83         return $self->filename($mountpoint);
84     }
85     return "ERROR"; #should never happen?
86 }
87
88 sub dirents
89 {
90     my($self)=@_;
91     my @dents=();
92     my @fents=();
93     my $state=$self->state();
94 #    print "DIRENTS: STATE: $state\n";
95 #    print "DIRENTS: FILE: $self->{path}\n";
96     if($state==$STATE_ALL)
97     {
98         @dents=($self->filter($PATH_ALLTRACKS, $PATH_NOARTIST), $self->artists());
99     }
100     elsif($state==$STATE_TAG || $state==$STATE_TAGVAL)
101     {
102         my $tail=$self->tail();
103         if($state==$STATE_TAG && $self->is($TYPE_TAG, $tail) &&
104            $self->{db}->tag_has_values($self->tail()->id()))
105         {
106             @dents=$self->tags();
107         }
108         else
109         {
110             if($self->{maxtagdepth} && ($self->{curtagdepth} < $self->{maxtagdepth}))
111             {
112                 @dents=qw(AND OR);
113             }
114             push(@dents, $self->filter($PATH_ALLTRACKS, $PATH_NOARTIST), $self->artists());
115         }
116     }
117     elsif($state==$STATE_BOOLEAN)
118     {
119         my $parent=$self->tail();
120         unless($self->is($TYPE_BOOL, $parent) &&
121                $parent->{name} eq "NOT")
122         {
123             @dents=("NOT");
124         }
125         push(@dents,$self->tags());
126     }
127     elsif($state==$STATE_ROOT)
128     {
129         @dents=(qw(ALL NOT), $self->tags());
130     }
131     elsif($state==$STATE_ALBUMS)
132     {
133         @dents=$self->filter($PATH_ALLTRACKS, $PATH_NOALBUM, $self->albums());
134     }
135     elsif($state==$STATE_TRACKLIST)
136     {
137         @fents=$self->tracks();
138     }
139     else
140     {
141         print "DIRENTS: UNHANDLED STATE: $state\n";
142     }
143     return(\@dents, \@fents);
144 }
145
146 # State Machine Of Doom
147 sub parse
148 {
149     my($self)=@_;
150     @{$self->{components}}=split(/\//, $self->{path});
151     shift @{$self->{components}}; # drop empty field before leading /
152 #    print "PATH: $self->{path}\n";
153     $self->state($STATE_ROOT);
154     return if($self->{path} eq "/");
155     my @parts=@{$self->{components}};
156     my($tag, $tagval);
157     $self->{elements}=[];
158     $self->{in_all}=0;
159     my $root_not=0;
160     my $tags_seen=0;
161     while(defined(my $name=shift @parts))
162     {
163 #       print "NAME: $name\n";
164         my $state=$self->state();
165         if($state==$STATE_INVALID)
166         {
167 #           print "SM: INVALID: $name\n";
168             return;
169         }
170         elsif($state==$STATE_ROOT)
171         {
172 #           print "SM: ROOT: $name\n";
173             if($name eq "ALL")
174             {
175                 $self->{in_all}=1;
176                 $self->state($STATE_ALL);
177             }
178             elsif($name eq "NOT")
179             {
180                 $root_not=1;
181                 push(@{$self->{elements}}, ID3FS::Path::Node->new($self->{db}, $TYPE_BOOL, $name));
182                 $self->state($STATE_BOOLEAN);
183             }
184             else
185             {
186                 $tag=ID3FS::Path::Node->new($self->{db}, $TYPE_TAG, $name);
187                 if($tag)
188                 {
189                     push(@{$self->{elements}}, $tag);
190                     $tags_seen++;
191                     $self->state($STATE_TAG);
192                 }
193                 else
194                 {
195                     $self->state($STATE_INVALID);
196                 }
197             }
198         }
199         elsif($state==$STATE_TAG || $state==$STATE_TAGVAL)
200         {
201             my $tag=$self->tail();
202 #           print "SM: TAG/TAGVAL($state): $name\n";
203             if($state==$STATE_TAG && $self->is($TYPE_TAG, $tag) &&
204                $self->{db}->tag_has_values($tag->id()))
205             {
206 #               print "Parsing: parent: $tag->id()\n";
207                 my $tagval=ID3FS::Path::Node->new($self->{db}, $TYPE_TAG, $name, $tag->id());
208                 if(defined($tagval))
209                 {
210                     $self->state($STATE_TAGVAL);
211                     # stay in tag state
212                     push(@{$self->{elements}}, $tagval);
213                 }
214                 else
215                 {
216                     $self->state($STATE_INVALID);
217                 }
218             }
219             elsif($name eq $PATH_ALLTRACKS)
220             {
221                 $self->state($STATE_TRACKLIST);
222             }
223             elsif($name eq $PATH_NOARTIST)
224             {
225                 $self->state($STATE_TRACKLIST);
226             }
227             elsif($name eq "AND")
228             {
229                 $self->state($STATE_BOOLEAN);
230                 push(@{$self->{elements}}, ID3FS::Path::Node->new($self->{db}, $TYPE_BOOL, $name));
231             }
232             elsif($name eq "OR")
233             {
234                 $self->state($STATE_BOOLEAN);
235                 push(@{$self->{elements}}, ID3FS::Path::Node->new($self->{db}, $TYPE_BOOL, $name));
236             }
237             else
238             {
239                 my $artist=ID3FS::Path::Node->new($self->{db}, $TYPE_ARTIST, $name);
240                 if($artist)
241                 {
242                     push(@{$self->{elements}}, $artist);
243                     $self->state($STATE_ALBUMS);
244                 }
245                 else
246                 {
247                     $self->state($STATE_INVALID);
248                 }
249             }
250         }
251         elsif($state==$STATE_BOOLEAN)
252         {
253 #           print "SM: BOOLEAN: $name\n";
254             my $parent=$self->tail();
255             my $allownot=1;
256             if($self->is($TYPE_BOOL, $parent) &&
257                $parent->{name} eq "NOT")
258             {
259                 $allownot=0;
260             }
261             if($allownot && $name eq "NOT")
262             {
263                 $self->state($STATE_BOOLEAN);
264                 push(@{$self->{elements}}, ID3FS::Path::Node->new($self->{db}, $TYPE_BOOL, $name));
265             }
266             else
267             {
268                 my $tag=ID3FS::Path::Node->new($self->{db}, $TYPE_TAG, $name);
269                 if($tag)
270                 {
271                     push(@{$self->{elements}}, $tag);
272                     $tags_seen++;
273                     $self->state($STATE_TAG);
274                 }
275                 else
276                 {
277                     $self->state($STATE_INVALID);
278                 }
279             }
280         }
281         elsif($state==$STATE_ALBUMS)
282         {
283 #           print "SM: ALBUM: $name\n";
284             if($name eq $PATH_ALLTRACKS)
285             {
286                 $self->state($STATE_TRACKLIST);
287             }
288             elsif($name eq $PATH_NOALBUM)
289             {
290                 $self->state($STATE_TRACKLIST);
291             }
292             else
293             {
294                 my $album=ID3FS::Path::Node->new($self->{db}, $TYPE_ALBUM, $name);
295                 if($album)
296                 {
297                     push(@{$self->{elements}}, $album);
298                     $self->state($STATE_TRACKLIST);
299                 }
300                 else
301                 {
302                     $self->state($STATE_INVALID);
303                 }
304             }
305         }
306         elsif($state==$STATE_TRACKLIST)
307         {
308 #           print "SM: TRACKLIST: $name\n";
309             my $track=ID3FS::Path::Node->new($self->{db}, $TYPE_FILE, $name);
310             if($track)
311             {
312                 push(@{$self->{elements}}, $track);
313                 $self->state($STATE_FILE);
314             }
315             else
316             {
317                 $self->state($STATE_INVALID);
318             }
319         }
320         elsif($state==$STATE_FILE)
321         {
322 #           print "SM: FILE: $name\n";
323             # Can't have anything after a filename
324             $self->state($STATE_INVALID);
325         }
326         elsif($state==$STATE_ALL)
327         {
328             if($name eq $PATH_ALLTRACKS)
329             {
330                 $self->state($STATE_TRACKLIST);
331             }
332             elsif($name eq $PATH_NOARTIST)
333             {
334                 $self->state($STATE_TRACKLIST);
335             }
336             else
337             {
338                 my $artist=ID3FS::Path::Node->new($self->{db}, $TYPE_ARTIST, $name);
339                 if($artist)
340                 {
341                     push(@{$self->{elements}}, $artist);
342                     $self->state($STATE_ALBUMS);
343                 }
344                 else
345                 {
346                     $self->state($STATE_INVALID);
347                 }
348             }
349         }
350         else
351         {
352             print "SM: ERROR: UNKNOWN STATE: $self->{state}\n";
353             $self->state($STATE_INVALID);
354         }
355     }
356
357     # remove trailing boolean
358     my @elements=@{$self->{elements}};
359     while(@elements && $self->is($TYPE_BOOL, $elements[$#elements]))
360     {
361         pop @elements;
362     }
363     # sort elements by precedence
364     @elements=$self->sort_elements(@elements);
365     $self->{tagtree}=$self->elements_to_tree(\@elements);
366     if($self->{tagtree})
367     {
368 #       use Data::Dumper;
369 #       print "TREE\n";
370 #       print Dumper $self->{tagtree};
371 #       my ($conditions, @joins)=$self->{tagtree}->to_sql();
372 #       print "CONDITIONS(", scalar(@joins), "): ", $conditions, "\n";
373     }
374 }
375
376 sub state
377 {
378     my($self, $newstate)=@_;
379     if(defined($newstate))
380     {
381         $self->{state}=$newstate;
382         $self->{curtagdepth}++ if($newstate == $STATE_TAG);
383     }
384     return $self->{state};
385 }
386
387 sub elements_to_tree
388 {
389     my($self, $elements)=@_;
390     return undef unless(@$elements);
391     my ($left, $right, $op)=(undef, undef, undef);
392     my $thing=pop @$elements;
393     if($self->is($TYPE_BOOL, $thing))
394     {
395         $right=$self->elements_to_tree($elements);
396         if($thing->{name} ne "NOT")
397         {
398             $left=$self->elements_to_tree($elements);
399         }
400         $thing->left($left);
401         $thing->right($right);
402     }
403     return $thing;
404 }
405
406 # Dijkstra's shunting-yard algorithm
407 sub sort_elements
408 {
409     my ($self, @input)=@_;
410     my @opstack=();
411     my @output=();
412 #    print "INPUT: ", join(', ', map { $_->{name}; } @input), "\n";
413     while(my $thing = shift @input)
414     {
415         if($self->is($TYPE_TAG, $thing))
416         {
417             # Handle tag values by dropping parent
418             if(@input && $self->is($TYPE_TAG, $input[0]))
419             {
420                 $thing=shift @input;
421             }
422             push(@output, $thing);
423         }
424         elsif($self->is($TYPE_BOOL, $thing))
425         {
426             # bool
427             while(@opstack &&
428                   ($priorities{$thing->{name}} <= $priorities{$opstack[$#opstack]->{name}}))
429             {
430                 push(@output, pop(@opstack));
431             }
432             push(@opstack, $thing);
433         }
434     }
435     while(@opstack)
436     {
437         push(@output, pop(@opstack));
438     }
439 #    print "STACK: ", join(', ', map { $_->{name}; } @output), "\n";
440     return @output;
441 }
442
443 sub used_tags
444 {
445     my($self)=@_;
446     return() unless(defined($self->{tagtree}));
447     return($self->{tagtree}->used_tags());
448 }
449
450 sub expecting_values
451 {
452     my($self)=@_;
453     my $tail=$self->tail();
454     if($self->is($TYPE_TAG, $tail))
455     {
456         return($self->{db}->tag_has_values($tail->id()));
457     }
458 }
459
460 sub trailing_tag_id
461 {
462     my($self)=@_;
463     my $tail=$self->tail();
464     if($self->is($TYPE_TAG, $tail))
465     {
466         return($tail->id());
467     }
468     return undef;
469 }
470
471 sub trailing_tag_parent
472 {
473     my($self)=@_;
474     my $tail=$self->tail();
475     if($self->is($TYPE_TAG, $tail))
476     {
477         return($tail->{parents_id});
478     }
479     return undef;
480 }
481
482 sub tail
483 {
484     my($self)=@_;
485     return($self->{elements}->[$#{$self->{elements}}]);
486 }
487
488 sub is
489 {
490     my($self, $type, $thing)=@_;
491     return 0 unless($thing);
492     return 0 unless($thing->type());
493     return 1 if($type == $thing->type());
494     return 0;
495 }
496
497 # the one before last
498 sub tail_parent
499 {
500     my($self)=@_;
501     return($self->{elements}->[($#{$self->{elements}}) - 1]);
502 }
503
504 ######################################################################
505
506 sub tags
507 {
508     my($self)=@_;
509     if(!$self->{tagtree}) # / or /NOT
510     {
511         my $sql="SELECT DISTINCT name FROM tags WHERE parents_id='';";
512         return($self->{db}->cmd_firstcol($sql));
513     }
514     my $hasvals=$self->expecting_values();
515     my $parent=$self->trailing_tag_parent();
516 #    print "THASVALS: $hasvals\n";
517 #    print "TPARENT: ", (defined($parent)? $parent : "NO"), "\n";
518     my @ids=();
519     my $sql="SELECT tags.name FROM ";
520     if($self->in_or())
521     {
522         $sql .= "files_x_tags\n";
523     }
524     else
525     {
526         $sql .= ("(\n" .
527                  $self->tags_subselect() .
528                  ") AS subselect\n" .
529                  "INNER JOIN files_x_tags ON subselect.files_id=files_x_tags.files_id\n");
530     }
531     $sql .= "INNER JOIN tags ON files_x_tags.tags_id=tags.id\n";
532     my @andclauses=();
533     my $id=$self->trailing_tag_id();
534
535     my $parentclause= "tags.parents_id='";
536     if($hasvals)
537     {
538         $parentclause .= $id;
539     }
540     elsif($parent)
541     {
542         $parentclause .= $parent;
543     }
544     $parentclause .= "'";
545     push(@andclauses, $parentclause);
546
547     my @used=$self->used_tags();
548     if(@used)
549     {
550         push(@andclauses, "tags.id NOT IN (" . join(', ', @used) . ")");
551     }
552     if(@andclauses)
553     {
554         $sql .= "WHERE " . join(' AND ', @andclauses) . "\n";
555     }
556
557     $sql .= "GROUP BY tags.name;";
558     print "SQL(TAGS): $sql\n" if($self->{verbose});
559     my @tagnames=$self->{db}->cmd_firstcol($sql);
560     print("SUBNAMES: ", join(', ', @tagnames), "\n") if($self->{verbose});
561     return(@tagnames);
562 }
563
564 sub artists
565 {
566     my($self)=@_;
567     if(!@{$self->{elements}}) # /ALL
568     {
569         my $sql="SELECT DISTINCT name FROM artists WHERE name!='';";
570         return($self->{db}->cmd_firstcol($sql));
571     }
572     my @ids=();
573     my $sql=$self->sql_start("artists.name");
574     $sql .= ("INNER JOIN artists ON files.artists_id=artists.id\n" .
575              "WHERE artists.name != ''\n" .
576              "GROUP BY artists.name;");
577     print "SQL(ARTISTS): $sql\n" if($self->{verbose});
578     my @tagnames=$self->{db}->cmd_firstcol($sql);
579     print("ARTISTS: ", join(', ', @tagnames), "\n") if($self->{verbose});
580     return(@tagnames);
581 }
582
583 sub albums
584 {
585     my($self)=@_;
586     my @ids=();
587     my $tail=$self->tail();
588     if($self->is($TYPE_ARTIST, $tail))
589     {
590         return $self->artist_albums($tail->id());
591     }
592     my $sql=$self->sql_start("albums.name");
593     $sql .= ("INNER JOIN albums ON files.albums_id=albums.id\n" .
594              "WHERE albums.name != ''\n" .
595              "GROUP BY albums.name;");
596     print "SQL(ALBUMS): \n$sql\n" if($self->{verbose});
597     my @names=$self->{db}->cmd_firstcol($sql);
598     print("ALBUMS: ", join(', ', @names), "\n") if($self->{verbose});
599     return(@names);
600 }
601
602 sub artist_albums
603 {
604     my($self, $artist_id)=@_;
605     my $sql=$self->sql_start("albums.name");
606     $sql .= ("INNER JOIN albums ON albums.id=files.albums_id\n" .
607              "INNER JOIN artists ON artists.id=files.artists_id\n" .
608              "WHERE artists.id=? and albums.name <> ''\n" .
609              "GROUP BY albums.name\n");
610     print "ARTIST_ALBUMS SQL: $sql\n" if($self->{verbose});
611     my @albums=$self->{db}->cmd_firstcol($sql, $artist_id);
612     print("ALBUMS: ", join(', ', @albums), "\n") if($self->{verbose});
613     return(@albums);
614 }
615
616 sub artist_tracks
617 {
618     my($self, $artist_id)=@_;
619     my $sql=$self->sql_start("files.name");
620     $sql .= ("INNER JOIN artists ON artists.id=files.artists_id\n" .
621              "INNER JOIN albums  ON albums.id=files.albums_id\n" .
622              "WHERE artists.id=? AND albums.name=''\n" .
623              "GROUP BY files.name\n");
624     print "ARTIST_TRACKS SQL: $sql\n" if($self->{verbose});
625     my @names=$self->{db}->cmd_firstcol($sql, $artist_id);
626     print("ARTISTTRACKS: ", join(', ', @names), "\n") if($self->{verbose});
627     return(@names);
628 }
629
630 sub album_tracks
631 {
632     my($self, $artist_id, $album_id)=@_;
633     my $sql=("SELECT files.name FROM files\n" .
634              "INNER JOIN albums  ON albums.id=files.albums_id\n" .
635              "INNER JOIN artists ON artists.id=files.artists_id\n" .
636              "WHERE artists.id=? AND albums.id=?\n" .
637              "GROUP BY files.name\n");
638     print "ALBUM_TRACKS SQL($artist_id, $album_id): $sql\n" if($self->{verbose});
639     my @names=$self->{db}->cmd_firstcol($sql, $artist_id, $album_id);
640     print("TRACKS: ", join(', ', @names), "\n") if($self->{verbose});
641     return(@names);
642 }
643
644 sub tracks
645 {
646     my($self)=@_;
647     my $tail=$self->tail();
648     if($self->is($TYPE_ARTIST, $tail))
649     {
650         return $self->artist_tracks($tail->id());
651     }
652     elsif($self->is($TYPE_ALBUM, $tail))
653     {
654         my $artist_id=0;
655         my $artist=$self->tail_parent();
656         if($self->is($TYPE_ARTIST, $artist))
657         {
658             # should always happen
659             $artist_id=$artist->id();
660         }
661         return $self->album_tracks($artist_id, $tail->id());
662     }
663     my $sql=$self->sql_start("files.name");
664     $sql .= "INNER JOIN artists ON files.artists_id=artists.id\n";
665     if($self->{components}->[$#{$self->{components}}] eq $PATH_NOARTIST)
666     {
667         $sql .= "WHERE artists.name =''\n";
668     }
669     $sql .= "GROUP BY files.name;";
670     print "TRACKS SQL($self->{path}): $sql\n" if($self->{verbose});
671     my @names=$self->{db}->cmd_firstcol($sql);
672     print("TRACKS: ", join(', ', @names), "\n") if($self->{verbose});
673     return(@names);
674 }
675
676 sub filename
677 {
678     my($self, $mountpoint)=@_;
679     my $tail=$self->tail();
680     if($self->is($TYPE_FILE, $tail))
681     {
682         my $id=$tail->id();
683         my $sql=("SELECT paths.name, files.name FROM files\n" .
684                  "INNER JOIN paths ON files.paths_id=paths.id\n" .
685                  "WHERE files.id=?\n" .
686                  "GROUP BY paths.name, files.name");
687         print "FILENAME SQL: $sql\n" if($self->{verbose});
688         my ($path, $name)=$self->{db}->cmd_onerow($sql, $id);
689         my $id3fs_path=join('/', map { $_->{name}; }  @{$self->{elements}});
690         return($self->{db}->relativise($path, $name, $mountpoint, $self->{path}));
691     }
692     # should never happen
693     return "ERROR";
694 }
695
696 sub tags_subselect
697 {
698     my($self)=@_;
699     my $hasvals=$self->expecting_values();
700     my $tree=$self->{tagtree};
701     my $parent=$self->trailing_tag_parent();
702
703     my $tag=undef;
704     if($hasvals)
705     {
706         $tag=$self->trailing_tag_id();
707 #       print "Trailing id: $tag\n";
708     }
709     my ($sqlclause, @joins)=(undef, ());
710     ($sqlclause, @joins) = $tree->to_sql($hasvals) if($tree);
711 #    print "SQL(" . scalar(@joins) ."): $sqlclause\n";
712     my $sql="\tSELECT fxt1.files_id FROM tags t1";
713     my @crosses=();
714     my @inners=();
715 #    $joinsneeded++ if($tag);
716     for(my $i=0; $i <= $#joins; $i++)
717     {
718         my $cnt=$i+1;
719         my $join=$joins[$i];
720         my $inner=("\t$join JOIN files_x_tags fxt$cnt ON " .
721                    "t${cnt}.id=fxt${cnt}.tags_id");
722         if($i > 0)
723         {
724             push(@crosses, "CROSS JOIN tags t$cnt");
725             $inner .= " AND fxt1.files_id=fxt${cnt}.files_id";
726         }
727         push(@inners, $inner);
728     }
729     $sql .= ("\n\t" . join(" ", @crosses)) if(@crosses);
730     $sql .= ("\n" . join("\n", @inners)) if(@inners);
731     $sql .= "\n\tWHERE $sqlclause" if($sqlclause);
732 #    if($tag)
733 #    {
734 #       $sql .= " AND t${joinsneeded}.parents_id='$tag'";
735 #    }
736     $sql .= "\n\tGROUP BY fxt1.files_id\n";
737     return $sql;
738 }
739
740 sub sql_start
741 {
742     my($self, $tables)=@_;
743     my $sql="SELECT $tables FROM ";
744     if($self->{in_all})
745     {
746         $sql .= "files\n";
747     }
748     else
749     {
750         $sql .= ("(\n" .
751                  $self->tags_subselect() .
752                  ") AS subselect\n" .
753                  "INNER JOIN files ON subselect.files_id=files.id\n");
754     }
755     return $sql;
756 }
757
758 # we just filter $ALLTRACKS, $NOARTIST and $NOALBUM
759 # filtering tags properly requires up to four levels of recursion
760 # (tag/tagval/AND/NOT) and is too slow
761 sub filter
762 {
763     my($self, @dirs)=@_;
764     return(@dirs) unless($ENABLE_FILTER);
765     my $base=$self->{path};
766     my @outdirs=();
767     for my $dir (@dirs)
768     {
769 #       print "\nFILTER (",$self->state(), "): $base / $dir\n";
770         if($self->empty("$base/$dir"))
771         {
772 #           print "empty: $base / $dir\n";
773         }
774         else
775         {
776 #           print "non-empty, accepting: $base / $dir\n";
777             push(@outdirs, $dir);
778         }
779     }
780     return(@outdirs);
781 }
782
783 sub empty
784 {
785     my($self, $dir)=@_;
786     my $path=ID3FS::Path->new($self->{db}, $dir, $self->{verbose},
787                               ($self->{maxtagdepth} - $self->{curtagdepth}));
788     return 1 unless($path->isvalid());
789     my($subdirs,$subfiles)=$path->dirents();
790     return 0 if(@$subfiles || @$subdirs);
791     return 1;
792 }
793
794 # if path is .../OR/ or .../OR/NOT
795 sub in_or
796 {
797     my($self)=@_;
798     my $tail=$self->tail();
799     return 0 unless($tail);
800     return 0 unless($tail->type() == $TYPE_BOOL);
801     return 1 if($tail->name() eq "OR");
802     return 0 unless($tail->name() eq "NOT");
803     my $parent=$self->tail_parent();
804     return 0 unless($parent);
805     return 0 unless($parent->type() == $TYPE_BOOL);
806     return 1 if($parent->name() eq "OR");
807     return 0;
808 }
809
810 1;