basic tags display in root of fs
[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, $path)=@_;
87     my $sql="SELECT DISTINCT name FROM tags;";
88     my $tags=$self->cmd_rows($sql);
89     return(map { $_->[0]; } @$tags);
90 }
91
92 sub add
93 {
94     my($self,$path)=@_;
95     my $file=ID3FS::File->new($path);
96     return unless(defined($file));
97     my $artist=$file->artist();
98     my $album=$file->album();
99     my $v1genre=$file->v1genre();
100     my $year=$file->year();
101     my $audiotype=$file->album();
102     my $tags=$file->tags();
103     my $haspic=$file->haspic();
104
105     my $file_id=$self->add_to_table("files", $path);
106     my $artists_id=$self->add_to_table("artists",  $artist);
107     my $albums_id=$self->add_to_table("albums",  $album);
108     for my $tag (keys %$tags)
109     {
110         $self->add_tag($file_id, $tag, $tags->{$tag});
111     }
112
113     if($self->ok($year))
114     {
115         $self->add_tag($file_id, "year", $year);
116         if($year=~/^(\d\d\d)\d$/)
117         {
118             $self->add_tag($file_id, "decade", "${1}0s");
119         }
120     }
121     if($self->ok($v1genre))
122     {
123         $self->add_tag($file_id, "v1genre", $v1genre);
124     }
125
126     if($haspic)
127     {
128         $self->add_tag($file_id, "haspic", undef);
129     }
130
131     $self->add_relation("files_x_artists",
132                         { "files_id" => $file_id,
133                           "artists_id" => $artists_id });
134
135     $self->add_relation("artists_x_albums",
136                       { "artists_id" => $artists_id,
137                         "albums_id" => $albums_id});
138 }
139
140 sub add_tag
141 {
142     my($self, $file_id, $tag, $val)=@_;
143     my $tag_id=$self->add_to_table("tags",  $tag);
144     $self->add_relation("files_x_tags",
145                         { "files_id" => $file_id,
146                           "tags_id"  => $tag_id });
147     if(defined($val))
148     {
149         my $val_id=$self->add_to_table("tagvals", $val);
150         $self->add_relation("tags_x_tagvals",
151                             { "tags_id"     => $tag_id,
152                               "tagvals_id"  => $val_id });
153     }
154 }
155
156 sub add_to_table
157 {
158     my($self, $table, $name, $extradata)=@_;
159     my $id=$self->lookup_id($table, $name);
160     unless(defined($id))
161     {
162         my $sql="INSERT INTO $table (";
163         my @fields=qw(name);
164         if(defined($extradata))
165         {
166             push(@fields, sort keys(%$extradata));
167         }
168         $sql .= join(", ", @fields);
169         $sql .=") VALUES (";
170         $sql .= join(", ", map { "?"; } @fields);
171         $sql .= ");";
172         $id=$self->cmd_id($sql, $name, map { $extradata->{$_} || ""; } sort keys %$extradata);
173     }
174     return $id;
175 }
176
177 sub add_relation
178 {
179     my ($self, $relname, $fields)=@_;
180     return if($self->relation_exists($relname, $fields));
181     my $sql="INSERT INTO $relname (";
182     $sql .= join(", ", sort keys(%$fields));
183     $sql .= ") VALUES (";
184     $sql .= join(", ", map { "?"; } sort keys(%$fields));
185     $sql .= ");";
186     $self->cmd($sql, map { $fields->{$_}; } sort keys(%$fields));
187 }
188
189 sub lookup_id
190 {
191     my($self, $table, $name)=@_;
192     my($id)=$self->cmd_onerow("SELECT id FROM $table where name=?", $name);
193     return $id;
194 }
195
196 sub relation_exists
197 {
198     my ($self, $relname, $fields)=@_;
199     my $sql="SELECT count(1) FROM $relname WHERE ";
200     my @exprs=();
201     my @vals=();
202     for my $field (keys %$fields)
203     {
204         push(@exprs,$field);
205         push(@vals,$fields->{$field});
206     }
207     $sql .= join(' AND ', map { "$_=?"; } @exprs);
208     my ($ret)=$self->cmd_onerow($sql, @vals);
209     return $ret;
210 }
211
212 sub ok
213 {
214     my($self, $thing)=@_;
215     return(defined($thing) && length($thing));
216 }
217
218 sub cmd
219 {
220     my ($self, @args)=@_;
221     # don't care about retcode
222     $self->cmd_sth(@args);
223 }
224
225 sub cmd_onerow
226 {
227     my ($self, @args)=@_;
228     my $sth=$self->cmd_sth(@args);
229     return($sth->fetchrow_array());
230 }
231
232 sub cmd_rows
233 {
234     my ($self, @args)=@_;
235     my $sth=$self->cmd_sth(@args);
236     return $sth->fetchall_arrayref();
237 }
238
239 sub cmd_id
240 {
241     my ($self, @args)=@_;
242     $self->cmd_sth(@args);
243     return($self->last_insert_id());
244 }
245
246 sub last_insert_id
247 {
248     my $self=shift;
249     return $self->{dbh}->last_insert_id("","","","");
250 }
251
252 __DATA__
253
254 CREATE TABLE id3fs (
255     schema_version
256 );
257
258 CREATE TABLE files (
259     id INTEGER PRIMARY KEY,
260     name
261 );
262
263 CREATE TABLE artists (
264     id INTEGER PRIMARY KEY,
265     name
266 );
267
268 CREATE TABLE albums (
269     id INTEGER PRIMARY KEY,
270     name
271 );
272
273 CREATE TABLE tags (
274     id INTEGER PRIMARY KEY,
275     name
276 );
277
278 CREATE TABLE tagvals (
279     id INTEGER PRIMARY KEY,
280     name
281 );
282
283 CREATE TABLE files_x_tags (
284     files_id,
285     tags_id
286 );
287
288 CREATE TABLE tags_x_tagvals (
289     tags_id,
290     tagvals_id
291 );
292
293 CREATE TABLE files_x_artists (
294     files_id,
295     artists_id
296 );
297
298 CREATE TABLE artists_x_albums (
299     artists_id,
300     albums_id
301 );