9d3a3d539e6e10205f85c677893fcd329d30241b
[cinaest] / src / imdb / imdb-sqlite.vala
1 /* This file is part of Cinaest.
2  *
3  * Copyright (C) 2009 Philipp Zabel
4  *
5  * Cinaest is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * Cinaest is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Cinaest. If not, see <http://www.gnu.org/licenses/>.
17  */
18
19 using Sqlite;
20
21 class IMDbSqlite : Object {
22         Database db;
23         List<string> genres;
24
25         public delegate void ReceiveMovieFunction (string title, string? aka, int year, int rating, int genres);
26
27         public IMDbSqlite (string filename) {
28                 int rc;
29
30                 genres = new List<string> ();
31
32                 rc = Database.open (filename, out db);
33                 if (rc != Sqlite.OK) {
34                         stderr.printf ("Can't open database: %d, %s\n", rc, db.errmsg ());
35                         return;
36                 }
37
38                 rc = db.exec ("PRAGMA journal_mode = OFF;", callback, null);
39                 if (rc != Sqlite.OK) {
40                         stderr.printf ("Can't turn off journal mode: %d, %s\n", rc, db.errmsg ());
41                         return;
42                 }
43
44                 rc = db.exec ("PRAGMA locking_mode = EXCLUSIVE;", callback, null);
45                 if (rc != Sqlite.OK) {
46                         stderr.printf ("Can't get exclusive lock: %d, %s\n", rc, db.errmsg ());
47                         return;
48                 }
49
50                 rc = db.exec ("PRAGMA synchronous = OFF;", callback, null);
51                 if (rc != Sqlite.OK)
52                         stderr.printf ("Can't turn off synchronous access: %d, %s\n", rc, db.errmsg ());
53
54         }
55
56         public static int callback (int n_columns, string[] values,
57                                     string[] column_names) {
58                 for (int i = 0; i < n_columns; i++) {
59                         stdout.printf ("%s = %s\n", column_names[i], values[i]);
60                 }
61                 stdout.printf ("\n");
62
63                 return 0;
64         }
65
66         Statement add_statement = null;
67         public int add_movie (string title, int year) {
68                 int rc;
69
70                 if (add_statement == null) {
71                         string sql = "INSERT INTO Movies(Title, Year) VALUES (?, ?);";
72                         assert (db.prepare_v2 (sql, -1, out add_statement) == Sqlite.OK);
73                 }
74
75                 assert (add_statement.bind_text (1, title) == Sqlite.OK);
76                 assert (add_statement.bind_int (2, year) == Sqlite.OK);
77
78                 rc = add_statement.step ();
79                 if (rc != Sqlite.DONE) {
80                         if (rc == Sqlite.CONSTRAINT)
81                                 stderr.printf ("Skipped duplicate \"%s\" (%d)\n", title, year);
82                         else
83                                 stderr.printf ("Failed to insert movie \"%s\" (%d): %d, %s\n", title, year, rc, db.errmsg ());
84                         add_statement.reset ();
85                         return 1;
86                 }
87                 add_statement.reset ();
88
89                 return 0;
90         }
91
92         Statement rating_statement = null;
93         public int movie_set_rating (string title, int rating, int votes) {
94                 int rc;
95
96                 if (rating_statement == null) {
97                         var sql = "UPDATE Movies SET Rating=?, Votes=? WHERE Title=?;";
98                         assert (db.prepare_v2 (sql, -1, out rating_statement) == Sqlite.OK);
99                 }
100
101                 assert (rating_statement.bind_int (1, rating) == Sqlite.OK);
102                 assert (rating_statement.bind_int (2, votes) == Sqlite.OK);
103                 assert (rating_statement.bind_text (3, title) == Sqlite.OK);
104
105                 rc = rating_statement.step ();
106                 if (rc != Sqlite.DONE) {
107                         stderr.printf ("Failed to set rating %d(%d) for \"%s\": %d, %s\n", rating, votes, title, rc, db.errmsg ());
108                         rating_statement.reset ();
109                         return 1;
110                 }
111                 rating_statement.reset ();
112
113                 return 0;
114         }
115
116         Statement genre_statement = null;
117         public int movie_add_genre (string title, string genre) {
118                 string sql;
119                 int bit;
120                 int rc;
121
122                 bit = genre_bit (genre);
123                 if (bit == 0) {
124                         genres.append (genre);
125                         bit = genre_bit (genre);
126
127                         sql = "INSERT INTO Genres(Bit, Genre) VALUES (%d, \"%s\");".printf (bit, genre);
128
129                         rc = db.exec (sql, callback, null);
130                         if (rc != Sqlite.OK) {
131                                 stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
132                                 return 1;
133                         }
134                 }
135
136                 if (genre_statement == null) {
137                         sql = "UPDATE Movies SET Genres=Genres|? WHERE Title=?;";
138                         assert (db.prepare_v2 (sql, -1, out genre_statement) == Sqlite.OK);
139                 }
140
141                 assert (genre_statement.bind_int (1, bit) == Sqlite.OK);
142                 assert (genre_statement.bind_text (2, title) == Sqlite.OK);
143
144
145                 rc = genre_statement.step ();
146                 if (rc != Sqlite.DONE) {
147                         stderr.printf ("Failed to add genre %s to \"%s\": %d, %s\n", genre, title, rc, db.errmsg ());
148                         rating_statement.reset ();
149                         return 1;
150                 }
151                 genre_statement.reset ();
152
153                 return 0;
154         }
155
156         int genre_bit (string genre) {
157                 for (int i = 0; i < genres.length (); i++) {
158                         if (genres.nth_data (i) == genre)
159                                 return 1 << i;
160                 }
161                 return 0;
162         }
163
164         Statement aka_statement = null;
165         public int add_aka (string title, string aka) {
166                 int rc;
167                 int rowid;
168
169                 if (!movie_exists (title, out rowid))
170                         return 1;
171
172                 if (aka_statement == null) {
173                         string sql = "INSERT INTO Akas(Aka, TitleID) VALUES (?, ?);";
174                         assert (db.prepare_v2 (sql, -1, out aka_statement) == Sqlite.OK);
175                 }
176
177                 assert (aka_statement.bind_text (1, aka) == Sqlite.OK);
178                 assert (aka_statement.bind_int (2, rowid) == Sqlite.OK);
179                 
180                 rc = aka_statement.step ();
181                 if (rc != Sqlite.DONE) {
182                         stderr.printf ("Failed to add aka \"%s\" to \"%s\": %d, %s\n", aka, title, rc, db.errmsg ());
183                         aka_statement.reset ();
184                         return 1;
185                 }
186                 aka_statement.reset ();
187
188                 return 0;
189         }
190
191         Statement plot_statement = null;
192         public int add_plot (string title, string plot, string? author) {
193                 int rc;
194                 int rowid;
195
196                 if (!movie_exists (title, out rowid))
197                         return 1;
198
199                 if (plot_statement == null) {
200                         var sql = "INSERT INTO Plots(rowid, Plot, Author) VALUES (?, ?, ?);";
201                         assert (db.prepare_v2 (sql, -1, out plot_statement) == Sqlite.OK);
202                 }
203
204                 assert (plot_statement.bind_int (1, rowid) == Sqlite.OK);
205                 assert (plot_statement.bind_text (2, plot.replace ("\"", "&quot;")) == Sqlite.OK);
206                 if (author != null)
207                         assert (plot_statement.bind_text (3, author) == Sqlite.OK);
208                 else
209                         assert (plot_statement.bind_null (3) == Sqlite.OK);
210
211                 rc = plot_statement.step ();
212                 if (rc != Sqlite.DONE) {
213                         stderr.printf ("Failed to add plot for \"%s\": %d, %s\n", title, rc, db.errmsg ());
214                         plot_statement.reset ();
215                         return 1;
216                 }
217                 plot_statement.reset ();
218
219                 return 0;
220         }
221
222         Statement exists_statement = null;
223         public bool movie_exists (string title, out int rowid = null) {
224                 if (exists_statement == null) {
225                         string sql = "SELECT rowid FROM Movies WHERE Title=?";
226                         assert (db.prepare_v2 (sql, -1, out exists_statement) == Sqlite.OK);
227                 }
228
229                 assert (exists_statement.bind_text (1, title) == Sqlite.OK);
230
231                 int rc = Sqlite.OK;
232                 do {
233                         rc = exists_statement.step ();
234                         if (rc == Sqlite.ROW) {
235                                 if (&rowid != null) {
236                                         rowid = exists_statement.column_int (0);
237                                 }
238                                 exists_statement.reset ();
239                                 return true;
240                         }
241                 } while (rc == Sqlite.ROW);
242                 exists_statement.reset ();
243
244                 return false;
245         }
246
247         public int count (string table) {
248                 string sql = "SELECT count(*) FROM %s".printf (table);
249                 Statement stmt;
250                 int rc;
251                 int count = 0;
252
253                 rc = db.prepare_v2 (sql, -1, out stmt);
254                 if (rc != Sqlite.OK) {
255                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
256                         db.progress_handler (0, null);
257                         return 0;
258                 }
259
260                 do {
261                         rc = stmt.step ();
262                         if (rc == Sqlite.ROW) {
263                                 count = stmt.column_int (0);
264                         }
265                 } while (rc == Sqlite.ROW);
266
267                 return count;
268         }
269
270         public bool has_plots () {
271                 return (count ("Plots") > 0);
272         }
273
274         public string get_plot (string title) {
275                 string sql = "SELECT Plot FROM Plots WHERE rowid in (SELECT rowid FROM Movies WHERE Title=\"%s\")".printf (title);
276                 Statement stmt;
277                 int rc;
278                 int count = 0;
279
280                 rc = db.prepare_v2 (sql, -1, out stmt);
281                 if (rc != Sqlite.OK) {
282                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
283                         db.progress_handler (0, null);
284                         return "";
285                 }
286
287                 do {
288                         rc = stmt.step ();
289                         if (rc == Sqlite.ROW) {
290                                 return stmt.column_text (0).replace ("&quot;", "\"");
291                         }
292                 } while (rc == Sqlite.ROW);
293
294                 return "";
295         }
296
297         public int clear () {
298                 int rc;
299
300                 rc = db.exec (
301                         "DROP TABLE IF EXISTS Movies;" +
302                         "CREATE TABLE Movies (Title TEXT PRIMARY KEY COLLATE NOCASE, Year INTEGER, Rating INTEGER, Votes INTEGER NOT NULL DEFAULT 0, Genres INTEGER NOT NULL DEFAULT 0);" +
303                         "DROP TABLE IF EXISTS Genres;" +
304                         "CREATE TABLE Genres (Bit INTEGER PRIMARY KEY, Genre TEXT NOT NULL);" +
305                         "DROP TABLE IF EXISTS Akas;" +
306                         "CREATE TABLE Akas (Aka TEXT NOT NULL COLLATE NOCASE, TitleID INTEGER NOT NULL);" +
307                         "DROP TABLE IF EXISTS Plots;" +
308                         "CREATE TABLE Plots (Plot TEXT NOT NULL, Author TEXT)",
309                         callback, null);
310                 if (rc != Sqlite.OK) {
311                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
312                         return 1;
313                 }
314
315                 return 0;
316         }
317
318         public int create_aka_index () {
319                 int rc;
320
321                 rc = db.exec ("CREATE INDEX AkasAka ON Akas(Aka);", callback, null);
322                 if (rc != Sqlite.OK) {
323                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
324                         return 1;
325                 }
326
327                 return 0;
328         }
329
330         public int create_votes_index () {
331                 int rc;
332
333                 rc = db.exec ("CREATE INDEX MovieVotes ON Movies(Votes);", callback, null);
334                 if (rc != Sqlite.OK) {
335                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
336                         return 1;
337                 }
338
339                 return 0;
340         }
341
342         private Cancellable? _cancellable;
343         public async int query (MovieFilter filter, ReceiveMovieFunction receive_movie, int limit, Cancellable? cancellable) {
344                 var sql = "SELECT Title, Year, Rating, Genres FROM Movies";
345                 var sep = " WHERE ";
346                 string match = null;
347                 Statement stmt;
348                 int rc;
349
350                 // FIXME - how many opcodes until main loop iteration for best responsivity?
351                 _cancellable = cancellable;
352                 db.progress_handler (1000, progress_handler);
353
354                 if (filter.title != null && filter.title != "") {
355                         if ("*" in filter.title) {
356                                 match = "GLOB \"%s (*)\"".printf (filter.title);
357                         } else {
358                                 match = "LIKE \"%s%%\"".printf (filter.title);
359                         }
360                         sql += sep + "(Title %s OR rowid IN (SELECT TitleID FROM Akas WHERE Aka %s))".printf (match, match);
361                         sep = " AND ";
362                 }
363                 if (filter.year_min > 0) {
364                         sql += sep + "Year >= %d".printf (filter.year_min);
365                         sep = " AND ";
366                 }
367                 if (filter.year_max > 0) {
368                         sql += sep + "Year <= %d".printf (filter.year_max);
369                         sep = " AND ";
370                 }
371                 if (filter.rating_min > 0) {
372                         sql += sep + "Rating >= %d".printf (filter.rating_min);
373                         sep = " AND ";
374                 }
375                 if (filter.genres.field != 0) {
376                         sql += sep + "Genres&%d = %d".printf (filter.genres.field, filter.genres.field);
377                 }
378                 sql += " ORDER BY Votes DESC LIMIT %d;".printf (limit + 1);
379
380                 stdout.printf("SQL: \"%s\"\n", sql);
381
382                 rc = db.prepare_v2 (sql, -1, out stmt);
383                 if (rc != Sqlite.OK) {
384                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
385                         db.progress_handler (0, null);
386                         return 0;
387                 }
388
389                 int n = 0;
390                 do {
391                         if (++n > limit)
392                                 break;
393                         Idle.add (query.callback);
394                         yield;
395                         rc = stmt.step ();
396                         if (rc == Sqlite.ROW) {
397                                 int year = stmt.column_int (1);
398                                 string title = stmt.column_text (0);
399                                 int rating;
400                                 if (stmt.column_type (2) != Sqlite.NULL)
401                                         rating = stmt.column_int (2);
402                                 else
403                                         rating = -1;
404                                 int genres = stmt.column_int (3);
405                                 string aka = null;
406                                 if (match != null && !(filter.matches_title (strip_year (title, year)))) {
407                                         aka = movie_aka (title, match);
408                                         if (aka != null)
409                                                 aka = strip_year (aka, year);
410                                 }
411                                 receive_movie (strip_year (title, year), aka, year, rating, genres);
412                         }
413                 } while (rc == Sqlite.ROW);
414
415                 db.progress_handler (0, null);
416                 return n;
417         }
418
419         private string? movie_aka (string title, string match) {
420                 string sql = "SELECT Aka FROM Akas WHERE (TitleID = (SELECT rowid FROM Movies WHERE Title = \"%s\") AND Aka %s) LIMIT 1;".printf (title, match);
421                 Statement stmt;
422                 int rc;
423                 string aka = null;
424
425                 rc = db.prepare_v2 (sql, -1, out stmt);
426                 if (rc != Sqlite.OK) {
427                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
428                         return null;
429                 }
430
431                 do {
432                         rc = stmt.step ();
433                         if (rc == Sqlite.ROW) {
434                                 aka = stmt.column_text (0);
435                         }
436                 } while (rc == Sqlite.ROW);
437
438                 return aka;
439         }
440
441         private int progress_handler () {
442                 ((MainContext) null).iteration (false);
443                 return (int) _cancellable.is_cancelled ();
444         }
445
446         private string strip_year (string title, int year) {
447                 string year_suffix = " (%d)".printf (year);
448                 if (title.has_suffix (year_suffix))
449                         return title.substring (0, title.length - year_suffix.length);
450                 year_suffix = " (%d/I)".printf (year);
451                 if (title.has_suffix (year_suffix))
452                         return title.substring (0, title.length - year_suffix.length);
453                 year_suffix = " (%d/II)".printf (year);
454                 if (title.has_suffix (year_suffix))
455                         return title.substring (0, title.length - year_suffix.length);
456                 return title.dup ();
457         }
458 }