be05605c0d53b639b501177af0f078b88fd9ffc7
[cinaest] / src / plugins / catalog-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 CatalogSqlite : Object {
22         Database db;
23
24         public delegate void ReceiveMovieFunction (string title, int year, int rating, int genres);
25
26         public CatalogSqlite (string filename) {
27                 int rc;
28
29                 rc = Database.open (filename, out db);
30                 if (rc != Sqlite.OK) {
31                         stderr.printf ("Can't open database: %d, %s\n", rc, db.errmsg ());
32                         return;
33                 }
34
35                 rc = db.exec ("PRAGMA locking_mode = EXCLUSIVE;", callback, null);
36                 if (rc != Sqlite.OK) {
37                         stderr.printf ("Can't get exclusive lock: %d, %s\n", rc, db.errmsg ());
38                         return;
39                 }
40
41                 rc = db.exec ("PRAGMA synchronous = OFF;", callback, null);
42                 if (rc != Sqlite.OK)
43                         stderr.printf ("Can't turn off synchronous access: %d, %s\n", rc, db.errmsg ());
44
45                 prepare ();
46         }
47
48         public static int callback (int n_columns, string[] values,
49                                     string[] column_names) {
50                 for (int i = 0; i < n_columns; i++) {
51                         stdout.printf ("%s = %s\n", column_names[i], values[i]);
52                 }
53                 stdout.printf ("\n");
54
55                 return 0;
56         }
57
58         public int add_movie (string table, Movie movie) {
59                 string sql = "INSERT INTO %s(Title, Year, Rating, Genres) VALUES (\"%s\", %d, %d, %d);".printf (table, movie.title, movie.year, movie.rating, movie.genres.field);
60                 int rc;
61
62                 rc = db.exec (sql, callback, null);
63                 if (rc != Sqlite.OK) {
64                         stderr.printf ("Failed to insert movie \"%s\" (%d): %d, %s\n", movie.title, movie.year, rc, db.errmsg ());
65                         return 1;
66                 }
67
68                 return 0;
69         }
70
71         public int delete_movie (string table, Movie movie) {
72                 string sql = "DELETE FROM %s WHERE Title=\"%s\" AND Year=%d".printf (table, movie.title, movie.year);
73                 int rc;
74
75                 rc = db.exec (sql, callback, null);
76                 if (rc != Sqlite.OK) {
77                         stderr.printf ("Failed to delete movie \"%s\" (%d): %d, %s\n", movie.title, movie.year, rc, db.errmsg ());
78                         return 1;
79                 }
80
81                 return 0;
82         }
83
84         public int count (string table) {
85                 string sql = "SELECT count(*) FROM %s".printf (table);
86                 Statement stmt;
87                 int rc;
88                 int count = 0;
89
90                 rc = db.prepare_v2 (sql, -1, out stmt);
91                 if (rc != Sqlite.OK) {
92                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
93                         db.progress_handler (0, null);
94                         return 0;
95                 }
96
97                 do {
98                         rc = stmt.step ();
99                         if (rc == Sqlite.ROW) {
100                                 count = stmt.column_int (0);
101                         }
102                 } while (rc == Sqlite.ROW);
103
104                 return count;
105         }
106
107         public bool contains (string table, Movie movie) {
108                 string sql = "SELECT count(*) FROM %s WHERE Title=\"%s\" AND Year=%d".printf (table, movie.title, movie.year);
109                 Statement stmt;
110                 int rc;
111                 int count = 0;
112
113                 rc = db.prepare_v2 (sql, -1, out stmt);
114                 if (rc != Sqlite.OK) {
115                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
116                         db.progress_handler (0, null);
117                         return false;
118                 }
119
120                 do {
121                         rc = stmt.step ();
122                         if (rc == Sqlite.ROW) {
123                                 count = stmt.column_int (0);
124                         }
125                 } while (rc == Sqlite.ROW);
126
127                 return (count > 0);
128         }
129
130         private int prepare () {
131                 Statement stmt;
132                 int rc;
133
134                 rc = db.exec ("CREATE TABLE IF NOT EXISTS Collection (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0); " +
135                               "CREATE TABLE IF NOT EXISTS Loaned (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0); " +
136                               "CREATE TABLE IF NOT EXISTS Watched (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0, Date INTEGER); " +
137                               "CREATE TABLE IF NOT EXISTS Watchlist (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0);",
138                               callback, null);
139                 if (rc != Sqlite.OK) {
140                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
141                         return 1;
142                 }
143
144                 // Add a date column to the Watched table
145                 rc = db.prepare_v2 ("SELECT sql FROM sqlite_master WHERE (type = 'table' AND name = 'Watched');",
146                                     -1, out stmt);
147                 if (rc != Sqlite.OK) {
148                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
149                         return 1;
150                 }
151                 do {
152                         rc = stmt.step ();
153                         if (rc == Sqlite.ROW) {
154                                 var sql = stmt.column_text (0);
155                                 if (sql == "CREATE TABLE Watched (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0)") {
156                                         rc = db.exec ("ALTER TABLE Watched ADD COLUMN Date INTEGER;",
157                                                       callback, null);
158                                         if (rc != Sqlite.OK) {
159                                                 stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
160                                                 return 1;
161                                         }
162                                 }
163                                 break;
164                         }
165                 } while (rc == Sqlite.ROW);
166
167                 return 0;
168         }
169
170         public int clear () {
171                 int rc;
172
173                 rc = db.exec ("DROP TABLE IF EXISTS Collection; CREATE TABLE Collection (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0); DROP TABLE IF EXISTS Loaned; CREATE TABLE Loaned (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0); DROP TABLE IF EXISTS Watchlist; CREATE TABLE Watchlist (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0);", callback, null);
174                 if (rc != Sqlite.OK) {
175                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
176                         return 1;
177                 }
178
179                 return 0;
180         }
181
182         private Cancellable? _cancellable;
183         public async int query (string table, MovieFilter filter, MovieSource.ReceiveMovieFunction callback, int limit, Cancellable? cancellable) {
184                 var sql = new StringBuilder ();
185                 sql.append ("SELECT Title, Year, Rating, Genres");
186                 if (table == "Watched")
187                         sql.append (", Date");
188                 sql.append (" FROM ");
189                 sql.append (table);
190                 var sep = " WHERE ";
191                 Statement stmt;
192                 int rc;
193
194                 // FIXME - how many opcodes until main loop iteration for best responsivity?
195                 _cancellable = cancellable;
196                 db.progress_handler (1000, progress_handler);
197
198                 if (filter.title != null && filter.title != "") {
199                         if ("*" in filter.title) {
200                                 sql.append (sep);
201                                 sql.append_printf ("Title GLOB \"%s\"", filter.title);
202                         } else {
203                                 sql.append (sep);
204                                 sql.append_printf ("Title LIKE \"%s%%\"", filter.title);
205                         }
206                         sep = " AND ";
207                 }
208                 if (filter.year_min > 0) {
209                         sql.append (sep);
210                         sql.append_printf ("Year >= %d", filter.year_min);
211                         sep = " AND ";
212                 }
213                 if (filter.year_max > 0) {
214                         sql.append (sep);
215                         sql.append_printf ("Year <= %d", filter.year_max);
216                         sep = " AND ";
217                 }
218                 if (filter.rating_min > 0) {
219                         sql.append (sep);
220                         sql.append_printf ("Rating >= %d", filter.rating_min);
221                         sep = " AND ";
222                 }
223                 if (filter.genres.field != 0) {
224                         sql.append (sep);
225                         sql.append_printf ("Genres&%d = %d", filter.genres.field, filter.genres.field);
226                 }
227                 sql.append_printf (" LIMIT %d;", limit);
228
229                 stdout.printf("SQL: \"%s\"\n", sql.str);
230
231                 rc = db.prepare_v2 (sql.str, -1, out stmt);
232                 if (rc != Sqlite.OK) {
233                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
234                         db.progress_handler (0, null);
235                         return 1;
236                 }
237
238                 do {
239                         Idle.add (query.callback);
240                         yield;
241                         rc = stmt.step ();
242                         if (rc == Sqlite.ROW) {
243                                 var movie = new Movie ();
244                                 movie.year = stmt.column_int (1);
245                                 movie.title = stmt.column_text (0);
246                                 movie.rating = stmt.column_int (2);
247                                 movie.genres.field = stmt.column_int (3);
248                                 // TODO - depending on settings, this could be something else, like director info or runtime
249                                 movie.secondary = movie.genres.to_string ();
250                                 callback (movie);
251                         }
252                 } while (rc == Sqlite.ROW);
253
254                 db.progress_handler (0, null);
255                 return 0;
256         }
257
258         private int progress_handler () {
259                 ((MainContext) null).iteration (false);
260                 return (int) _cancellable.is_cancelled ();
261         }
262 }