Add catalog plugin
authorPhilipp Zabel <philipp.zabel@gmail.com>
Tue, 17 Nov 2009 17:02:58 +0000 (18:02 +0100)
committerPhilipp Zabel <philipp.zabel@gmail.com>
Wed, 18 Nov 2009 14:36:44 +0000 (15:36 +0100)
A plugin that lets the user manage his own movie collection
and keep track of movies loaned to friends or otherwise
interesting movies.

Makefile.am
po/POTFILES.in
src/plugins/catalog-plugin.vala [new file with mode: 0644]
src/plugins/catalog-sqlite.vala [new file with mode: 0644]

index 6a17876..bd5e92d 100644 (file)
@@ -14,6 +14,7 @@ libexec_PROGRAMS = \
        imdb-plaintext-downloader
 
 pkglib_LTLIBRARIES = \
+       libcatalog-plugin.la \
        libgoogle-plugin.la \
        libimdb-plugin.la
 
@@ -73,6 +74,35 @@ cinaest_CFLAGS = ${DBUS_CFLAGS} ${GCONF_CFLAGS} ${HILDON_CFLAGS} ${OSSO_CFLAGS}
        -DGETTEXT_PACKAGE=\"@GETTEXT_PACKAGE@\"
 cinaest_LDADD = ${DBUS_LIBS} ${GCONF_LIBS} ${HILDON_LIBS} ${OSSO_LIBS} ${GMODULE_LIBS}
 
+libcatalog_plugin_la_SOURCES = \
+       src/plugins/catalog-plugin.c \
+       src/cell-renderer-vbox.c \
+       src/genres.c \
+        src/movie.c \
+       src/plugin-interface.c \
+       src/plugins/catalog-sqlite.c \
+       src/source-list-view.c
+
+libcatalog_plugin_la_VALASOURCES = \
+       src/plugins/catalog-plugin.vala \
+       src/cell-renderer-vbox.vala \
+       src/genres.vala \
+        src/movie.vala \
+       src/movie-filter.vala \
+       src/plugin-interface.vala \
+       src/plugins/catalog-sqlite.vala \
+       src/source-list-view.vala
+
+libcatalog_plugin_la_VALAFLAGS = --vapidir ./vapi --pkg config \
+       --pkg hildon-1 --pkg libosso --pkg sqlite3
+libcatalog_plugin_la_CFLAGS = ${HILDON_CFLAGS} ${OSSO_CFLAGS} ${SQLITE3_CFLAGS} \
+       -DGETTEXT_PACKAGE=\"@GETTEXT_PACKAGE@\"
+libcatalog_plugin_la_LIBADD = ${HILDON_LIBS} ${OSSO_LIBS} ${SQLITE3_LIBS}
+libcatalog_plugin_la_LDFLAGS = -module
+
+src/plugins/catalog-plugin.c: ${libcatalog_plugin_la_VALASOURCES}
+       ${VALAC} -C ${libcatalog_plugin_la_VALASOURCES} ${libcatalog_plugin_la_VALAFLAGS}
+
 libgoogle_plugin_la_SOURCES = \
        src/plugins/google-plugin.c \
        src/genres.c \
@@ -168,6 +198,7 @@ ACLOCAL_AMFLAGS = -Im4
 
 CLEANFILES = \
        ${cinaest_SOURCES} \
+       ${libcatalog_plugin_la_SOURCES} \
        ${libgoogle_plugin_la_SOURCES} \
        ${libimdb_plugin_la_SOURCES} \
        ${imdb_plaintext_downloader_SOURCES} \
index 220fac1..78adb00 100644 (file)
@@ -1,6 +1,7 @@
 src/main.vala
 src/movie-list-menu.vala
 src/movie-list-window.vala
+src/plugins/catalog-plugin.vala
 src/plugins/imdb-plugin.vala
 src/plugins/google-plugin.vala
 src/settings-dialog.vala
