IMDb SQLite database: add get_cast method
[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 person_statement = null;
223         public int add_person (string name) {
224                 int rc;
225
226                 if (person_statement == null) {
227                         string sql = "INSERT INTO People(Name) VALUES (?);";
228                         rc = db.prepare_v2 (sql, -1, out person_statement);
229                         if (rc != Sqlite.OK) {
230                                 error ("Failed to prepare statement \"" + sql + "\": " + db.errmsg ());
231                         }
232                 }
233
234                 assert (person_statement.bind_text (1, name) == Sqlite.OK);
235
236                 rc = person_statement.step ();
237                 if (rc != Sqlite.DONE) {
238                         if (rc == Sqlite.CONSTRAINT)
239                                 stderr.printf ("Skipped duplicate person \"%s\"\n", name);
240                         else
241                                 stderr.printf ("Failed to insert person \"%s\": %d, %s\n", name, rc, db.errmsg ());
242                         person_statement.reset ();
243                         return 1;
244                 }
245                 person_statement.reset ();
246
247                 return 0;
248         }
249
250         Statement actor_statement;
251         public int add_actor (string name, string title, string? info, string? character, int number = 0) {
252                 int actorid;
253                 int titleid;
254                 int rc;
255
256                 if (!person_exists (name, out actorid))
257                         return 1;
258                 if (!movie_exists (title, out titleid))
259                         return 1;
260
261                 if (actor_statement == null) {
262                         string sql = "INSERT INTO Actors(ActorID, TitleID, Info, Character, Number) VALUES (?, ?, ?, ?, ?);";
263                         assert (db.prepare_v2 (sql, -1, out actor_statement) == Sqlite.OK);
264                 }
265
266                 assert (actor_statement.bind_int (1, actorid) == Sqlite.OK);
267                 assert (actor_statement.bind_int (2, titleid) == Sqlite.OK);
268                 if (info != null)
269                         assert (actor_statement.bind_text (3, info) == Sqlite.OK);
270                 else
271                         assert (actor_statement.bind_null (3) == Sqlite.OK);
272
273                 if (character != null)
274                         assert (actor_statement.bind_text (4, character) == Sqlite.OK);
275                 else
276                         assert (actor_statement.bind_null (4) == Sqlite.OK);
277                 assert (actor_statement.bind_int (5, number) == Sqlite.OK);
278
279                 rc = actor_statement.step ();
280                 if (rc != Sqlite.DONE) {
281                         stderr.printf ("Failed to insert actor \"%s\": %d, %s\n", name, rc, db.errmsg ());
282                         actor_statement.reset ();
283                         return 1;
284                 }
285                 actor_statement.reset ();
286
287                 return 0;
288
289         }
290
291         Statement exists_statement = null;
292         public bool movie_exists (string title, out int rowid = null) {
293                 if (exists_statement == null) {
294                         string sql = "SELECT rowid FROM Movies WHERE Title=?";
295                         assert (db.prepare_v2 (sql, -1, out exists_statement) == Sqlite.OK);
296                 }
297
298                 assert (exists_statement.bind_text (1, title) == Sqlite.OK);
299
300                 int rc = Sqlite.OK;
301                 do {
302                         rc = exists_statement.step ();
303                         if (rc == Sqlite.ROW) {
304                                 if (&rowid != null) {
305                                         rowid = exists_statement.column_int (0);
306                                 }
307                                 exists_statement.reset ();
308                                 return true;
309                         }
310                 } while (rc == Sqlite.ROW);
311                 exists_statement.reset ();
312
313                 return false;
314         }
315
316         Statement person_exists_statement = null;
317         public bool person_exists (string name, out int rowid = null) {
318                 if (person_exists_statement == null) {
319                         string sql = "SELECT rowid FROM People WHERE Name=?";
320                         assert (db.prepare_v2 (sql, -1, out person_exists_statement) == Sqlite.OK);
321                 }
322
323                 assert (person_exists_statement.bind_text (1, name) == Sqlite.OK);
324
325                 int rc = Sqlite.OK;
326                 do {
327                         rc = person_exists_statement.step ();
328                         if (rc == Sqlite.ROW) {
329                                 if (&rowid != null) {
330                                         rowid = person_exists_statement.column_int (0);
331                                 }
332                                 person_exists_statement.reset ();
333
334                                 return true;
335                         }
336                 } while (rc == Sqlite.ROW);
337                 person_exists_statement.reset ();
338
339                 return false;
340         }
341
342         public int count (string table) {
343                 string sql = "SELECT count(*) FROM %s".printf (table);
344                 Statement stmt;
345                 int rc;
346                 int count = 0;
347
348                 rc = db.prepare_v2 (sql, -1, out stmt);
349                 if (rc != Sqlite.OK) {
350                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
351                         db.progress_handler (0, null);
352                         return 0;
353                 }
354
355                 do {
356                         rc = stmt.step ();
357                         if (rc == Sqlite.ROW) {
358                                 count = stmt.column_int (0);
359                         }
360                 } while (rc == Sqlite.ROW);
361
362                 return count;
363         }
364
365         public bool has_table (string table) {
366                 string sql = "SELECT * FROM sqlite_master WHERE type=\"table\" AND name=\"%s\"".printf (table);
367                 Statement stmt;
368                 int rc;
369
370                 rc = db.prepare_v2 (sql, -1, out stmt);
371                 if (rc != Sqlite.OK) {
372                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
373                         db.progress_handler (0, null);
374                         return false;
375                 }
376
377                 do {
378                         rc = stmt.step ();
379                         if (rc == Sqlite.ROW)
380                                 return true;
381                 } while (rc == Sqlite.ROW);
382
383                 return false;
384         }
385
386         public string get_plot (string title) {
387                 string sql = "SELECT Plot FROM Plots WHERE rowid in (SELECT rowid FROM Movies WHERE Title=\"%s\")".printf (title);
388                 Statement stmt;
389                 int rc;
390                 int count = 0;
391
392                 rc = db.prepare_v2 (sql, -1, out stmt);
393                 if (rc != Sqlite.OK) {
394                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
395                         db.progress_handler (0, null);
396                         return "";
397                 }
398
399                 do {
400                         rc = stmt.step ();
401                         if (rc == Sqlite.ROW) {
402                                 return stmt.column_text (0).replace ("&quot;", "\"");
403                         }
404                 } while (rc == Sqlite.ROW);
405
406                 return "";
407         }
408
409         public List<Role> get_cast (string title) {
410                 int rowid;
411                 string sql = "SELECT Name,Character FROM Actors,People WHERE TitleID IN (SELECT rowid FROM Movies WHERE Title=\"%s\") AND ActorID=People.rowid AND Number>0 ORDER BY NUMBER LIMIT 3".printf (title);
412                 Statement stmt;
413                 int rc;
414                 int count = 0;
415                 var cast = new List<Role> ();
416
417                 if (!movie_exists (title, out rowid))
418                         return null;
419
420                 rc = db.prepare_v2 (sql, -1, out stmt);
421                 if (rc != Sqlite.OK) {
422                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
423                         return null;
424                 }
425
426                 do {
427                         rc = stmt.step ();
428                         if (rc == Sqlite.ROW) {
429                                 var role = new Role ();
430                                 role.actor_name = stmt.column_text (0);
431                                 role.character = stmt.column_text (1);
432                                 cast.append (role);
433                         }
434                 } while (rc == Sqlite.ROW);
435
436                 return cast;
437         }
438
439         public int clear () {
440                 int rc;
441
442                 rc = db.exec (
443                         "DROP TABLE IF EXISTS Movies;" +
444                         "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);" +
445                         "DROP TABLE IF EXISTS Genres;" +
446                         "CREATE TABLE Genres (Bit INTEGER PRIMARY KEY, Genre TEXT NOT NULL);" +
447                         "DROP TABLE IF EXISTS Akas;" +
448                         "CREATE TABLE Akas (Aka TEXT NOT NULL COLLATE NOCASE, TitleID INTEGER NOT NULL);" +
449                         "DROP TABLE IF EXISTS Plots;" +
450                         "CREATE TABLE Plots (Plot TEXT NOT NULL, Author TEXT);" +
451                         "DROP TABLE IF EXISTS People;" +
452                         "CREATE TABLE People (Name TEXT PRIMARY KEY COLLATE NOCASE);" +
453                         "DROP TABLE IF EXISTS Actors;" +
454                         "CREATE TABLE Actors (ActorID INTEGER NOT NULL, TitleID INTEGER NOT NULL, Info TEXT, Character TEXT, Number INTEGER NOT NULL DEFAULT 0);" +
455                         "CREATE INDEX MovieCast ON Actors(TitleID);" +
456                         "CREATE INDEX ActorFilmography ON Actors(ActorID);",
457                         callback, null);
458                 if (rc != Sqlite.OK) {
459                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
460                         return 1;
461                 }
462
463                 return 0;
464         }
465
466         public int create_aka_index () {
467                 int rc;
468
469                 rc = db.exec ("CREATE INDEX AkasAka ON Akas(Aka);", callback, null);
470                 if (rc != Sqlite.OK) {
471                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
472                         return 1;
473                 }
474
475                 return 0;
476         }
477
478         public int create_votes_index () {
479                 int rc;
480
481                 rc = db.exec ("CREATE INDEX MovieVotes ON Movies(Votes);", callback, null);
482                 if (rc != Sqlite.OK) {
483                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
484                         return 1;
485                 }
486
487                 return 0;
488         }
489
490         private Cancellable? _cancellable;
491         public async int query (MovieFilter filter, ReceiveMovieFunction receive_movie, int limit, Cancellable? cancellable) {
492                 var sql = "SELECT Title, Year, Rating, Genres FROM Movies";
493                 var sep = " WHERE ";
494                 string match = null;
495                 Statement stmt;
496                 int rc;
497
498                 // FIXME - how many opcodes until main loop iteration for best responsivity?
499                 _cancellable = cancellable;
500                 db.progress_handler (1000, progress_handler);
501
502                 if (filter.title != null && filter.title != "") {
503                         if ("*" in filter.title) {
504                                 match = "GLOB \"%s (*)\"".printf (filter.title);
505                         } else {
506                                 match = "LIKE \"%s%%\"".printf (filter.title);
507                         }
508                         sql += sep + "(Title %s OR rowid IN (SELECT TitleID FROM Akas WHERE Aka %s))".printf (match, match);
509                         sep = " AND ";
510                 }
511                 if (filter.year_min > 0) {
512                         sql += sep + "Year >= %d".printf (filter.year_min);
513                         sep = " AND ";
514                 }
515                 if (filter.year_max > 0) {
516                         sql += sep + "Year <= %d".printf (filter.year_max);
517                         sep = " AND ";
518                 }
519                 if (filter.rating_min > 0) {
520                         sql += sep + "Rating >= %d".printf (filter.rating_min);
521                         sep = " AND ";
522                 }
523                 if (filter.genres.field != 0) {
524                         sql += sep + "Genres&%d = %d".printf (filter.genres.field, filter.genres.field);
525                 }
526                 sql += " ORDER BY Votes DESC LIMIT %d;".printf (limit + 1);
527
528                 stdout.printf("SQL: \"%s\"\n", sql);
529
530                 rc = db.prepare_v2 (sql, -1, out stmt);
531                 if (rc != Sqlite.OK) {
532                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
533                         db.progress_handler (0, null);
534                         return 0;
535                 }
536
537                 int n = 0;
538                 do {
539                         if (++n > limit)
540                                 break;
541                         Idle.add (query.callback);
542                         yield;
543                         rc = stmt.step ();
544                         if (rc == Sqlite.ROW) {
545                                 int year = stmt.column_int (1);
546                                 string title = stmt.column_text (0);
547                                 int rating;
548                                 if (stmt.column_type (2) != Sqlite.NULL)
549                                         rating = stmt.column_int (2);
550                                 else
551                                         rating = -1;
552                                 int genres = stmt.column_int (3);
553                                 string aka = null;
554                                 if (match != null && !(filter.matches_title (strip_year (title, year)))) {
555                                         aka = movie_aka (title, match);
556                                         if (aka != null)
557                                                 aka = strip_year (aka, year);
558                                 }
559                                 receive_movie (strip_year (title, year), aka, year, rating, genres);
560                         }
561                 } while (rc == Sqlite.ROW);
562
563                 db.progress_handler (0, null);
564                 return n;
565         }
566
567         private string? movie_aka (string title, string match) {
568                 string sql = "SELECT Aka FROM Akas WHERE (TitleID = (SELECT rowid FROM Movies WHERE Title = \"%s\") AND Aka %s) LIMIT 1;".printf (title, match);
569                 Statement stmt;
570                 int rc;
571                 string aka = null;
572
573                 rc = db.prepare_v2 (sql, -1, out stmt);
574                 if (rc != Sqlite.OK) {
575                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
576                         return null;
577                 }
578
579                 do {
580                         rc = stmt.step ();
581                         if (rc == Sqlite.ROW) {
582                                 aka = stmt.column_text (0);
583                         }
584                 } while (rc == Sqlite.ROW);
585
586                 return aka;
587         }
588
589         private int progress_handler () {
590                 ((MainContext) null).iteration (false);
591                 return (int) _cancellable.is_cancelled ();
592         }
593
594         private string strip_year (string title, int year) {
595                 string year_suffix = " (%d)".printf (year);
596                 if (title.has_suffix (year_suffix))
597                         return title.substring (0, title.length - year_suffix.length);
598                 year_suffix = " (%d/I)".printf (year);
599                 if (title.has_suffix (year_suffix))
600                         return title.substring (0, title.length - year_suffix.length);
601                 year_suffix = " (%d/II)".printf (year);
602                 if (title.has_suffix (year_suffix))
603                         return title.substring (0, title.length - year_suffix.length);
604                 return title.dup ();
605         }
606 }