Catalog plugin: add Date column to Watched table
[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 = "SELECT Title, Year, Rating, Genres FROM %s".printf (table);
185                 var sep = " WHERE ";
186                 Statement stmt;
187                 int rc;
188
189                 // FIXME - how many opcodes until main loop iteration for best responsivity?
190                 _cancellable = cancellable;
191                 db.progress_handler (1000, progress_handler);
192
193                 if (filter.title != null && filter.title != "") {
194                         if ("*" in filter.title)
195                                 sql += sep + "Title GLOB \"%s (*)\"".printf (filter.title);
196                         else
197                                 sql += sep + "Title LIKE \"%s%%\"".printf (filter.title);
198                         sep = " AND ";
199                 }
200                 if (filter.year_min > 0) {
201                         sql += sep + "Year >= %d".printf (filter.year_min);
202                         sep = " AND ";
203                 }
204                 if (filter.year_max > 0) {
205                         sql += sep + "Year <= %d".printf (filter.year_max);
206                         sep = " AND ";
207                 }
208                 if (filter.rating_min > 0) {
209                         sql += sep + "Rating >= %d".printf (filter.rating_min);
210                         sep = " AND ";
211                 }
212                 if (filter.genres.field != 0) {
213                         sql += sep + "Genres&%d = %d".printf (filter.genres.field, filter.genres.field);
214                 }
215                 sql += " LIMIT %d;".printf (limit);
216
217                 stdout.printf("SQL: \"%s\"\n", sql);
218
219                 rc = db.prepare_v2 (sql, -1, out stmt);
220                 if (rc != Sqlite.OK) {
221                         stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
222                         db.progress_handler (0, null);
223                         return 1;
224                 }
225
226                 do {
227                         Idle.add (query.callback);
228                         yield;
229                         rc = stmt.step ();
230                         if (rc == Sqlite.ROW) {
231                                 var movie = new Movie ();
232                                 movie.year = stmt.column_int (1);
233                                 movie.title = stmt.column_text (0);
234                                 movie.rating = stmt.column_int (2);
235                                 movie.genres.field = stmt.column_int (3);
236                                 // TODO - depending on settings, this could be something else, like director info or runtime
237                                 movie.secondary = movie.genres.to_string ();
238                                 callback (movie);
239                         }
240                 } while (rc == Sqlite.ROW);
241
242                 db.progress_handler (0, null);
243                 return 0;
244         }
245
246         private int progress_handler () {
247                 ((MainContext) null).iteration (false);
248                 return (int) _cancellable.is_cancelled ();
249         }
250 }