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