diff --git a/src/plugins/catalog-plugin.vala b/src/plugins/catalog-plugin.vala
new file mode 100644 (file)
index 0000000..cd7b33a
--- /dev/null
@@ -0,0 +1,177 @@
+/* This file is part of Cinaest.
+ *
+ * Copyright (C) 2009 Philipp Zabel
+ *
+ * Cinaest is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Cinaest is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Cinaest. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Gtk;
+using Hildon;
+
+class CatalogPlugin : Plugin {
+       List<CatalogSource> sources;
+       private CatalogSqlite sqlite;
+       private Gtk.Dialog dialog;
+
+       public override void hello (Gtk.Window window, Osso.Context context) {
+               string data_dir = Path.build_filename (Environment.get_user_data_dir(), "cinaest");
+               string filename = Path.build_filename (data_dir, "catalog.db");
+
+               // Make sure the data directory is available
+               DirUtils.create_with_parents (data_dir, 0770);
+
+               sqlite = new CatalogSqlite (filename);
+               sources = new List<CatalogSource> ();
+
+               var source = new CatalogSource ("Collection", _("Collection"), _("Personal movie list"), sqlite);
+               sources.append (source);
+
+               source = new CatalogSource ("Loaned", _("Loaned movies"), _("Movies loaned to friends"), sqlite);
+               sources.append (source);
+
+               source = new CatalogSource ("Watchlist", _("Watchlist"), _("Movies of interest"), sqlite);
+               sources.append (source);
+
+               stdout.printf ("Catalog Plugin Loaded.\n");
+       }
+
+       public override unowned List<MovieSource> get_sources () {
+               return (List<MovieSource>) sources;
+       }
+
+       public override List<MovieAction> get_actions (Movie movie, Gtk.Window window) {
+               var list = new List<MovieAction> ();
+
+               list.append (new MovieAction (_("Add to catalog"), on_add_to_catalog, movie, window));
+
+               return list;
+       }
+
+       private void on_add_to_catalog (Movie movie, Gtk.Window window) {
+               dialog = new Gtk.Dialog ();
+               dialog.set_transient_for (window);
+               dialog.set_title (_("Add movie to catalog - Select list"));
+
+               int i = 0;
+               var available_sources = new List<MovieSource> ();
+               foreach (CatalogSource s in sources) {
+                       if (!s.contains (movie)) {
+                               available_sources.append ((MovieSource) s);
+                               i++;
+                       }
+               }
+
+               var source_list = new SourceListView (available_sources);
+
+               var content = (VBox) dialog.get_content_area ();
+               content.pack_start (source_list, true, true, 0);
+               if (i > 5)
+                       i = 5;
+               content.set_size_request (-1, i*70);
+
+               // Connect signals
+               source_list.source_activated.connect (on_source_activated);
+
+               dialog.show_all ();
+               int res = dialog.run ();
+               if (res >= 0) {
+                       var source = sources.nth_data (res);
+                       source.add_movie (movie);
+
+                       var banner = (Banner) Banner.show_information_with_markup (window, null, _("'%s' added to list '%s'").printf (movie.title, source.get_name ()));
+                       banner.set_timeout (1500);
+               }
+               dialog.destroy ();
+               dialog = null;
+       }
+
+       private void on_source_activated (MovieSource source) {
+               int n = sources.index ((CatalogSource) source);
+
+               dialog.response (n);
+       }
+
+       public override void settings_dialog (Gtk.Window window) {
+               var dialog = new Gtk.Dialog ();
+               dialog.set_transient_for (window);
+               dialog.set_title (_("Catalog plugin settings"));
+
+               var button = new Hildon.Button (SizeType.FINGER_HEIGHT, ButtonArrangement.VERTICAL);
+               button.set_title (_("Select active movie lists"));
+               button.set_value (_("Collection, Loaned movies, Watchlist"));
+
+               var content = (VBox) dialog.get_content_area ();
+               content.pack_start (button, true, true, 0);
+
+               dialog.add_button (_("Done"), ResponseType.ACCEPT);
+
+               dialog.show_all ();
+               int res = dialog.run ();
+               if (res == ResponseType.ACCEPT) {
+               }
+               dialog.destroy ();
+       }
+
+       public override unowned string get_name () {
+               return _("Catalog");
+       }
+}
+
+class CatalogSource : MovieSource {
+       private string table;
+       private string name;
+       private string description;
+       private CatalogSqlite sqlite;
+
+       public CatalogSource (string _table, string _name, string _description, CatalogSqlite _sqlite) {
+               table = _table;
+               name = _name;
+               description = _description;
+               sqlite = _sqlite;
+       }
+
+       public override async void get_movies (MovieFilter filter, MovieSource.ReceiveMovieFunction callback, int limit, Cancellable? cancellable) {
+               yield sqlite.query (table, filter, callback, limit, cancellable);
+       }
+
+       public override void add_movie (Movie movie) {
+               sqlite.add_movie (table, movie);
+       }
+
+       public override void delete_movie (Movie movie) {
+               sqlite.delete_movie (table, movie);
+       }
+
+       internal bool contains (Movie movie) {
+               return sqlite.contains (table, movie);
+       }
+
+       public override unowned string get_name () {
+               return name;
+       }
+
+       public override unowned string get_description () {
+               return description;
+       }
+
+       public override bool get_editable () {
+               return true;
+       }
+}
+
+[ModuleInit]
+public Type register_plugin () {
+       // types are registered automatically
+       return typeof (CatalogPlugin);
+}
diff --git a/src/plugins/catalog-sqlite.vala b/src/plugins/catalog-sqlite.vala
new file mode 100644 (file)
index 0000000..cc55361
--- /dev/null
@@ -0,0 +1,199 @@
+/* This file is part of Cinaest.
+ *
+ * Copyright (C) 2009 Philipp Zabel
+ *
+ * Cinaest is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Cinaest is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Cinaest. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Sqlite;
+
+class CatalogSqlite : Object {
+       Database db;
+
+       public delegate void ReceiveMovieFunction (string title, int year, int rating, int genres);
+
+       public CatalogSqlite (string filename) {
+               int rc;
+
+               rc = Database.open (filename, out db);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("Can't open database: %d, %s\n", rc, db.errmsg ());
+                       return;
+               }
+
+               rc = db.exec ("PRAGMA locking_mode = EXCLUSIVE;", callback, null);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("Can't get exclusive lock: %d, %s\n", rc, db.errmsg ());
+                       return;
+               }
+
+               rc = db.exec ("PRAGMA synchronous = OFF;", callback, null);
+               if (rc != Sqlite.OK)
+                       stderr.printf ("Can't turn off synchronous access: %d, %s\n", rc, db.errmsg ());
+
+               prepare ();
+       }
+
+       public static int callback (int n_columns, string[] values,
+                                   string[] column_names) {
+               for (int i = 0; i < n_columns; i++) {
+                       stdout.printf ("%s = %s\n", column_names[i], values[i]);
+               }
+               stdout.printf ("\n");
+
+               return 0;
+       }
+
+       public int add_movie (string table, Movie movie) {
+               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);
+               int rc;
+
+               rc = db.exec (sql, callback, null);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("Failed to insert movie \"%s\" (%d): %d, %s\n", movie.title, movie.year, rc, db.errmsg ());
+                       return 1;
+               }
+
+               return 0;
+       }
+
+       public int delete_movie (string table, Movie movie) {
+               string sql = "DELETE FROM %s WHERE Title=\"%s\" AND Year=%d".printf (table, movie.title, movie.year);
+               int rc;
+
+               rc = db.exec (sql, callback, null);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("Failed to delete movie \"%s\" (%d): %d, %s\n", movie.title, movie.year, rc, db.errmsg ());
+                       return 1;
+               }
+
+               return 0;
+       }
+
+       public bool contains (string table, Movie movie) {
+               string sql = "SELECT count(*) FROM %s WHERE Title=\"%s\" AND Year=%d".printf (table, movie.title, movie.year);
+               Statement stmt;
+               int rc;
+               int count = 0;
+
+               rc = db.prepare_v2 (sql, -1, out stmt);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
+                       db.progress_handler (0, null);
+                       return false;
+               }
+
+               do {
+                       rc = stmt.step ();
+                       if (rc == Sqlite.ROW) {
+                               count = stmt.column_int (0);
+                       }
+               } while (rc == Sqlite.ROW);
+
+               return (count > 0);
+       }
+
+       private int prepare () {
+               int rc;
+
+               rc = db.exec ("CREATE TABLE IF NOT EXISTS Collection (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS Loaned (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS Watchlist (Title TEXT NOT NULL, Year INTEGER, Rating INTEGER, Genres INTEGER NOT NULL DEFAULT 0);", callback, null);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
+                       return 1;
+               }
+
+               return 0;
+       }
+
+       public int clear () {
+               int rc;
+
+               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);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
+                       return 1;
+               }
+
+               return 0;
+       }
+
+       private Cancellable? _cancellable;
+       public async int query (string table, MovieFilter filter, MovieSource.ReceiveMovieFunction callback, int limit, Cancellable? cancellable) {
+               var sql = "SELECT Title, Year, Rating, Genres FROM %s".printf (table);
+               var sep = " WHERE ";
+               Statement stmt;
+               int rc;
+
+               // FIXME - how many opcodes until main loop iteration for best responsivity?
+               _cancellable = cancellable;
+               db.progress_handler (1000, progress_handler);
+
+               if (filter.title != null && filter.title != "") {
+                       if ("*" in filter.title)
+                               sql += sep + "Title GLOB \"%s (*)\"".printf (filter.title);
+                       else
+                               sql += sep + "Title LIKE \"%s%%\"".printf (filter.title);
+                       sep = " AND ";
+               }
+               if (filter.year_min > 0) {
+                       sql += sep + "Year >= %d".printf (filter.year_min);
+                       sep = " AND ";
+               }
+               if (filter.year_max > 0) {
+                       sql += sep + "Year <= %d".printf (filter.year_max);
+                       sep = " AND ";
+               }
+               if (filter.rating_min > 0) {
+                       sql += sep + "Rating >= = %d".printf (filter.rating_min);
+                       sep = " AND ";
+               }
+               if (filter.genres.field != 0) {
+                       sql += sep + "Genres&%d = %d".printf (filter.genres.field, filter.genres.field);
+               }
+               sql += " LIMIT %d;".printf (limit);
+
+               stdout.printf("SQL: \"%s\"\n", sql);
+
+               rc = db.prepare_v2 (sql, -1, out stmt);
+               if (rc != Sqlite.OK) {
+                       stderr.printf ("SQL error: %d, %s\n", rc, db.errmsg ());
+                       db.progress_handler (0, null);
+                       return 1;
+               }
+
+               do {
+                       Idle.add (query.callback);
+                       yield;
+                       rc = stmt.step ();
+                       if (rc == Sqlite.ROW) {
+                               var movie = new Movie ();
+                               movie.year = stmt.column_int (1);
+                               movie.title = stmt.column_text (0);
+                               movie.rating = stmt.column_int (2);
+                               movie.genres.field = stmt.column_int (3);
+                               // TODO - depending on settings, this could be something else, like director info or runtime
+                               movie.secondary = movie.genres.to_string ();
+                               callback (movie);
+                       }
+               } while (rc == Sqlite.ROW);
+
+               db.progress_handler (0, null);
+               return 0;
+       }
+
+       private int progress_handler () {
+               ((MainContext) null).iteration (false);
+               return (int) _cancellable.is_cancelled ();
+       }
+}