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