basic tag path traversal
[id3fs.git] / lib / ID3FS / DB.pm
1 package ID3FS::DB;
2
3 use strict;
4 use warnings;
5 use DBI;
6 use ID3FS::File;
7
8 our $SCHEMA_VERSION=1;
9
10 my $dbfile=".id3fs";
11
12 sub new
13 {
14     my $proto=shift;
15     my $class=ref($proto) || $proto;
16     my $self={};
17     bless($self,$class);
18
19     my($dir, $init, $me)=@_;
20     $self->{dbpath}="$dir/$dbfile";
21     $self->{me}=$me;
22
23     my $exists=-f $self->{dbpath};
24     die("$me: $self->{dbpath}: not found. use --init to create.\n") if(!$exists && !$init);
25     die("$me: --init used but $self->{dbpath} exists.\n")           if($exists && $init);
26
27     $self->{dbh}=DBI->connect("dbi:SQLite:dbname=$self->{dbpath}","","",
28                               { AutoCommit=>1 } );
29     unless(defined($self->{dbh}))
30     {
31         die("$me: DB Error: " . $DBI::errstr . "\n");
32     }
33
34     if($init)
35     {
36         $self->create();
37     }
38     else
39     {
40         $self->checkschema();
41     }
42
43     return $self;
44 }
45
46 sub create
47 {
48     my($self,$name)=@_;
49     my @schema=split(/\n\n/,join("", <DATA>));
50     close(DATA);
51     for my $cmd (@schema)
52     {
53         $self->{dbh}->do($cmd);
54     }
55     $self->cmd("INSERT INTO id3fs (schema_version) VALUES (?)", $SCHEMA_VERSION);
56 }
57
58 sub checkschema
59 {
60     my $self=shift;
61     my ($version)=$self->cmd_onerow("SELECT schema_version from id3fs");
62     if(!defined($version) || $version != $SCHEMA_VERSION)
63     {
64         die("$self->{me}: id3fs database version " .
65             defined($version) ? $version : '""' .
66             "not known, current version is $SCHEMA_VERSION.\n");
67     }
68 }
69
70 sub cmd_sth
71 {
72     my($self, $sql, @params)=@_;
73     my $sth=$self->{dbh}->prepare($sql);
74     my $idx=1;
75     for my $param (@params)
76     {
77         $param="" unless(defined($param));
78         $sth->bind_param($idx++, $param);
79     }
80     $sth->execute();
81     return $sth;
82 }
83
84 sub tags
85 {
86     my($self, @constraints)=@_;
87     if(!@constraints) # /
88     {
89         my $sql="SELECT DISTINCT name FROM tags;";
90         my $tags=$self->cmd_rows($sql);
91         return(map { $_->[0]; } @$tags);
92     }
93     my @file_ids=();
94     my @tag_ids=();
95     my $main_sql_start=("SELECT DISTINCT tags.name FROM files\n" .
96                         "INNER JOIN files_x_tags ON files.id=files_x_tags.files_id\n" .
97                         "INNER JOIN tags ON tags.id=files_x_tags.tags_id\n" .
98                         "WHERE files.id in (" .
99                         ("\tSELECT DISTINCT files.id FROM files\n" .
100                          "\tINNER JOIN files_x_tags ON files.id=files_x_tags.files_id\n" .
101                          "\tINNER JOIN tags ON tags.id=files_x_tags.tags_id\n" .
102                          "\tWHERE tags.id in ("));
103     my $main_sql_mid=")\n) AND tags.id NOT IN (";
104     my $main_sql_end=")\n";
105     while(my $constraint=shift @constraints)
106     {
107         print "CONSTRAINT: $constraint->{name}\n";
108         my $cid=$constraint->{id};
109         push(@tag_ids, $cid);
110     }
111     my $sql = ($main_sql_start . join(", ", @tag_ids) .
112                $main_sql_mid   . join(", ", @tag_ids) .
113                $main_sql_end);
114     print "SQL: $sql\n";
115     my $result=$self->cmd_rows($sql);
116     my @tagnames=map { $_->[0]; } @$result;
117     print "SUBNAMES: ", join(', ', @tagnames), "\n";
118     return(@tagnames);
119 }
120
121 sub tag_values
122 {
123     my($self, $tag)=@_;
124     my $sql=("SELECT DISTINCT tagvals.name FROM tags\n" .
125              "INNER JOIN tags_x_tagvals ON tags.id=tags_x_tagvals.tags_id\n" .
126              "INNER JOIN tagvals ON tagvals.id=tags_x_tagvals.tagvals_id\n" .
127              "WHERE tags.name=?");
128     my $tags=$self->cmd_rows($sql, $tag);
129     return(map { $_->[0]; } @$tags);
130 }
131
132 sub tag_id
133 {
134     my($self, $tag)=@_;
135     my $sql='SELECT id FROM tags WHERE name=?';
136     my ($id)=$self->cmd_onerow($sql, $tag);
137     return($id);
138 }
139
140 sub add
141 {
142     my($self,$path)=@_;
143     my $file=ID3FS::File->new($path);
144     return unless(defined($file));
145     my $artist=$file->artist();
146     my $album=$file->album();
147     my $v1genre=$file->v1genre();
148     my $year=$file->year();
149     my $audiotype=$file->album();
150     my $tags=$file->tags();
151     my $haspic=$file->haspic();
152
153     my $file_id=$self->add_to_table("files", $path);
154     my $artists_id=$self->add_to_table("artists",  $artist);
155     my $albums_id=$self->add_to_table("albums",  $album);
156     for my $tag (keys %$tags)
157     {
158         $self->add_tag($file_id, $tag, $tags->{$tag});
159     }
160
161     if($self->ok($year))
162     {
163         $self->add_tag($file_id, "year", $year);
164         if($year=~/^(\d\d\d)\d$/)
165         {
166             $self->add_tag($file_id, "decade", "${1}0s");
167         }
168     }
169     if($self->ok($v1genre))
170     {
171         $self->add_tag($file_id, "v1genre", $v1genre);
172     }
173
174     if($haspic)
175     {
176         $self->add_tag($file_id, "haspic", undef);
177     }
178
179     $self->add_relation("files_x_artists",
180                         { "files_id" => $file_id,
181                           "artists_id" => $artists_id });
182
183     $self->add_relation("artists_x_albums",
184                       { "artists_id" => $artists_id,
185                         "albums_id" => $albums_id});
186 }
187
188 sub add_tag
189 {
190     my($self, $file_id, $tag, $val)=@_;
191     my $tag_id=$self->add_to_table("tags",  $tag);
192     $self->add_relation("files_x_tags",
193                         { "files_id" => $file_id,
194                           "tags_id"  => $tag_id });
195     if(defined($val))
196     {
197         my $val_id=$self->add_to_table("tagvals", $val);
198         $self->add_relation("tags_x_tagvals",
199                             { "tags_id"     => $tag_id,
200                               "tagvals_id"  => $val_id });
201     }
202 }
203
204 sub add_to_table
205 {
206     my($self, $table, $name, $extradata)=@_;
207     my $id=$self->lookup_id($table, $name);
208     unless(defined($id))
209     {
210         my $sql="INSERT INTO $table (";
211         my @fields=qw(name);
212         if(defined($extradata))
213         {
214             push(@fields, sort keys(%$extradata));
215         }
216         $sql .= join(", ", @fields);
217         $sql .=") VALUES (";
218         $sql .= join(", ", map { "?"; } @fields);
219         $sql .= ");";
220         $id=$self->cmd_id($sql, $name, map { $extradata->{$_} || ""; } sort keys %$extradata);
221     }
222     return $id;
223 }
224
225 sub add_relation
226 {
227     my ($self, $relname, $fields)=@_;
228     return if($self->relation_exists($relname, $fields));
229     my $sql="INSERT INTO $relname (";
230     $sql .= join(", ", sort keys(%$fields));
231     $sql .= ") VALUES (";
232     $sql .= join(", ", map { "?"; } sort keys(%$fields));
233     $sql .= ");";
234     $self->cmd($sql, map { $fields->{$_}; } sort keys(%$fields));
235 }
236
237 sub lookup_id
238 {
239     my($self, $table, $name)=@_;
240     my($id)=$self->cmd_onerow("SELECT id FROM $table where name=?", $name);
241     return $id;
242 }
243
244 sub relation_exists
245 {
246     my ($self, $relname, $fields)=@_;
247     my $sql="SELECT count(1) FROM $relname WHERE ";
248     my @exprs=();
249     my @vals=();
250     for my $field (keys %$fields)
251     {
252         push(@exprs,$field);
253         push(@vals,$fields->{$field});
254     }
255     $sql .= join(' AND ', map { "$_=?"; } @exprs);
256     my ($ret)=$self->cmd_onerow($sql, @vals);
257     return $ret;
258 }
259
260 sub ok
261 {
262     my($self, $thing)=@_;
263     return(defined($thing) && length($thing));
264 }
265
266 sub cmd
267 {
268     my ($self, @args)=@_;
269     # don't care about retcode
270     $self->cmd_sth(@args);
271 }
272
273 sub cmd_onerow
274 {
275     my ($self, @args)=@_;
276     my $sth=$self->cmd_sth(@args);
277     return($sth->fetchrow_array());
278 }
279
280 sub cmd_rows
281 {
282     my ($self, @args)=@_;
283     my $sth=$self->cmd_sth(@args);
284     return $sth->fetchall_arrayref();
285 }
286
287 sub cmd_id
288 {
289     my ($self, @args)=@_;
290     $self->cmd_sth(@args);
291     return($self->last_insert_id());
292 }
293
294 sub last_insert_id
295 {
296     my $self=shift;
297     return $self->{dbh}->last_insert_id("","","","");
298 }
299
300 __DATA__
301
302 CREATE TABLE id3fs (
303     schema_version
304 );
305
306 CREATE TABLE files (
307     id INTEGER PRIMARY KEY,
308     name
309 );
310
311 CREATE TABLE artists (
312     id INTEGER PRIMARY KEY,
313     name
314 );
315
316 CREATE TABLE albums (
317     id INTEGER PRIMARY KEY,
318     name
319 );
320
321 CREATE TABLE tags (
322     id INTEGER PRIMARY KEY,
323     name
324 );
325
326 CREATE TABLE tagvals (
327     id INTEGER PRIMARY KEY,
328     name
329 );
330
331 CREATE TABLE files_x_tags (
332     files_id,
333     tags_id
334 );
335
336 CREATE TABLE tags_x_tagvals (
337     tags_id,
338     tagvals_id
339 );
340
341 CREATE TABLE files_x_artists (
342     files_id,
343     artists_id
344 );
345
346 CREATE TABLE artists_x_albums (
347     artists_id,
348     albums_id
349 );