remove obsolete FIXMEs
[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                 $self->state($STATE_TRACKLIST);
330             }
331             else
332             {
333                 my $artist=ID3FS::PathElement::Artist->new($self->{db}, $name);
334                 if($artist)
335                 {
336                     push(@{$self->{elements}}, $artist);
337                     $self->state($STATE_ALBUMS);
338                 }
339                 else
340                 {
341                     $self->state($STATE_INVALID);
342                 }
343             }
344         }
345         else
346         {
347             print "SM: ERROR: UNKNOWN STATE: $self->{state}\n";
348             $self->state($STATE_INVALID);
349         }
350     }
351
352     if($root_not && ($tags_seen < 2))
353     {
354         $self->{bare_not}=1;
355     }
356
357     # remove trailing boolean
358     my @elements=@{$self->{elements}};
359     while(@elements && ref($elements[$#elements]) eq "ID3FS::PathElement::Boolean")
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         ($self->{sqlconditions},
369          $self->{joins}) = $self->{tagtree}->to_sql();
370 #       print "TREE: ",  $self->{tagtree}->print(), "\n";
371 #       print("SQL CONDITION(", scalar(@{$self->{joins}}), "): ",
372 #             $self->{sqlconditions}, "\n");
373 #       use Data::Dumper;
374 #       print Dumper $self->{tagtree};
375     }
376 }
377
378 sub state
379 {
380     my($self, $newstate)=@_;
381     if(defined($newstate))
382     {
383         $self->{state}=$newstate;
384         $self->{curtagdepth}++ if($newstate == $STATE_TAG);
385     }
386     return $self->{state};
387 }
388
389 sub elements_to_tree
390 {
391     my($self, $elements)=@_;
392     return undef unless(@$elements);
393     my ($left, $right, $op)=(undef, undef, undef);
394     my $thing=pop @$elements;
395     if(ref($thing) eq "ID3FS::PathElement::Boolean")
396     {
397         my $op=$thing;
398         $right=$self->elements_to_tree($elements);
399         if($op->{name} ne "NOT")
400         {
401             $left=$self->elements_to_tree($elements);
402         }
403         return ID3FS::Path::Node->new($left, $op, $right);
404     }
405     else
406     {
407         return ID3FS::Path::Node->new($thing);
408     }
409 }
410
411 # Dijkstra's shunting-yard algorithm
412 sub sort_elements
413 {
414     my ($self, @input)=@_;
415     my @opstack=();
416     my @output=();
417 #    print "INPUT: ", join(', ', map { $_->{name}; } @input), "\n";
418     while(my $thing = shift @input)
419     {
420         if(ref($thing) eq "ID3FS::PathElement::Tag")
421         {
422             # Handle tag values by dropping parent
423             if(@input && ref($input[0]) eq "ID3FS::PathElement::Tag")
424             {
425                 $thing=shift @input;
426             }
427             push(@output, $thing);
428         }
429         elsif(ref($thing) eq "ID3FS::PathElement::Boolean")
430         {
431             # bool
432             while(@opstack &&
433                   ($priorities{$thing->{name}} <= $priorities{$opstack[$#opstack]->{name}}))
434             {
435                 push(@output, pop(@opstack));
436             }
437             push(@opstack, $thing);
438         }
439     }
440     while(@opstack)
441     {
442         push(@output, pop(@opstack));
443     }
444 #    print "STACK: ", join(', ', map { $_->{name}; } @output), "\n";
445     return @output;
446 }
447
448 sub used_tags
449 {
450     my($self)=@_;
451     return() unless(defined($self->{tagtree}));
452     return($self->{tagtree}->used_tags());
453 }
454
455 sub expecting_values
456 {
457     my($self)=@_;
458     my $tail=$self->tail();
459     if($tail && ref($tail) eq "ID3FS::PathElement::Tag")
460     {
461         return($self->{db}->tag_has_values($tail->{id}));
462     }
463 }
464
465 sub trailing_tag_id
466 {
467     my($self)=@_;
468     my $tail=$self->tail();
469     if($tail && ref($tail) eq "ID3FS::PathElement::Tag")
470     {
471         return($tail->{id});
472     }
473     return undef;
474 }
475
476 sub trailing_tag_parent
477 {
478     my($self)=@_;
479     my $tail=$self->tail();
480     if($tail && ref($tail) eq "ID3FS::PathElement::Tag")
481     {
482         return($tail->{parents_id});
483     }
484     return undef;
485 }
486
487 sub tail
488 {
489     my($self)=@_;
490     return($self->{elements}->[$#{$self->{elements}}]);
491 }
492
493 # the one before last
494 sub tail_parent
495 {
496     my($self)=@_;
497     return($self->{elements}->[($#{$self->{elements}}) - 1]);
498 }
499
500 ######################################################################
501
502 sub tags
503 {
504     my($self)=@_;
505     if(!$self->{tagtree}) # / or /NOT
506     {
507         my $sql="SELECT DISTINCT name FROM tags WHERE parents_id='';";
508         return($self->{db}->cmd_firstcol($sql));
509     }
510     my $hasvals=$self->expecting_values();
511     my $parent=$self->trailing_tag_parent();
512 #    print "THASVALS: $hasvals\n";
513 #    print "TPARENT: ", (defined($parent)? $parent : "NO"), "\n";
514     my @ids=();
515     my $sql=("SELECT tags.name FROM (\n" .
516              $self->tags_subselect() .
517              ") AS subselect\n" .
518              "INNER JOIN files_x_tags ON subselect.files_id=files_x_tags.files_id\n" .
519              "INNER JOIN tags ON files_x_tags.tags_id=tags.id\n");
520     my @allused=$self->used_tags();
521     my @used=grep { ref($_) ne "ARRAY"; } @allused;
522     my @used_with_vals=grep { ref($_) eq "ARRAY"; } @allused;
523 #    print "tags(): USED: ", join(", ", @used), "\n";
524 #    print "tags(): USED_WITH_VALS: ", join(", ", map { "[".$_->[0]. ", ".$_->[1]."]";} @used_with_vals), "\n";
525     my @orclauses=();
526     my @andclauses=();
527     my $id=$self->trailing_tag_id();
528     if($hasvals)
529     {
530 #       print "HAS_VALUES\n";
531         my @values=map { "'".$_->[1]."'"; } grep { $_->[0] == $id; } @used_with_vals;
532         my $clause="(tags.parents_id='$id'";
533         if(@values)
534         {
535             $clause .= " AND tags.id NOT IN (" . join(', ', @values) . ")";
536         }
537         $clause .= ")";
538         push(@andclauses, $clause);
539     }
540     else
541     {
542 #       print "HASNT VALUES\n";;
543         if(@used)
544         {
545             push(@andclauses, "(NOT (tags.parents_id='' AND tags.id IN (" . join(', ', @used) . ")))");
546         }
547         for my $pair (@used_with_vals)
548         {
549             push(@andclauses, "(NOT (tags.parents_id='" . $pair->[0] . "' AND tags.id='" . $pair->[1] . "'))");
550         }
551     }
552
553     my $parentclause= "(tags.parents_id='";
554     if($hasvals)
555     {
556         $parentclause .= $id;
557     }
558     elsif($parent)
559     {
560         $parentclause .= $parent;
561     }
562     $parentclause .= "')";
563     push(@andclauses, $parentclause);
564
565     if(@orclauses)
566     {
567         push(@andclauses, '( ' . join(' OR ', @orclauses) . ' )');
568     }
569     if(@andclauses)
570     {
571         $sql .= "WHERE " . join(' AND ', @andclauses) . "\n";
572     }
573     $sql .= "GROUP BY tags.name;";
574     print "SQL(TAGS): $sql\n" if($self->{verbose});
575     my @tagnames=$self->{db}->cmd_firstcol($sql);
576     print("SUBNAMES: ", join(', ', @tagnames), "\n") if($self->{verbose});
577     return(@tagnames);
578 }
579
580 sub artists
581 {
582     my($self)=@_;
583     if(!@{$self->{elements}}) # /ALL
584     {
585         my $sql="SELECT DISTINCT name FROM artists WHERE name!='';";
586         return($self->{db}->cmd_firstcol($sql));
587     }
588     my @ids=();
589     my $sql=$self->sql_start("artists.name");
590     $sql .= ("INNER JOIN artists ON files.artists_id=artists.id\n" .
591              "WHERE artists.name != ''\n" .
592              "GROUP BY artists.name;");
593     print "SQL(ARTISTS): $sql\n" if($self->{verbose});
594     my @tagnames=$self->{db}->cmd_firstcol($sql);
595     print("ARTISTS: ", join(', ', @tagnames), "\n") if($self->{verbose});
596     return(@tagnames);
597 }
598
599 sub albums
600 {
601     my($self)=@_;
602     my @ids=();
603     my $tail=$self->tail();
604     # FIXME: rework PathElements
605     if(ref($tail) eq "ID3FS::PathElement::Artist")
606     {
607         return $self->artist_albums($tail->{id});
608     }
609     my $sql=$self->sql_start("albums.name");
610     $sql .= ("INNER JOIN albums ON files.albums_id=albums.id\n" .
611              "WHERE albums.name != ''\n" .
612              "GROUP BY albums.name;");
613     print "SQL(ALBUMS): \n$sql\n" if($self->{verbose});
614     my @names=$self->{db}->cmd_firstcol($sql);
615     print("ALBUMS: ", join(', ', @names), "\n") if($self->{verbose});
616     return(@names);
617 }
618
619 sub artist_albums
620 {
621     my($self, $artist_id)=@_;
622     my $sql=$self->sql_start("albums.name");
623     $sql .= ("INNER JOIN albums ON albums.id=files.albums_id\n" .
624              "INNER JOIN artists ON artists.id=files.artists_id\n" .
625              "WHERE artists.id=? and albums.name <> ''\n" .
626              "GROUP BY albums.name\n");
627     print "ARTIST_ALBUMS SQL: $sql\n" if($self->{verbose});
628     my @albums=$self->{db}->cmd_firstcol($sql, $artist_id);
629     print("ALBUMS: ", join(', ', @albums), "\n") if($self->{verbose});
630     return(@albums);
631 }
632
633 sub artist_tracks
634 {
635     my($self, $artist_id)=@_;
636     my $sql=$self->sql_start("files.name");
637     $sql .= ("INNER JOIN artists ON artists.id=files.artists_id\n" .
638              "INNER JOIN albums  ON albums.id=files.albums_id\n" .
639              "WHERE artists.id=? AND albums.name=''\n" .
640              "GROUP BY files.name\n");
641     print "ARTIST_TRACKS SQL: $sql\n" if($self->{verbose});
642     my @names=$self->{db}->cmd_firstcol($sql, $artist_id);
643     print("ARTISTTRACKS: ", join(', ', @names), "\n") if($self->{verbose});
644     return(@names);
645 }
646
647 sub album_tracks
648 {
649     my($self, $artist_id, $album_id)=@_;
650     my $sql=("SELECT files.name FROM files\n" .
651              "INNER JOIN albums  ON albums.id=files.albums_id\n" .
652              "INNER JOIN artists ON artists.id=files.artists_id\n" .
653              "WHERE artists.id=? AND albums.id=?\n" .
654              "GROUP BY files.name\n");
655     print "ALBUM_TRACKS SQL($artist_id, $album_id): $sql\n" if($self->{verbose});
656     my @names=$self->{db}->cmd_firstcol($sql, $artist_id, $album_id);
657     print("TRACKS: ", join(', ', @names), "\n") if($self->{verbose});
658     return(@names);
659 }
660
661 sub tracks
662 {
663     my($self)=@_;
664     # FIXME: rework PathElements
665     my $tail=$self->tail();
666     if(ref($tail) eq "ID3FS::PathElement::Artist")
667     {
668         return $self->artist_tracks($tail->{id});
669     }
670     elsif(ref($tail) eq "ID3FS::PathElement::Album")
671     {
672         my $artist_id=0;
673         my $artist=$self->tail_parent();
674         if(defined($artist) && (ref($artist) eq "ID3FS::PathElement::Artist"))
675         {
676             # should always happen
677             $artist_id=$artist->{id};
678         }
679         return $self->album_tracks($artist_id, $tail->{id});
680     }
681     my $sql=$self->sql_start("files.name");
682     $sql .= "INNER JOIN artists ON files.artists_id=artists.id\n";
683     if($self->{components}->[$#{$self->{components}}] eq $PATH_NOARTIST)
684     {
685         $sql .= "WHERE artists.name =''\n";
686     }
687     $sql .= "GROUP BY files.name;";
688     print "TRACKS SQL($self->{path}): $sql\n" if($self->{verbose});
689     my @names=$self->{db}->cmd_firstcol($sql);
690     print("TRACKS: ", join(', ', @names), "\n") if($self->{verbose});
691     return(@names);
692 }
693
694 sub filename
695 {
696     my($self, $mountpoint)=@_;
697     my $tail=$self->tail();
698     if(ref($tail) eq "ID3FS::PathElement::File")
699     {
700         my $id=$tail->{id};
701         my $sql=("SELECT paths.name, files.name FROM files\n" .
702                  "INNER JOIN paths ON files.paths_id=paths.id\n" .
703                  "WHERE files.id=?\n" .
704                  "GROUP BY paths.name, files.name");
705         print "FILENAME SQL: $sql\n" if($self->{verbose});
706         my ($path, $name)=$self->{db}->cmd_onerow($sql, $id);
707         my $id3fs_path=join('/', map { $_->{name}; }  @{$self->{elements}});
708         return($self->{db}->relativise($path, $name, $mountpoint));
709     }
710     # should never happen
711     return "ERROR";
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     @tags=map{ "\"$_\""; } @tags;
840     @tags_vals=map( { [ map({ "\"$_\""; } @$_ ) ] } @tags_vals);
841     $lasttag="\"$lasttag\"" if defined($lasttag);
842     return(\@tags, \@tags_vals, $lasttag);
843 }
844
845 # we just filter $ALLTRACKS, $NOARTIST and $NOALBUM
846 # filtering tags properly requires up to four levels of recursion
847 # (tag/tagval/AND/NOT) and is too slow
848 sub filter
849 {
850     my($self, @dirs)=@_;
851     my $base=$self->{path};
852     my @outdirs=();
853     for my $dir (@dirs)
854     {
855         print "\nFILTER (",$self->state(), "): $base / $dir\n";
856         if($self->empty("$base/$dir"))
857         {
858             print "empty: $base / $dir\n";
859         }
860         else
861         {
862             print "non-empty, accepting: $base / $dir\n";
863             push(@outdirs, $dir);
864         }
865     }
866     return(@outdirs);
867 }
868
869 sub empty
870 {
871     my($self, $dir)=@_;
872     my $path=ID3FS::Path->new($self->{db}, $dir, $self->{verbose},
873                               ($self->{maxtagdepth} - $self->{curtagdepth}));
874     return 1 unless($path->isvalid());
875     my($subdirs,$subfiles)=$path->dirents();
876     return 0 if(@$subfiles || @$subdirs);
877     return 1;
878 }
879
880 1;