specify max tag depth (default: 15)
[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, $PATH_ALLTRACKS, $PATH_NOARTIST, $self->artists());
105         }
106     }
107     elsif($state==$STATE_BOOLEAN)
108     {
109         my $parent=$self->tail();
110         unless(defined($parent) &&
111                ref($parent) eq "ID3FS::PathElement::Boolean" &&
112                $parent->{name} eq "NOT")
113         {
114             @dents=("NOT");
115         }
116         push(@dents,$self->tags());
117     }
118     elsif($state==$STATE_ROOT)
119     {
120         @dents=(qw(ALL NOT), $self->tags());
121     }
122     elsif($state==$STATE_ALBUMS)
123     {
124         @dents=($PATH_ALLTRACKS, $PATH_NOALBUM, $self->albums());
125     }
126     elsif($state==$STATE_TRACKLIST)
127     {
128         @fents=$self->tracks();
129     }
130     else
131     {
132         print "DIRENTS: UNHANDLED STATE: $state\n";
133     }
134     return(\@dents, \@fents);
135 }
136
137 sub parse
138 {
139     my($self)=@_;
140     @{$self->{components}}=split(/\//, $self->{path});
141     shift @{$self->{components}}; # drop empty field before leading /
142 #    print "PATH: $self->{path}\n";
143     $self->state($STATE_ROOT);
144     return if($self->{path} eq "/");
145     my @parts=@{$self->{components}};
146     my($tag, $tagval);
147     $self->{elements}=[];
148     $self->{bare_not}=0;
149     $self->{in_all}=0;
150     my $root_not=0;
151     my $tags_seen=0;
152     while(defined(my $name=shift @parts))
153     {
154 #       print "NAME: $name\n";
155         my $state=$self->state();
156         if($state==$STATE_INVALID)
157         {
158 #           print "SM: INVALID: $name\n";
159             return;
160         }
161         elsif($state==$STATE_ROOT)
162         {
163 #           print "SM: ROOT: $name\n";
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::PathElement::Boolean->new($self->{db}, $name));
173                 $self->state($STATE_BOOLEAN);
174             }
175             else
176             {
177                 $tag=ID3FS::PathElement::Tag->new($self->{db}, $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 || $state==$STATE_TAGVAL)
191         {
192 #           print "SM: TAG/TAGVAL($state): $name\n";
193             my $tag=$self->tail();
194             if($state==$STATE_TAG &&
195                defined($tag) &&
196                ref($tag) eq "ID3FS::PathElement::Tag" &&
197                $self->{db}->tag_has_values($tag->{id}))
198             {
199 #               print "Parsing: parent: $tag->{id}\n";
200                 my $tagval=ID3FS::PathElement::Tag->new($self->{db}, $name, $tag->{id});
201                 if(defined($tagval))
202                 {
203                     $self->state($STATE_TAGVAL);
204                     # stay in tag state
205                     push(@{$self->{elements}}, $tagval);
206                 }
207                 else
208                 {
209                     $self->state($STATE_INVALID);
210                 }
211             }
212             elsif($name eq $PATH_ALLTRACKS)
213             {
214                 $self->state($STATE_TRACKLIST);
215             }
216             elsif($name eq $PATH_NOARTIST)
217             {
218                 $self->state($STATE_TRACKLIST);
219             }
220             elsif($name eq "AND")
221             {
222                 $self->state($STATE_BOOLEAN);
223                 push(@{$self->{elements}}, ID3FS::PathElement::Boolean->new($self->{db}, $name));
224             }
225             elsif($name eq "OR")
226             {
227                 $self->state($STATE_BOOLEAN);
228                 push(@{$self->{elements}}, ID3FS::PathElement::Boolean->new($self->{db}, $name));
229             }
230             else
231             {
232                 my $artist=ID3FS::PathElement::Artist->new($self->{db}, $name);
233                 if($artist)
234                 {
235                     push(@{$self->{elements}}, $artist);
236                     $self->state($STATE_ALBUMS);
237                 }
238                 else
239                 {
240                     $self->state($STATE_INVALID);
241                 }
242             }
243         }
244         elsif($state==$STATE_BOOLEAN)
245         {
246 #           print "SM: BOOLEAN: $name\n";
247             my $parent=$self->tail();
248             my $allownot=1;
249             if(defined($parent) &&
250                ref($parent) eq "ID3FS::PathElement::Boolean" &&
251                $parent->{name} eq "NOT")
252             {
253                 $allownot=0;
254             }
255             if($allownot && $name eq "NOT")
256             {
257                 $self->state($STATE_BOOLEAN);
258                 push(@{$self->{elements}}, ID3FS::PathElement::Boolean->new($self->{db}, $name));
259             }
260             else
261             {
262                 my $tag=ID3FS::PathElement::Tag->new($self->{db}, $name);
263                 if($tag)
264                 {
265                     push(@{$self->{elements}}, $tag);
266                     $tags_seen++;
267                     $self->state($STATE_TAG);
268                 }
269                 else
270                 {
271                     $self->state($STATE_INVALID);
272                 }
273             }
274         }
275         elsif($state==$STATE_ALBUMS)
276         {
277 #           print "SM: ALBUM: $name\n";
278             if($name eq $PATH_ALLTRACKS)
279             {
280                 $self->state($STATE_TRACKLIST);
281             }
282             elsif($name eq $PATH_NOALBUM)
283             {
284                 $self->state($STATE_TRACKLIST);
285             }
286             else
287             {
288                 my $album=ID3FS::PathElement::Album->new($self->{db}, $name);
289                 if($album)
290                 {
291                     push(@{$self->{elements}}, $album);
292                     $self->state($STATE_TRACKLIST);
293                 }
294                 else
295                 {
296                     $self->state($STATE_INVALID);
297                 }
298             }
299         }
300         elsif($state==$STATE_TRACKLIST)
301         {
302 #           print "SM: TRACKLIST: $name\n";
303             my $track=ID3FS::PathElement::File->new($self->{db}, $name);
304             if($track)
305             {
306                 push(@{$self->{elements}}, $track);
307                 $self->state($STATE_FILE);
308             }
309             else
310             {
311                 $self->state($STATE_INVALID);
312             }
313         }
314         elsif($state==$STATE_FILE)
315         {
316 #           print "SM: FILE: $name\n";
317             # Can't have anything after a filename
318             $self->state($STATE_INVALID);
319         }
320         elsif($state==$STATE_ALL)
321         {
322             if($name eq $PATH_ALLTRACKS)
323             {
324                 $self->state($STATE_TRACKLIST);
325             }
326             elsif($name eq $PATH_NOARTIST)
327             {
328                 # FIXME
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     die("DB::filename: unhandled case\n"); #FIXME
711 }
712
713 sub tags_subselect
714 {
715     my($self)=@_;
716     my $hasvals=$self->expecting_values();
717     # we need to specially handle a bare /NOT/tag with no other clauses,
718     # using a simple WHERE id !='tagid' instead of a LEFT JOIN
719     if($self->{bare_not})
720     {
721         return $self->bare_not_subselect();
722     }
723     if($self->{in_all})
724     {
725         return "\tSELECT id FROM files AS files_id\n";
726     }
727     my $tree=$self->{tagtree};
728     my $parent=$self->trailing_tag_parent();
729
730 #    print "ELEMENTS: ", join('/', map { $_->{name}; } @{$self->{elements}}), "\n";
731 #    print "TREE: ", $tree->print(), "\n";
732     my $tag=undef;
733     if($hasvals)
734     {
735         $tag=$self->trailing_tag_id();
736 #       print "Trailing id: $tag\n";
737     }
738     my ($sqlclause, @joins)=(undef, ());
739     ($sqlclause, @joins) = $tree->to_sql($hasvals) if($tree);
740 #    print "SQL(" . scalar(@joins) .": $sqlclause\n";
741     my $sql="\tSELECT fxt1.files_id FROM tags t1";
742     my @crosses=();
743     my @inners=();
744 #    $joinsneeded++ if($tag);
745     for(my $i=0; $i <= $#joins; $i++)
746     {
747         my $cnt=$i+1;
748         my $join=$joins[$i];
749         my $inner=("\t$join JOIN files_x_tags fxt$cnt ON " .
750                    "t${cnt}.id=fxt${cnt}.tags_id");
751         if($i > 0)
752         {
753             push(@crosses, "CROSS JOIN tags t$cnt");
754             $inner .= " AND fxt1.files_id=fxt${cnt}.files_id";
755         }
756         push(@inners, $inner);
757     }
758     $sql .= ("\n\t" . join(" ", @crosses)) if(@crosses);
759     $sql .= ("\n" . join("\n", @inners)) if(@inners);
760     $sql .= "\n\tWHERE $sqlclause" if($sqlclause);
761 #    if($tag)
762 #    {
763 #       $sql .= " AND t${joinsneeded}.parents_id='$tag'";
764 #    }
765     $sql .= "\n\tGROUP BY fxt1.files_id\n";
766     return $sql;
767 }
768
769 sub bare_not_subselect
770 {
771     my($self)=@_;
772     my @tags=grep { ref($_) eq "ID3FS::PathElement::Tag"; } @{$self->{elements}};
773     my $sql=("\tSELECT f1.id AS files_id FROM files f1 WHERE f1.id NOT IN (\n" .
774              "\t\tSELECT fxt1.files_id FROM tags t1\n" .
775              "\t\tINNER JOIN files_x_tags fxt1 ON t1.id=fxt1.tags_id\n" .
776              "\t\tWHERE ");
777     if(scalar(@tags) > 1)
778     {
779         $sql .= ("(t1.parents_id='" . $tags[0]->{id} . "' AND t1.id='" .
780                  $tags[1]->{id} . "')");
781     }
782     else
783     {
784         $sql .= ("(t1.parents_id='' AND t1.id='" . $tags[0]->{id} . "')");
785     }
786     $sql .= "\n\t\tGROUP BY fxt1.files_id\n\t)\n";
787     return($sql);
788 }
789
790 sub sql_start
791 {
792     my($self, $tables)=@_;
793     my $sql="SELECT $tables FROM ";
794     if($self->{in_all})
795     {
796         $sql .= "files\n";
797     }
798     else
799     {
800         $sql .= ("(\n" .
801                  $self->tags_subselect() .
802                  ") AS subselect\n" .
803                  "INNER JOIN files ON subselect.files_id=files.id\n");
804     }
805     return $sql;
806 }
807
808
809 sub constraints_tag_list
810 {
811     my($self, @constraints)=@_;
812     my $lasttag=undef;
813     my @tags=();
814     my @tags_vals=();
815     for my $constraint (@constraints)
816     {
817 #       print ref($constraint), ": ", $constraint->{name}, "\n";
818         if(ref($constraint) eq "ID3FS::PathElement::Tag")
819         {
820             if(defined($lasttag))
821             {
822 #               print "TAGVAL\n";
823                 push(@tags_vals, [$lasttag, $constraint->{id}]) if defined($constraint->{id});
824                 $lasttag=undef;
825             }
826             elsif($self->tag_has_values($constraint->{id}))
827             {
828 #               print "HASVALUES\n";
829                 $lasttag=$constraint->{id} if defined($constraint->{id});
830             }
831             else
832             {
833 #               print "NOVALUES\n";
834                 push(@tags, $constraint->{id}) if(defined($constraint->{id}));
835             }
836         }
837     }
838     unless($self->{db}->{postgres})
839     {
840         @tags=map{ "\"$_\""; } @tags;
841         @tags_vals=map( { [ map({ "\"$_\""; } @$_ ) ] } @tags_vals);
842         $lasttag="\"$lasttag\"" if defined($lasttag);
843     }
844     return(\@tags, \@tags_vals, $lasttag);
845 }
846
847 # Not used, slows things down too much
848 sub filter
849 {
850     my($self, @dirs)=@_;
851     my $base=$self->{path};
852     my @outdirs=();
853     # depth 4 to allow for tag/tagval/AND/NOT
854     my $maxdepth=4;
855     for my $dir (@dirs)
856     {
857 #       print "\nFILTER (",$self->state(), "): $base / $dir\n";
858         if($self->empty("$base/$dir", $maxdepth))
859         {
860 #           print "empty: $base / $dir\n";
861         }
862         else
863         {
864 #           print "non-empty, accepting: $base / $dir\n";
865             push(@outdirs, $dir);
866         }
867     }
868     return(@outdirs);
869 }
870
871 sub empty
872 {
873     my($self, $dir, $maxdepth)=@_;
874     return 0 unless($maxdepth);
875 #    print "testing($maxdepth): $dir\n";
876     my $path=ID3FS::Path->new($self->{db}, $dir, $self->{verbose}, $self->{tagdepth});
877 #    print "PATH INVALID\n" unless($path->isvalid());
878     return 1 unless($path->isvalid());
879     my($subdirs,$subfiles)=$path->dirents();
880 #    print "SUBDENTS: ", join(", ", @$subdirs, @$subfiles), "\n";
881 #    print("SUBFILES: ", join(', ', @$subfiles), "\n") if(@$subfiles);
882     return 0 if(@$subfiles);
883     for my $subdir (@$subdirs)
884     {
885 #       print "SUBSUB $dir/$subdir\n";
886         if(1) #$self->dir_is_special($subdir))
887         {
888             if($self->empty("$dir/$subdir", ($maxdepth-1)))
889             {
890 #               print "EMPTY: $dir / $subdir\n";
891             }
892             else
893             {
894 #               print "NONEMPTY: $dir / $subdir\n";
895                 return 0;
896             }
897         }
898         else
899         {
900             return 0;
901         }
902 #       return 0 if($self->nonempty("$dir/$subdir", ($maxdepth-1)));
903     }
904     return 1;
905 }
906
907 sub dir_is_special
908 {
909     my($self, $dir)=@_;
910     my $id=$self->{db}->lookup_id("tags", $dir);
911     if((grep { $_ eq $dir; } (qw(AND OR NOT), $PATH_ALLTRACKS,
912                               $PATH_NOARTIST, $PATH_NOALBUM)) ||
913        ($id && $self->{db}->tag_has_values($id)))
914     {
915         return 1;
916     }
917     return 0;
918 }
919
920 1;