Use sqlite for cinema and DKDM recipient lists.
authorCarl Hetherington <cth@carlh.net>
Sat, 20 May 2023 20:51:49 +0000 (22:51 +0200)
committerCarl Hetherington <cth@carlh.net>
Mon, 6 May 2024 18:42:50 +0000 (20:42 +0200)
53 files changed:
platform/osx/make_dmg.sh
platform/windows/wscript
src/lib/cinema.cc [deleted file]
src/lib/cinema.h
src/lib/cinema_list.cc [new file with mode: 0644]
src/lib/cinema_list.h [new file with mode: 0644]
src/lib/config.cc
src/lib/config.h
src/lib/dkdm_recipient.cc
src/lib/dkdm_recipient.h
src/lib/dkdm_recipient_list.cc [new file with mode: 0644]
src/lib/dkdm_recipient_list.h [new file with mode: 0644]
src/lib/exceptions.h
src/lib/id.cc [new file with mode: 0644]
src/lib/id.h [new file with mode: 0644]
src/lib/kdm_cli.cc
src/lib/kdm_with_metadata.h
src/lib/screen.cc
src/lib/screen.h
src/lib/sqlite_statement.cc [new file with mode: 0644]
src/lib/sqlite_statement.h [new file with mode: 0644]
src/lib/sqlite_table.cc [new file with mode: 0644]
src/lib/sqlite_table.h [new file with mode: 0644]
src/lib/sqlite_transaction.cc [new file with mode: 0644]
src/lib/sqlite_transaction.h [new file with mode: 0644]
src/lib/unzipper.cc
src/lib/unzipper.h
src/lib/util.cc
src/lib/util.h
src/lib/wscript
src/tools/dcpomatic.cc
src/tools/dcpomatic_batch.cc
src/tools/dcpomatic_kdm.cc
src/tools/wscript
src/wx/kdm_dialog.cc
src/wx/load_config_from_zip_dialog.cc [new file with mode: 0644]
src/wx/load_config_from_zip_dialog.h [new file with mode: 0644]
src/wx/recipients_panel.cc
src/wx/recipients_panel.h
src/wx/screens_panel.cc
src/wx/screens_panel.h
src/wx/wscript
src/wx/wx_util.cc
test/cinema_list_test.cc [new file with mode: 0644]
test/config_test.cc
test/data
test/dkdm_recipient_list_test.cc [new file with mode: 0644]
test/kdm_cli_test.cc
test/kdm_naming_test.cc
test/recover_test.cc
test/test.cc
test/wscript
wscript

index 633b830b72949b6762fce9f45d58c928ae899b5a..dd42ebe3274a59a5f2becd33322aad6f71d9f362 100644 (file)
@@ -220,6 +220,7 @@ function copy_libs {
     copy_lib_env libgio "$dest"
     copy_lib_env libz "$dest"
        copy_lib_env libdav1d "$dest"
+       copy_lib_env libsqlite "$dest"
 }
 
 # @param #1 directory to copy to
index 2f0bf2c22bd88ae5e72bf9f0d22494ee3c9fc7ac..2c53a1422b96a747310c31072f7cd6ef784c5987 100644 (file)
@@ -196,6 +196,7 @@ File "%static_deps%/bin/libbrotlidec.dll"
 File "%static_deps%/bin/libbrotlicommon.dll"
 File "%static_deps%/bin/libfribidi-0.dll"
 File "%static_deps%/bin/libsharpyuv-0.dll"
+File "%static_deps%/bin/libsqlite3-0.dll"
     """, file=f)
 
     if bits == 32:
diff --git a/src/lib/cinema.cc b/src/lib/cinema.cc
deleted file mode 100644 (file)
index b1681fc..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
-    Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
-
-    This file is part of DCP-o-matic.
-
-    DCP-o-matic 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 2 of the License, or
-    (at your option) any later version.
-
-    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "cinema.h"
-#include "screen.h"
-#include "dcpomatic_assert.h"
-#include <libcxml/cxml.h>
-#include <dcp/raw_convert.h>
-#include <libxml++/libxml++.h>
-
-
-using std::make_shared;
-using std::shared_ptr;
-using std::string;
-using dcp::raw_convert;
-using dcpomatic::Screen;
-
-
-Cinema::Cinema (cxml::ConstNodePtr node)
-       : name (node->string_child ("Name"))
-       , notes (node->optional_string_child("Notes").get_value_or(""))
-{
-       for (auto i: node->node_children("Email")) {
-               emails.push_back (i->content ());
-       }
-
-       int hour = 0;
-
-       if (node->optional_number_child<int>("UTCOffset")) {
-               hour = node->number_child<int>("UTCOffset");
-       } else {
-               hour = node->optional_number_child<int>("UTCOffsetHour").get_value_or(0);
-       }
-
-       int minute = node->optional_number_child<int>("UTCOffsetMinute").get_value_or(0);
-
-       utc_offset= { hour, minute };
-}
-
-/* This is necessary so that we can use shared_from_this in add_screen (which cannot be done from
-   a constructor)
-*/
-void
-Cinema::read_screens (cxml::ConstNodePtr node)
-{
-       for (auto i: node->node_children("Screen")) {
-               add_screen (make_shared<Screen>(i));
-       }
-}
-
-void
-Cinema::as_xml (xmlpp::Element* parent) const
-{
-       cxml::add_text_child(parent, "Name", name);
-
-       for (auto i: emails) {
-               cxml::add_text_child(parent, "Email", i);
-       }
-
-       cxml::add_text_child(parent, "Notes", notes);
-
-       cxml::add_text_child(parent, "UTCOffsetHour", raw_convert<string>(utc_offset.hour()));
-       cxml::add_text_child(parent, "UTCOffsetMinute", raw_convert<string>(utc_offset.minute()));
-
-       for (auto i: _screens) {
-               i->as_xml(cxml::add_child(parent, "Screen"));
-       }
-}
-
-void
-Cinema::add_screen (shared_ptr<Screen> s)
-{
-       s->cinema = shared_from_this ();
-       _screens.push_back (s);
-}
-
-void
-Cinema::remove_screen (shared_ptr<Screen> s)
-{
-       auto iter = std::find(_screens.begin(), _screens.end(), s);
-       if (iter != _screens.end()) {
-               _screens.erase(iter);
-       }
-}
-
index 05f6fb7fce0d0652a958e8e11df121d70a40d7d4..44f232f9173f01c357159489839072a12989915d 100644 (file)
 
 */
 
+
 /** @file  src/lib/cinema.h
  *  @brief Cinema class.
  */
 
 
 #include <dcp/utc_offset.h>
-#include <libcxml/cxml.h>
 #include <memory>
+#include <string>
+#include <vector>
 
 
-namespace xmlpp {
-       class Element;
-}
-
-namespace dcpomatic {
-       class Screen;
-}
-
 /** @class Cinema
  *  @brief A description of a Cinema for KDM generation.
  *
- *  This is a cinema name, some metadata and a list of
- *  Screen objects.
+ *  This is a cinema name and some metadata.
  */
-class Cinema : public std::enable_shared_from_this<Cinema>
+class Cinema
 {
 public:
        Cinema(std::string const & name_, std::vector<std::string> const & e, std::string notes_, dcp::UTCOffset utc_offset_)
@@ -52,24 +45,8 @@ public:
                , utc_offset(std::move(utc_offset_))
        {}
 
-       explicit Cinema (cxml::ConstNodePtr);
-
-       void read_screens (cxml::ConstNodePtr);
-
-       void as_xml (xmlpp::Element *) const;
-
-       void add_screen (std::shared_ptr<dcpomatic::Screen>);
-       void remove_screen (std::shared_ptr<dcpomatic::Screen>);
-
        std::string name;
        std::vector<std::string> emails;
        std::string notes;
        dcp::UTCOffset utc_offset;
-
-       std::vector<std::shared_ptr<dcpomatic::Screen>> screens() const {
-               return _screens;
-       }
-
-private:
-       std::vector<std::shared_ptr<dcpomatic::Screen>> _screens;
 };
diff --git a/src/lib/cinema_list.cc b/src/lib/cinema_list.cc
new file mode 100644 (file)
index 0000000..41b9dba
--- /dev/null
@@ -0,0 +1,466 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "cinema.h"
+#include "cinema_list.h"
+#include "config.h"
+#include "dcpomatic_assert.h"
+#include "exceptions.h"
+#include "screen.h"
+#include "sqlite_statement.h"
+#include "sqlite_transaction.h"
+#include "util.h"
+#include <dcp/certificate.h>
+#include <sqlite3.h>
+#include <boost/algorithm/string.hpp>
+#include <iostream>
+#include <numeric>
+
+
+using std::pair;
+using std::make_pair;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+CinemaList::CinemaList()
+       : _cinemas("cinemas")
+       , _screens("screens")
+       , _trusted_devices("trusted_devices")
+{
+       setup_tables();
+       setup(Config::instance()->cinemas_file());
+}
+
+
+CinemaList::CinemaList(boost::filesystem::path db_file)
+       : _cinemas("cinemas")
+       , _screens("screens")
+       , _trusted_devices("trusted_devices")
+{
+       setup_tables();
+       setup(db_file);
+}
+
+
+void
+CinemaList::setup_tables()
+{
+       _cinemas.add_column("name", "TEXT");
+       _cinemas.add_column("emails", "TEXT");
+       _cinemas.add_column("notes", "TEXT");
+       _cinemas.add_column("utc_offset_hour", "INTEGER");
+       _cinemas.add_column("utc_offset_minute", "INTEGER");
+
+       _screens.add_column("cinema", "INTEGER");
+       _screens.add_column("name", "TEXT");
+       _screens.add_column("notes", "TEXT");
+       _screens.add_column("recipient", "TEXT");
+       _screens.add_column("recipient_file", "TEXT");
+
+       _trusted_devices.add_column("screen", "INTEGER");
+       _trusted_devices.add_column("certificate_or_thumbprint", "TEXT");
+}
+
+
+void
+CinemaList::read_legacy_file(boost::filesystem::path xml_file)
+{
+       cxml::Document doc("Cinemas");
+       doc.read_file(xml_file);
+       read_legacy_document(doc);
+}
+
+
+void
+CinemaList::read_legacy_string(std::string const& xml)
+{
+       cxml::Document doc("Cinemas");
+       doc.read_string(xml);
+       read_legacy_document(doc);
+}
+
+
+void
+CinemaList::read_legacy_document(cxml::Document const& doc)
+{
+       for (auto cinema_node: doc.node_children("Cinema")) {
+               vector<string> emails;
+               for (auto email_node: cinema_node->node_children("Email")) {
+                       emails.push_back(email_node->content());
+               }
+
+               int hour = 0;
+               if (cinema_node->optional_number_child<int>("UTCOffset")) {
+                       hour = cinema_node->number_child<int>("UTCOffset");
+               } else {
+                       hour = cinema_node->optional_number_child<int>("UTCOffsetHour").get_value_or(0);
+               }
+
+               int minute = cinema_node->optional_number_child<int>("UTCOffsetMinute").get_value_or(0);
+
+               Cinema cinema(
+                       cinema_node->string_child("Name"),
+                       emails,
+                       cinema_node->string_child("Notes"),
+                       dcp::UTCOffset(hour, minute)
+                       );
+
+               auto cinema_id = add_cinema(cinema);
+
+               for (auto screen_node: cinema_node->node_children("Screen")) {
+                       optional<dcp::Certificate> recipient;
+                       if (auto recipient_string = screen_node->optional_string_child("Recipient")) {
+                               recipient = dcp::Certificate(*recipient_string);
+                       }
+                       vector<TrustedDevice> trusted_devices;
+                       for (auto trusted_device_node: screen_node->node_children("TrustedDevice")) {
+                               trusted_devices.push_back(TrustedDevice(trusted_device_node->content()));
+                       }
+                       dcpomatic::Screen screen(
+                               screen_node->string_child("Name"),
+                               screen_node->string_child("Notes"),
+                               recipient,
+                               screen_node->optional_string_child("RecipientFile"),
+                               trusted_devices
+                               );
+                       add_screen(cinema_id, screen);
+               }
+       }
+}
+
+
+void
+CinemaList::clear()
+{
+       for (auto table: { "cinemas", "screens", "trusted_devices" }) {
+               SQLiteStatement sql(_db, String::compose("DELETE FROM %1", table));
+               sql.execute();
+       }
+}
+
+
+void
+CinemaList::setup(boost::filesystem::path db_file)
+{
+#ifdef DCPOMATIC_WINDOWS
+       auto rc = sqlite3_open16(db_file.c_str(), &_db);
+#else
+       auto rc = sqlite3_open(db_file.c_str(), &_db);
+#endif
+       if (rc != SQLITE_OK) {
+               throw FileError("Could not open SQLite database", db_file);
+       }
+
+       sqlite3_busy_timeout(_db, 500);
+
+       SQLiteStatement cinemas(_db, _cinemas.create());
+       cinemas.execute();
+
+       SQLiteStatement screens(_db, _screens.create());
+       screens.execute();
+
+       SQLiteStatement devices(_db, _trusted_devices.create());
+       devices.execute();
+}
+
+
+CinemaList::CinemaList(CinemaList&& other)
+       : _db(other._db)
+       , _cinemas(std::move(other._cinemas))
+       , _screens(std::move(other._screens))
+       , _trusted_devices(std::move(other._trusted_devices))
+{
+       other._db = nullptr;
+}
+
+
+CinemaList&
+CinemaList::operator=(CinemaList&& other)
+{
+       if (this != &other) {
+               _db = other._db;
+               other._db = nullptr;
+       }
+       return *this;
+}
+
+
+CinemaID
+CinemaList::add_cinema(Cinema const& cinema)
+{
+       SQLiteStatement statement(_db, _cinemas.insert());
+
+       statement.bind_text(1, cinema.name);
+       statement.bind_text(2, join_strings(cinema.emails));
+       statement.bind_text(3, cinema.notes);
+       statement.bind_int64(4, cinema.utc_offset.hour());
+       statement.bind_int64(5, cinema.utc_offset.minute());
+
+       statement.execute();
+
+       return sqlite3_last_insert_rowid(_db);
+}
+
+
+void
+CinemaList::update_cinema(CinemaID id, Cinema const& cinema)
+{
+       SQLiteStatement statement(_db, _cinemas.update("WHERE id=?"));
+
+       statement.bind_text(1, cinema.name);
+       statement.bind_text(2, join_strings(cinema.emails));
+       statement.bind_text(3, cinema.notes);
+       statement.bind_int64(4, cinema.utc_offset.hour());
+       statement.bind_int64(5, cinema.utc_offset.minute());
+       statement.bind_int64(6, id.get());
+
+       statement.execute();
+}
+
+
+void
+CinemaList::remove_cinema(CinemaID id)
+{
+       SQLiteStatement statement(_db, "DELETE FROM cinemas WHERE ID=?");
+       statement.bind_int64(1, id.get());
+       statement.execute();
+}
+
+
+CinemaList::~CinemaList()
+{
+       if (_db) {
+               sqlite3_close(_db);
+       }
+}
+
+
+static
+vector<pair<CinemaID, Cinema>>
+cinemas_from_result(SQLiteStatement& statement)
+{
+       vector<pair<CinemaID, Cinema>> output;
+
+       statement.execute([&output](SQLiteStatement& statement) {
+               DCPOMATIC_ASSERT(statement.data_count() == 6);
+               CinemaID const id = statement.column_int64(0);
+               auto const name = statement.column_text(1);
+               auto const join_strings = statement.column_text(2);
+               vector<string> emails;
+               boost::algorithm::split(emails, join_strings, boost::is_any_of(" "));
+               auto const notes = statement.column_text(3);
+               auto const utc_offset_hour = static_cast<int>(statement.column_int64(4));
+               auto const utc_offset_minute = static_cast<int>(statement.column_int64(5));
+               output.push_back(make_pair(id, Cinema(name, { emails }, notes, dcp::UTCOffset{utc_offset_hour, utc_offset_minute})));
+       });
+
+       return output;
+}
+
+
+vector<pair<CinemaID, Cinema>>
+CinemaList::cinemas() const
+{
+       SQLiteStatement statement(_db, _cinemas.select("ORDER BY name ASC"));
+       return cinemas_from_result(statement);
+}
+
+
+optional<Cinema>
+CinemaList::cinema(CinemaID id) const
+{
+       SQLiteStatement statement(_db, _cinemas.select("WHERE id=?"));
+       statement.bind_int64(1, id.get());
+       auto result = cinemas_from_result(statement);
+       if (result.empty()) {
+               return {};
+       }
+       return result[0].second;
+}
+
+ScreenID
+CinemaList::add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen)
+{
+       SQLiteTransaction transaction(_db);
+
+       SQLiteStatement add_screen(_db, _screens.insert());
+
+       add_screen.bind_int64(1, cinema_id.get());
+       add_screen.bind_text(2, screen.name);
+       add_screen.bind_text(3, screen.notes);
+       add_screen.bind_text(4, screen.recipient->certificate(true));
+       add_screen.bind_text(5, screen.recipient_file.get_value_or(""));
+
+       add_screen.execute();
+
+       auto const screen_id = sqlite3_last_insert_rowid(_db);
+
+       for (auto device: screen.trusted_devices) {
+               SQLiteStatement add_device(_db, _trusted_devices.insert());
+               add_device.bind_int64(1, screen_id);
+               add_device.bind_text(2, device.as_string());
+       }
+
+       transaction.commit();
+
+       return screen_id;
+}
+
+
+dcpomatic::Screen
+CinemaList::screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const
+{
+       auto certificate_string = statement.column_text(4);
+       optional<dcp::Certificate> certificate = certificate_string.empty() ? optional<dcp::Certificate>() : dcp::Certificate(certificate_string);
+       auto recipient_file_string = statement.column_text(5);
+       optional<string> recipient_file = recipient_file_string.empty() ? optional<string>() : recipient_file_string;
+
+       SQLiteStatement trusted_devices_statement(_db, _trusted_devices.select("WHERE screen=?"));
+       trusted_devices_statement.bind_int64(1, screen_id.get());
+       vector<TrustedDevice> trusted_devices;
+       trusted_devices_statement.execute([&trusted_devices](SQLiteStatement& statement) {
+               DCPOMATIC_ASSERT(statement.data_count() == 1);
+               auto description = statement.column_text(1);
+               if (boost::algorithm::starts_with(description, "-----BEGIN CERTIFICATE")) {
+                       trusted_devices.push_back(TrustedDevice(dcp::Certificate(description)));
+               } else {
+                       trusted_devices.push_back(TrustedDevice(description));
+               }
+       });
+
+       return dcpomatic::Screen(statement.column_text(2), statement.column_text(3), certificate, recipient_file, trusted_devices);
+}
+
+
+optional<dcpomatic::Screen>
+CinemaList::screen(ScreenID screen_id) const
+{
+       SQLiteStatement statement(_db, _screens.select("WHERE id=?"));
+       statement.bind_int64(1, screen_id.get());
+
+       optional<dcpomatic::Screen> output;
+
+       statement.execute([this, &output, screen_id](SQLiteStatement& statement) {
+               DCPOMATIC_ASSERT(statement.data_count() == 6);
+               output = screen_from_result(statement, screen_id);
+       });
+
+       return output;
+}
+
+
+
+vector<pair<ScreenID, dcpomatic::Screen>>
+CinemaList::screens_from_result(SQLiteStatement& statement) const
+{
+       vector<pair<ScreenID, dcpomatic::Screen>> output;
+
+       statement.execute([this, &output](SQLiteStatement& statement) {
+               DCPOMATIC_ASSERT(statement.data_count() == 6);
+               ScreenID const screen_id = statement.column_int64(0);
+               output.push_back({screen_id, screen_from_result(statement, screen_id)});
+       });
+
+       return output;
+}
+
+
+vector<pair<ScreenID, dcpomatic::Screen>>
+CinemaList::screens(CinemaID cinema_id) const
+{
+       SQLiteStatement statement(_db, _screens.select("WHERE cinema=?"));
+       statement.bind_int64(1, cinema_id.get());
+       return screens_from_result(statement);
+}
+
+
+vector<pair<ScreenID, dcpomatic::Screen>>
+CinemaList::screens_by_cinema_and_name(CinemaID id, std::string const& name) const
+{
+       SQLiteStatement statement(_db, _screens.select("WHERE cinema=? AND name=?"));
+       statement.bind_int64(1, id.get());
+       statement.bind_text(2, name);
+       return screens_from_result(statement);
+}
+
+
+optional<std::pair<CinemaID, Cinema>>
+CinemaList::cinema_by_name_or_email(std::string const& text) const
+{
+       SQLiteStatement statement(_db, _cinemas.select("WHERE name LIKE ? OR EMAILS LIKE ?"));
+       auto const wildcard = string("%") + text + "%";
+       statement.bind_text(1, wildcard);
+       statement.bind_text(2, wildcard);
+
+       auto all = cinemas_from_result(statement);
+       if (all.empty()) {
+               return {};
+       }
+       return all[0];
+}
+
+
+void
+CinemaList::update_screen(ScreenID id, dcpomatic::Screen const& screen)
+{
+       SQLiteStatement statement(_db, _screens.update("WHERE id=?"));
+
+       statement.bind_text(1, screen.name);
+       statement.bind_text(2, screen.notes);
+       statement.bind_text(3, screen.recipient->certificate(true));
+       statement.bind_text(4, screen.recipient_file.get_value_or(""));
+       statement.bind_int64(5, id.get());
+
+       statement.execute();
+}
+
+
+void
+CinemaList::remove_screen(ScreenID id)
+{
+       SQLiteStatement statement(_db, "DELETE FROM screens WHERE ID=?");
+       statement.bind_int64(1, id.get());
+       statement.execute();
+}
+
+
+optional<dcp::UTCOffset>
+CinemaList::unique_utc_offset(std::set<CinemaID> const& cinemas_to_check)
+{
+       optional<dcp::UTCOffset> offset;
+
+       for (auto const& cinema: cinemas()) {
+               if (cinemas_to_check.find(cinema.first) == cinemas_to_check.end()) {
+                       continue;
+               }
+
+               if (!offset) {
+                       offset = cinema.second.utc_offset;
+               } else if (cinema.second.utc_offset != *offset) {
+                       return dcp::UTCOffset();
+               }
+       }
+
+       return offset;
+}
+
diff --git a/src/lib/cinema_list.h b/src/lib/cinema_list.h
new file mode 100644 (file)
index 0000000..c91f294
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_CINEMA_LIST_H
+#define DCPOMATIC_CINEMA_LIST_H
+
+
+#include "id.h"
+#include "sqlite_table.h"
+#include <libcxml/cxml.h>
+#include <dcp/utc_offset.h>
+#include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
+#include <sqlite3.h>
+#include <set>
+
+
+class Cinema;
+namespace dcpomatic {
+       class Screen;
+}
+class SQLiteStatement;
+
+
+class CinemaID : public ID
+{
+public:
+       CinemaID(sqlite3_int64 id)
+               : ID(id) {}
+
+       bool operator<(CinemaID const& other) const {
+               return get() < other.get();
+       }
+};
+
+
+class ScreenID : public ID
+{
+public:
+       ScreenID(sqlite3_int64 id)
+               : ID(id) {}
+
+       bool operator==(ScreenID const& other) const {
+               return get() == other.get();
+       }
+
+       bool operator!=(ScreenID const& other) const {
+               return get() != other.get();
+       }
+
+       bool operator<(ScreenID const& other) const {
+               return get() < other.get();
+       }
+};
+
+
+class CinemaList
+{
+public:
+       CinemaList();
+       CinemaList(boost::filesystem::path db_file);
+       ~CinemaList();
+
+       CinemaList(CinemaList const&) = delete;
+       CinemaList& operator=(CinemaList const&) = delete;
+
+       CinemaList(CinemaList&& other);
+       CinemaList& operator=(CinemaList&& other);
+
+       void read_legacy_file(boost::filesystem::path xml_file);
+       void read_legacy_string(std::string const& xml);
+
+       void clear();
+
+       CinemaID add_cinema(Cinema const& cinema);
+       void update_cinema(CinemaID id, Cinema const& cinema);
+       void remove_cinema(CinemaID id);
+       std::vector<std::pair<CinemaID, Cinema>> cinemas() const;
+       boost::optional<Cinema> cinema(CinemaID id) const;
+       boost::optional<std::pair<CinemaID, Cinema>> cinema_by_partial_name(std::string const& text) const;
+       boost::optional<std::pair<CinemaID, Cinema>> cinema_by_name_or_email(std::string const& text) const;
+
+       ScreenID add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen);
+       void update_screen(ScreenID id, dcpomatic::Screen const& screen);
+       void remove_screen(ScreenID id);
+       boost::optional<dcpomatic::Screen> screen(ScreenID screen_id) const;
+       std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens(CinemaID cinema_id) const;
+       std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens_by_cinema_and_name(CinemaID id, std::string const& name) const;
+
+       boost::optional<dcp::UTCOffset> unique_utc_offset(std::set<CinemaID> const& cinemas);
+
+private:
+       dcpomatic::Screen screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const;
+       std::vector<std::pair<ScreenID, dcpomatic::Screen>> screens_from_result(SQLiteStatement& statement) const;
+       void setup_tables();
+       void setup(boost::filesystem::path db_file);
+       void read_legacy_document(cxml::Document const& doc);
+
+       sqlite3* _db = nullptr;
+       SQLiteTable _cinemas;
+       SQLiteTable _screens;
+       SQLiteTable _trusted_devices;
+};
+
+
+
+#endif
+
index 33b1a865659063042ca06d770b5dfca7d24290a1..c2c2cc24457e30c86858992d3360ecb0873bbe05 100644 (file)
 */
 
 
-#include "cinema.h"
+#include "cinema_list.h"
 #include "colour_conversion.h"
 #include "compose.hpp"
 #include "config.h"
 #include "constants.h"
 #include "cross.h"
 #include "dcp_content_type.h"
-#include "dkdm_recipient.h"
+#include "dkdm_recipient_list.h"
 #include "dkdm_wrapper.h"
 #include "film.h"
 #include "filter.h"
@@ -139,8 +139,8 @@ Config::set_defaults ()
        /* At the moment we don't write these files anywhere new after a version change, so they will be read from
         * ~/.config/dcpomatic2 (or equivalent) and written back there.
         */
-       _cinemas_file = read_path ("cinemas.xml");
-       _dkdm_recipients_file = read_path ("dkdm_recipients.xml");
+       _cinemas_file = read_path("cinemas.sqlite3");
+       _dkdm_recipients_file = read_path("dkdm_recipients.sqlite3");
        _show_hints_before_make_dcp = true;
        _confirm_kdm_email = true;
        _kdm_container_name_format = dcp::NameFormat("KDM_%f_%c");
@@ -276,7 +276,7 @@ Config::backup ()
                copy_file(path_to_copy, add_number(path_to_copy, n), ec);
        };
 
-       /* Make a backup copy of any config.xml, cinemas.xml, dkdm_recipients.xml that we might be about
+       /* Make a backup copy of any config.xml, cinemas.sqlite3, dkdm_recipients.sqlite3 that we might be about
         * to write over.  This is more intended for the situation where we have a corrupted config.xml,
         * and decide to overwrite it with a new one (possibly losing important details in the corrupted
         * file).  But we might as well back up the other files while we're about it.
@@ -296,15 +296,6 @@ Config::backup ()
 
 void
 Config::read ()
-{
-       read_config();
-       read_cinemas();
-       read_dkdm_recipients();
-}
-
-
-void
-Config::read_config()
 try
 {
        cxml::Document f ("Config");
@@ -406,11 +397,6 @@ try
 
        _default_kdm_directory = f.optional_string_child("DefaultKDMDirectory");
 
-       /* Read any cinemas that are still lying around in the config file
-        * from an old version.
-        */
-       read_cinemas (f);
-
        _mail_server = f.string_child ("MailServer");
        _mail_port = f.optional_number_child<int> ("MailPort").get_value_or (25);
 
@@ -548,8 +534,8 @@ try
                        _dkdms->add (DKDMBase::read (i));
                }
        }
-       _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.xml").string());
-       _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.xml").string());
+       _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.sqlite3").string());
+       _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.sqlite3").string());
        _show_hints_before_make_dcp = f.optional_bool_child("ShowHintsBeforeMakeDCP").get_value_or (true);
        _confirm_kdm_email = f.optional_bool_child("ConfirmKDMEmail").get_value_or (true);
        _kdm_container_name_format = dcp::NameFormat (f.optional_string_child("KDMContainerNameFormat").get_value_or ("KDM %f %c"));
@@ -688,40 +674,6 @@ catch (...) {
 }
 
 
-void
-Config::read_cinemas()
-{
-       if (dcp::filesystem::exists(_cinemas_file)) {
-               try {
-                       cxml::Document f("Cinemas");
-                       f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
-                       read_cinemas(f);
-               } catch (...) {
-                       backup();
-                       FailedToLoad(LoadFailure::CINEMAS);
-                       write_cinemas();
-               }
-       }
-}
-
-
-void
-Config::read_dkdm_recipients()
-{
-       if (dcp::filesystem::exists(_dkdm_recipients_file)) {
-               try {
-                       cxml::Document f("DKDMRecipients");
-                       f.read_file(dcp::filesystem::fix_long_path(_dkdm_recipients_file));
-                       read_dkdm_recipients(f);
-               } catch (...) {
-                       backup();
-                       FailedToLoad(LoadFailure::DKDM_RECIPIENTS);
-                       write_dkdm_recipients();
-               }
-       }
-}
-
-
 /** @return Singleton instance */
 Config *
 Config::instance ()
@@ -729,6 +681,30 @@ Config::instance ()
        if (_instance == nullptr) {
                _instance = new Config;
                _instance->read ();
+
+               auto cinemas_file = _instance->cinemas_file();
+               if (cinemas_file.extension() == ".xml") {
+                       auto sqlite = cinemas_file;
+                       sqlite.replace_extension(".sqlite3");
+
+                       if (dcp::filesystem::exists(cinemas_file) && !dcp::filesystem::exists(sqlite)) {
+                               _instance->set_cinemas_file(sqlite);
+                               CinemaList cinemas;
+                               cinemas.read_legacy_file(cinemas_file);
+                       }
+               }
+
+               auto dkdm_recipients_file = _instance->dkdm_recipients_file();
+               if (dkdm_recipients_file.extension() == ".xml") {
+                       auto sqlite = dkdm_recipients_file;
+                       sqlite.replace_extension(".sqlite3");
+
+                       if (dcp::filesystem::exists(dkdm_recipients_file) && !dcp::filesystem::exists(sqlite)) {
+                               _instance->set_dkdm_recipients_file(sqlite);
+                               DKDMRecipientList recipients;
+                               recipients.read_legacy_file(dkdm_recipients_file);
+                       }
+               }
        }
 
        return _instance;
@@ -739,8 +715,6 @@ void
 Config::write () const
 {
        write_config ();
-       write_cinemas ();
-       write_dkdm_recipients ();
 }
 
 void
@@ -1222,20 +1196,6 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t
 }
 
 
-void
-Config::write_cinemas () const
-{
-       write_file ("Cinemas", "Cinema", "1", _cinemas, _cinemas_file);
-}
-
-
-void
-Config::write_dkdm_recipients () const
-{
-       write_file ("DKDMRecipients", "DKDMRecipient", "1", _dkdm_recipients, _dkdm_recipients_file);
-}
-
-
 boost::filesystem::path
 Config::default_directory_or (boost::filesystem::path a) const
 {
@@ -1396,20 +1356,6 @@ Config::have_existing (string file)
 }
 
 
-void
-Config::read_cinemas (cxml::Document const & f)
-{
-       _cinemas.clear ();
-       for (auto i: f.node_children("Cinema")) {
-               /* Slightly grotty two-part construction of Cinema here so that we can use
-                  shared_from_this.
-               */
-               auto cinema = make_shared<Cinema>(i);
-               cinema->read_screens (i);
-               _cinemas.push_back (cinema);
-       }
-}
-
 void
 Config::set_cinemas_file (boost::filesystem::path file)
 {
@@ -1419,25 +1365,20 @@ Config::set_cinemas_file (boost::filesystem::path file)
 
        _cinemas_file = file;
 
-       if (dcp::filesystem::exists(_cinemas_file)) {
-               /* Existing file; read it in */
-               cxml::Document f ("Cinemas");
-               f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
-               read_cinemas (f);
-       }
-
-       changed (CINEMAS);
        changed (OTHER);
 }
 
 
 void
-Config::read_dkdm_recipients (cxml::Document const & f)
+Config::set_dkdm_recipients_file(boost::filesystem::path file)
 {
-       _dkdm_recipients.clear ();
-       for (auto i: f.node_children("DKDMRecipient")) {
-               _dkdm_recipients.push_back (make_shared<DKDMRecipient>(i));
+       if (file == _dkdm_recipients_file) {
+               return;
        }
+
+       _dkdm_recipients_file = file;
+
+       changed(OTHER);
 }
 
 
@@ -1670,10 +1611,10 @@ save_all_config_as_zip (boost::filesystem::path zip_file)
        auto config = Config::instance();
        zipper.add ("config.xml", dcp::file_to_string(config->config_read_file()));
        if (dcp::filesystem::exists(config->cinemas_file())) {
-               zipper.add ("cinemas.xml", dcp::file_to_string(config->cinemas_file()));
+               zipper.add("cinemas.sqlite3", dcp::file_to_string(config->cinemas_file()));
        }
        if (dcp::filesystem::exists(config->dkdm_recipients_file())) {
-               zipper.add ("dkdm_recipients.xml", dcp::file_to_string(config->dkdm_recipients_file()));
+               zipper.add("dkdm_recipients.sqlite3", dcp::file_to_string(config->dkdm_recipients_file()));
        }
 
        zipper.close ();
@@ -1681,22 +1622,58 @@ save_all_config_as_zip (boost::filesystem::path zip_file)
 
 
 void
-Config::load_from_zip(boost::filesystem::path zip_file)
+Config::load_from_zip(boost::filesystem::path zip_file, CinemasAction action)
 {
+       backup();
+
+       auto const current_cinemas = cinemas_file();
+       /* This is (unfortunately) a full path, and the user can't change it, so
+        * we always want to use that same path in the future no matter what is in the
+        * config.xml that we are about to load.
+        */
+       auto const current_dkdm_recipients = dkdm_recipients_file();
+
        Unzipper unzipper(zip_file);
        dcp::write_string_to_file(unzipper.get("config.xml"), config_write_file());
 
-       try {
-               dcp::write_string_to_file(unzipper.get("cinemas.xml"), cinemas_file());
-               dcp::write_string_to_file(unzipper.get("dkdm_recipient.xml"), dkdm_recipients_file());
-       } catch (std::runtime_error&) {}
+       if (action == CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) {
+               /* Read the zipped config, so that the cinemas file path is the new one and
+                * we write the cinemas to it.
+                */
+               read();
+               boost::filesystem::create_directories(cinemas_file().parent_path());
+               set_dkdm_recipients_file(current_dkdm_recipients);
+       }
+
+       if (unzipper.contains("cinemas.xml") && action != CinemasAction::IGNORE) {
+               CinemaList cinemas;
+               cinemas.clear();
+               cinemas.read_legacy_string(unzipper.get("cinemas.xml"));
+       }
+
+       if (unzipper.contains("dkdm_recipients.xml")) {
+               DKDMRecipientList recipients;
+               recipients.clear();
+               recipients.read_legacy_string(unzipper.get("dkdm_recipients.xml"));
+       }
 
-       read();
+       if (unzipper.contains("cinemas.sqlite3") && action != CinemasAction::IGNORE) {
+               dcp::write_string_to_file(unzipper.get("cinemas.sqlite3"), cinemas_file());
+       }
+
+       if (unzipper.contains("dkdm_recipients.sqlite3")) {
+               dcp::write_string_to_file(unzipper.get("dkdm_recipients.sqlite3"), dkdm_recipients_file());
+       }
+
+       if (action != CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) {
+               /* Read the zipped config, then reset the cinemas file to be the old one */
+               read();
+               set_cinemas_file(current_cinemas);
+               set_dkdm_recipients_file(current_dkdm_recipients);
+       }
 
        changed(Property::USE_ANY_SERVERS);
        changed(Property::SERVERS);
-       changed(Property::CINEMAS);
-       changed(Property::DKDM_RECIPIENTS);
        changed(Property::SOUND);
        changed(Property::SOUND_OUTPUT);
        changed(Property::PLAYER_CONTENT_DIRECTORY);
@@ -1733,6 +1710,25 @@ Config::initial_path(string id) const
 }
 
 
+bool
+Config::zip_contains_cinemas(boost::filesystem::path zip)
+{
+       Unzipper unzipper(zip);
+       return unzipper.contains("cinemas.sqlite3") || unzipper.contains("cinemas.xml");
+}
+
+
+boost::filesystem::path
+Config::cinemas_file_from_zip(boost::filesystem::path zip)
+{
+       Unzipper unzipper(zip);
+       DCPOMATIC_ASSERT(unzipper.contains("config.xml"));
+       cxml::Document document("Config");
+       document.read_string(unzipper.get("config.xml"));
+       return document.string_child("CinemasFile");
+}
+
+
 #ifdef DCPOMATIC_GROK
 
 Config::Grok::Grok(cxml::ConstNodePtr node)
index a7b238c04099c0c5dec736cb3d298325254350ec..67c78462078e6a055c4aa27b64469db5cc5adfac 100644 (file)
@@ -50,6 +50,8 @@ class DKDMRecipient;
 class Film;
 class Ratio;
 
+#undef IGNORE
+
 
 extern void save_all_config_as_zip (boost::filesystem::path zip_file);
 
@@ -81,13 +83,28 @@ public:
        boost::filesystem::path default_directory_or (boost::filesystem::path a) const;
        boost::filesystem::path default_kdm_directory_or (boost::filesystem::path a) const;
 
-       void load_from_zip(boost::filesystem::path zip_file);
+       enum class CinemasAction
+       {
+               /** Copy the cinemas.{xml,sqlite3} in the ZIP file to the path
+                *  specified in the current config, overwriting whatever is there,
+                *  and use that path.
+                */
+               WRITE_TO_CURRENT_PATH,
+               /** Copy the cinemas.{xml,sqlite3} in the ZIP file over the path
+                *  specified in the config.xml from the ZIP, overwriting whatever
+                *  is there and creating any required directories, and use
+                *  that path.
+                */
+               WRITE_TO_PATH_IN_ZIPPED_CONFIG,
+               /** Do nothing with the cinemas.{xml,sqlite3} in the ZIP file */
+               IGNORE
+       };
+
+       void load_from_zip(boost::filesystem::path zip_file, CinemasAction action);
 
        enum Property {
                USE_ANY_SERVERS,
                SERVERS,
-               CINEMAS,
-               DKDM_RECIPIENTS,
                SOUND,
                SOUND_OUTPUT,
                PLAYER_CONTENT_DIRECTORY,
@@ -163,14 +180,6 @@ public:
                return _tms_password;
        }
 
-       std::list<std::shared_ptr<Cinema>> cinemas () const {
-               return _cinemas;
-       }
-
-       std::list<std::shared_ptr<DKDMRecipient>> dkdm_recipients () const {
-               return _dkdm_recipients;
-       }
-
        std::list<int> allowed_dcp_frame_rates () const {
                return _allowed_dcp_frame_rates;
        }
@@ -716,26 +725,6 @@ public:
                maybe_set (_tms_password, p);
        }
 
-       void add_cinema (std::shared_ptr<Cinema> c) {
-               _cinemas.push_back (c);
-               changed (CINEMAS);
-       }
-
-       void remove_cinema (std::shared_ptr<Cinema> c) {
-               _cinemas.remove (c);
-               changed (CINEMAS);
-       }
-
-       void add_dkdm_recipient (std::shared_ptr<DKDMRecipient> c) {
-               _dkdm_recipients.push_back (c);
-               changed (DKDM_RECIPIENTS);
-       }
-
-       void remove_dkdm_recipient (std::shared_ptr<DKDMRecipient> c) {
-               _dkdm_recipients.remove (c);
-               changed (DKDM_RECIPIENTS);
-       }
-
        void set_allowed_dcp_frame_rates (std::list<int> const & r) {
                maybe_set (_allowed_dcp_frame_rates, r);
        }
@@ -965,6 +954,8 @@ public:
 
        void set_cinemas_file (boost::filesystem::path file);
 
+       void set_dkdm_recipients_file(boost::filesystem::path file);
+
        void set_show_hints_before_make_dcp (bool s) {
                maybe_set (_show_hints_before_make_dcp, s);
        }
@@ -1254,8 +1245,6 @@ public:
        */
        enum class LoadFailure {
                CONFIG,
-               CINEMAS,
-               DKDM_RECIPIENTS
        };
        static boost::signals2::signal<void (LoadFailure)> FailedToLoad;
        /** Emitted if read() issued a warning which the user might want to know about */
@@ -1275,8 +1264,6 @@ public:
 
        void write () const override;
        void write_config () const;
-       void write_cinemas () const;
-       void write_dkdm_recipients () const;
        void link (boost::filesystem::path new_file) const;
        void copy_and_link (boost::filesystem::path new_file) const;
        bool have_write_permission () const;
@@ -1297,6 +1284,9 @@ public:
        static bool have_existing (std::string);
        static boost::filesystem::path config_read_file ();
        static boost::filesystem::path config_write_file ();
+       static bool zip_contains_cinemas(boost::filesystem::path zip);
+       static boost::filesystem::path cinemas_file_from_zip(boost::filesystem::path zip);
+
 
        template <class T>
        void maybe_set (T& member, T new_value, Property prop = OTHER) {
@@ -1319,15 +1309,10 @@ public:
 private:
        Config ();
        void read () override;
-       void read_config();
-       void read_cinemas();
-       void read_dkdm_recipients();
        void set_defaults ();
        void set_kdm_email_to_default ();
        void set_notification_email_to_default ();
        void set_cover_sheet_to_default ();
-       void read_cinemas (cxml::Document const & f);
-       void read_dkdm_recipients (cxml::Document const & f);
        std::shared_ptr<dcp::CertificateChain> create_certificate_chain ();
        boost::filesystem::path directory_or (boost::optional<boost::filesystem::path> dir, boost::filesystem::path a) const;
        void add_to_history_internal (std::vector<boost::filesystem::path>& h, boost::filesystem::path p);
@@ -1394,8 +1379,6 @@ private:
        */
        boost::optional<boost::filesystem::path> _default_kdm_directory;
        bool _upload_after_make_dcp;
-       std::list<std::shared_ptr<Cinema>> _cinemas;
-       std::list<std::shared_ptr<DKDMRecipient>> _dkdm_recipients;
        std::string _mail_server;
        int _mail_port;
        EmailProtocol _mail_protocol;
index 4e56a37c4627423a9d8fecd0d1daeaca51b7c3a0..83ba96de65be53fe6f34391a0912862fc7afc938 100644 (file)
@@ -19,6 +19,7 @@
 */
 
 
+#include "cinema_list.h"
 #include "config.h"
 #include "dkdm_recipient.h"
 #include "film.h"
@@ -33,36 +34,16 @@ using std::vector;
 using dcp::raw_convert;
 
 
-DKDMRecipient::DKDMRecipient (cxml::ConstNodePtr node)
-       : KDMRecipient (node)
-{
-       for (auto i: node->node_children("Email")) {
-               emails.push_back (i->content());
-       }
-}
-
-
-void
-DKDMRecipient::as_xml (xmlpp::Element* node) const
-{
-       KDMRecipient::as_xml (node);
-
-       for (auto i: emails) {
-               cxml::add_text_child(node, "Email", i);
-       }
-}
-
-
 KDMWithMetadataPtr
 kdm_for_dkdm_recipient (
        shared_ptr<const Film> film,
        boost::filesystem::path cpl,
-       shared_ptr<DKDMRecipient> recipient,
+       DKDMRecipient const& recipient,
        dcp::LocalTime valid_from,
        dcp::LocalTime valid_to
        )
 {
-       if (!recipient->recipient) {
+       if (!recipient.recipient) {
                return {};
        }
 
@@ -72,7 +53,7 @@ kdm_for_dkdm_recipient (
        }
 
        auto const decrypted_kdm = film->make_kdm(cpl, valid_from, valid_to);
-       auto const kdm = decrypted_kdm.encrypt(signer, recipient->recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
+       auto const kdm = decrypted_kdm.encrypt(signer, recipient.recipient.get(), {}, dcp::Formulation::MODIFIED_TRANSITIONAL_1, true, 0);
 
        dcp::NameFormat::Map name_values;
        name_values['f'] = kdm.content_title_text();
@@ -80,6 +61,6 @@ kdm_for_dkdm_recipient (
        name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
        name_values['i'] = kdm.cpl_id();
 
-       return make_shared<KDMWithMetadata>(name_values, nullptr, recipient->emails, kdm);
+       return make_shared<KDMWithMetadata>(name_values, CinemaID(0), recipient.emails, kdm);
 }
 
index 3317ae6f9ed62f943aed7c0d74b165aeafdb51e0..64da41cff7cad650e2d7cd12163d51ef3191c9b5 100644 (file)
@@ -41,10 +41,6 @@ public:
 
        }
 
-       explicit DKDMRecipient (cxml::ConstNodePtr);
-
-       void as_xml (xmlpp::Element *) const override;
-
        std::vector<std::string> emails;
 };
 
@@ -53,7 +49,7 @@ KDMWithMetadataPtr
 kdm_for_dkdm_recipient (
        std::shared_ptr<const Film> film,
        boost::filesystem::path cpl,
-       std::shared_ptr<DKDMRecipient> recipient,
+       DKDMRecipient const& recipient,
        dcp::LocalTime valid_from,
        dcp::LocalTime valid_to
        );
diff --git a/src/lib/dkdm_recipient_list.cc b/src/lib/dkdm_recipient_list.cc
new file mode 100644 (file)
index 0000000..3417933
--- /dev/null
@@ -0,0 +1,243 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "config.h"
+#include "dkdm_recipient.h"
+#include "dkdm_recipient_list.h"
+#include "sqlite_statement.h"
+#include "sqlite_transaction.h"
+#include "util.h"
+#include <boost/algorithm/string.hpp>
+
+
+using std::make_pair;
+using std::pair;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+DKDMRecipientList::DKDMRecipientList()
+       : _dkdm_recipients("dkdm_recipients")
+{
+       setup(Config::instance()->dkdm_recipients_file());
+}
+
+
+DKDMRecipientList::DKDMRecipientList(boost::filesystem::path db_file)
+       : _dkdm_recipients("dkdm_recipients")
+{
+       setup(db_file);
+}
+
+
+
+DKDMRecipientList::~DKDMRecipientList()
+{
+       if (_db) {
+               sqlite3_close(_db);
+       }
+}
+
+
+void
+DKDMRecipientList::read_legacy_file(boost::filesystem::path xml_file)
+{
+       cxml::Document doc("DKDMRecipients");
+       doc.read_file(xml_file);
+
+       read_legacy_document(doc);
+}
+
+
+void
+DKDMRecipientList::read_legacy_string(string const& xml)
+{
+       cxml::Document doc("DKDMRecipients");
+       doc.read_file(xml);
+
+       read_legacy_document(doc);
+}
+
+
+void
+DKDMRecipientList::read_legacy_document(cxml::Document const& doc)
+{
+       for (auto recipient_node: doc.node_children("DKDMRecipient")) {
+               vector<string> emails;
+               for (auto email_node: recipient_node->node_children("Email")) {
+                       emails.push_back(email_node->content());
+               }
+
+               optional<dcp::Certificate> certificate;
+               if (auto certificate_string = recipient_node->optional_string_child("Recipient")) {
+                       certificate = dcp::Certificate(*certificate_string);
+               }
+
+               DKDMRecipient recipient(
+                       recipient_node->string_child("Name"),
+                       recipient_node->string_child("Notes"),
+                       certificate,
+                       emails
+                       );
+
+               add_dkdm_recipient(recipient);
+       }
+}
+
+
+void
+DKDMRecipientList::setup(boost::filesystem::path db_file)
+{
+       _dkdm_recipients.add_column("name", "TEXT");
+       _dkdm_recipients.add_column("notes", "TEXT");
+       _dkdm_recipients.add_column("recipient", "TEXT");
+       _dkdm_recipients.add_column("emails", "TEXT");
+
+#ifdef DCPOMATIC_WINDOWS
+       auto rc = sqlite3_open16(db_file.c_str(), &_db);
+#else
+       auto rc = sqlite3_open(db_file.c_str(), &_db);
+#endif
+       if (rc != SQLITE_OK) {
+               throw FileError("Could not open SQLite database", db_file);
+       }
+
+       sqlite3_busy_timeout(_db, 500);
+
+       SQLiteStatement screens(_db, _dkdm_recipients.create());
+       screens.execute();
+}
+
+
+DKDMRecipientList::DKDMRecipientList(DKDMRecipientList&& other)
+       : _dkdm_recipients(std::move(other._dkdm_recipients))
+{
+       _db = other._db;
+       other._db = nullptr;
+}
+
+
+DKDMRecipientList&
+DKDMRecipientList::operator=(DKDMRecipientList&& other)
+{
+       if (this != &other) {
+               _db = other._db;
+               other._db = nullptr;
+       }
+       return *this;
+}
+
+
+DKDMRecipientID
+DKDMRecipientList::add_dkdm_recipient(DKDMRecipient const& dkdm_recipient)
+{
+       SQLiteStatement add_dkdm_recipient(_db, _dkdm_recipients.insert());
+
+       add_dkdm_recipient.bind_text(1, dkdm_recipient.name);
+       add_dkdm_recipient.bind_text(2, dkdm_recipient.notes);
+       add_dkdm_recipient.bind_text(3, dkdm_recipient.recipient ? dkdm_recipient.recipient->certificate(true) : "");
+       add_dkdm_recipient.bind_text(4, join_strings(dkdm_recipient.emails));
+
+       add_dkdm_recipient.execute();
+
+       return sqlite3_last_insert_rowid(_db);
+}
+
+
+void
+DKDMRecipientList::update_dkdm_recipient(DKDMRecipientID id, DKDMRecipient const& dkdm_recipient)
+{
+       SQLiteStatement add_dkdm_recipient(_db, _dkdm_recipients.update("WHERE id=?"));
+
+       add_dkdm_recipient.bind_text(1, dkdm_recipient.name);
+       add_dkdm_recipient.bind_text(2, dkdm_recipient.notes);
+       add_dkdm_recipient.bind_text(3, dkdm_recipient.recipient ? dkdm_recipient.recipient->certificate(true) : "");
+       add_dkdm_recipient.bind_text(4, join_strings(dkdm_recipient.emails));
+       add_dkdm_recipient.bind_int64(5, id.get());
+
+       add_dkdm_recipient.execute();
+}
+
+
+void
+DKDMRecipientList::remove_dkdm_recipient(DKDMRecipientID id)
+{
+       SQLiteStatement statement(_db, "DELETE FROM dkdm_recipients WHERE ID=?");
+       statement.bind_int64(1, id.get());
+       statement.execute();
+}
+
+
+static
+vector<pair<DKDMRecipientID, DKDMRecipient>>
+dkdm_recipients_from_result(SQLiteStatement& statement)
+{
+       vector<pair<DKDMRecipientID, DKDMRecipient>> output;
+
+       statement.execute([&output](SQLiteStatement& statement) {
+               DCPOMATIC_ASSERT(statement.data_count() == 5);
+               DKDMRecipientID const id = statement.column_int64(0);
+               auto const name = statement.column_text(1);
+               auto const notes = statement.column_text(2);
+               auto certificate_string = statement.column_text(3);
+               optional<dcp::Certificate> certificate = certificate_string.empty() ? optional<dcp::Certificate>() : dcp::Certificate(certificate_string);
+               auto const join_with_spaces = statement.column_text(4);
+               vector<string> emails;
+               boost::algorithm::split(emails, join_with_spaces, boost::is_any_of(" "));
+               output.push_back(make_pair(id, DKDMRecipient(name, notes, certificate, { emails })));
+       });
+
+       return output;
+}
+
+
+
+
+vector<std::pair<DKDMRecipientID, DKDMRecipient>>
+DKDMRecipientList::dkdm_recipients() const
+{
+       SQLiteStatement statement(_db, _dkdm_recipients.select("ORDER BY name ASC"));
+       return dkdm_recipients_from_result(statement);
+}
+
+
+boost::optional<DKDMRecipient>
+DKDMRecipientList::dkdm_recipient(DKDMRecipientID id) const
+{
+       SQLiteStatement statement(_db, _dkdm_recipients.select("WHERE id=?"));
+       statement.bind_int64(1, id.get());
+       auto result = dkdm_recipients_from_result(statement);
+       if (result.empty()) {
+               return {};
+       }
+       return result[0].second;
+}
+
+
+void
+DKDMRecipientList::clear()
+{
+       SQLiteStatement sql(_db, "DELETE FROM dkdm_recipients");
+       sql.execute();
+}
+
+
diff --git a/src/lib/dkdm_recipient_list.h b/src/lib/dkdm_recipient_list.h
new file mode 100644 (file)
index 0000000..fc4d84b
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_DKDM_RECIPIENT_LIST_H
+#define DCPOMATIC_DKDM_RECIPIENT_LIST_H
+
+
+#include "id.h"
+#include "sqlite_table.h"
+#include <libcxml/cxml.h>
+#include <boost/filesystem.hpp>
+#include <boost/optional.hpp>
+
+
+class DKDMRecipient;
+
+
+class DKDMRecipientID : public ID
+{
+public:
+       DKDMRecipientID(sqlite3_int64 id)
+               : ID(id) {}
+
+       bool operator==(DKDMRecipientID const& other) const {
+               return get() == other.get();
+       }
+
+       bool operator!=(DKDMRecipientID const& other) const {
+               return get() != other.get();
+       }
+
+       bool operator<(DKDMRecipientID const& other) const {
+               return get() < other.get();
+       }
+};
+
+
+class DKDMRecipientList
+{
+public:
+       DKDMRecipientList();
+       DKDMRecipientList(boost::filesystem::path db_file);
+       ~DKDMRecipientList();
+
+       DKDMRecipientList(DKDMRecipientList const&) = delete;
+       DKDMRecipientList& operator=(DKDMRecipientList const&) = delete;
+
+       DKDMRecipientList(DKDMRecipientList&& other);
+       DKDMRecipientList& operator=(DKDMRecipientList&& other);
+
+       void read_legacy_file(boost::filesystem::path xml_file);
+       void read_legacy_string(std::string const& xml);
+
+       void clear();
+
+       DKDMRecipientID add_dkdm_recipient(DKDMRecipient const& dkdm_recipient);
+       void update_dkdm_recipient(DKDMRecipientID id, DKDMRecipient const& dkdm_recipient);
+       void remove_dkdm_recipient(DKDMRecipientID id);
+       std::vector<std::pair<DKDMRecipientID, DKDMRecipient>> dkdm_recipients() const;
+       boost::optional<DKDMRecipient> dkdm_recipient(DKDMRecipientID id) const;
+
+private:
+       void setup(boost::filesystem::path db_file);
+       void read_legacy_document(cxml::Document const& doc);
+
+       sqlite3* _db = nullptr;
+       SQLiteTable _dkdm_recipients;
+};
+
+
+#endif
+
index 10231f59acaf24571c890361ec15f8094d33e3c6..e083926890fabf719a3bc48f4f7daf3102955d88 100644 (file)
@@ -34,6 +34,7 @@ extern "C" {
 }
 #include <boost/filesystem.hpp>
 #include <boost/optional.hpp>
+#include <sqlite3.h>
 #include <cstring>
 #include <stdexcept>
 
@@ -480,4 +481,58 @@ public:
 };
 
 
+class SQLError : public std::runtime_error
+{
+public:
+       SQLError(sqlite3* db, char const* s)
+               : std::runtime_error(get_message(db, s))
+       {
+               _filename = get_filename(db);
+       }
+
+       SQLError(sqlite3* db, int rc)
+               : std::runtime_error(get_message(db, rc))
+       {
+               _filename = get_filename(db);
+       }
+
+       SQLError(sqlite3* db, int rc, std::string doing)
+               : std::runtime_error(get_message(db, rc, doing))
+       {
+               _filename = get_filename(db);
+       }
+
+       boost::filesystem::path filename() const {
+               return _filename;
+       }
+
+private:
+       boost::filesystem::path get_filename(sqlite3* db)
+       {
+               if (auto filename = sqlite3_db_filename(db, "main")) {
+                       return filename;
+               }
+
+               return {};
+       }
+
+       std::string get_message(sqlite3* db, char const* s)
+       {
+               return String::compose("%1 (in %2)", s, get_filename(db));
+       }
+
+       std::string get_message(sqlite3* db, int rc)
+       {
+               return String::compose("%1 (in %2)", sqlite3_errstr(rc), get_filename(db));
+       }
+
+       std::string get_message(sqlite3* db, int rc, std::string doing)
+       {
+               return String::compose("%1 (while doing %2) (in %3)", sqlite3_errstr(rc), doing, get_filename(db));
+       }
+
+       boost::filesystem::path _filename;
+};
+
+
 #endif
diff --git a/src/lib/id.cc b/src/lib/id.cc
new file mode 100644 (file)
index 0000000..2891fb4
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "id.h"
+
+
+bool
+operator==(ID const& a, ID const& b)
+{
+       return a.get() == b.get();
+}
+
diff --git a/src/lib/id.h b/src/lib/id.h
new file mode 100644 (file)
index 0000000..ca47210
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_ID_H
+#define DCPOMATIC_ID_H
+
+
+#include <sqlite3.h>
+
+
+class ID
+{
+public:
+       sqlite3_int64 get() const {
+               return _id;
+       }
+
+protected:
+       ID(sqlite3_int64 id)
+       : _id(id) {}
+
+private:
+       sqlite3_int64 _id;
+};
+
+
+bool operator==(ID const& a, ID const& b);
+
+
+#endif
index 21e8c75d31c0e6cdd8b8b1890ab182b3bde7357a..9d5b54b9abd472cea1a1040374537c1ac363a7d6 100644 (file)
@@ -25,6 +25,7 @@
 
 
 #include "cinema.h"
+#include "cinema_list.h"
 #include "config.h"
 #include "dkdm_wrapper.h"
 #include "email.h"
@@ -43,6 +44,7 @@
 using std::dynamic_pointer_cast;
 using std::list;
 using std::make_shared;
+using std::pair;
 using std::runtime_error;
 using std::shared_ptr;
 using std::string;
@@ -175,32 +177,25 @@ write_files (
 }
 
 
-static
-shared_ptr<Cinema>
-find_cinema (string cinema_name)
+class ScreenDetails
 {
-       auto cinemas = Config::instance()->cinemas ();
-       auto i = cinemas.begin();
-       while (
-               i != cinemas.end() &&
-               (*i)->name != cinema_name &&
-               find ((*i)->emails.begin(), (*i)->emails.end(), cinema_name) == (*i)->emails.end()) {
-
-               ++i;
-       }
-
-       if (i == cinemas.end ()) {
-               throw KDMCLIError (String::compose("could not find cinema \"%1\"", cinema_name));
-       }
+public:
+       ScreenDetails(CinemaID const& cinema_id, Cinema const& cinema, Screen const& screen)
+               : cinema_id(cinema_id)
+               , cinema(cinema)
+               , screen(screen)
+       {}
 
-       return *i;
-}
+       CinemaID cinema_id;
+       Cinema cinema;
+       Screen screen;
+};
 
 
 static
 void
 from_film (
-       vector<shared_ptr<Screen>> screens,
+       vector<ScreenDetails> const& screens,
        boost::filesystem::path film_dir,
        bool verbose,
        boost::filesystem::path output,
@@ -241,11 +236,22 @@ from_film (
 
        try {
                list<KDMWithMetadataPtr> kdms;
-               for (auto i: screens) {
+               for (auto screen_details: screens) {
                        std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm = [film, cpl](dcp::LocalTime begin, dcp::LocalTime end) {
                                return film->make_kdm(cpl, begin, end);
                        };
-                       auto p = kdm_for_screen(make_kdm, i, valid_from, valid_to, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio, period_checks);
+                       auto p = kdm_for_screen(
+                               make_kdm,
+                               screen_details.cinema_id,
+                               screen_details.cinema,
+                               screen_details.screen,
+                               valid_from,
+                               valid_to,
+                               formulation,
+                               disable_forensic_marking_picture,
+                               disable_forensic_marking_audio,
+                               period_checks
+                               );
                        if (p) {
                                kdms.push_back (p);
                        }
@@ -350,7 +356,7 @@ kdm_from_dkdm (
 static
 void
 from_dkdm (
-       vector<shared_ptr<Screen>> screens,
+       vector<ScreenDetails> const& screens,
        dcp::DecryptedKDM dkdm,
        bool verbose,
        boost::filesystem::path output,
@@ -370,15 +376,15 @@ from_dkdm (
 
        try {
                list<KDMWithMetadataPtr> kdms;
-               for (auto i: screens) {
-                       if (!i->recipient) {
+               for (auto const& screen_details: screens) {
+                       if (!screen_details.screen.recipient) {
                                continue;
                        }
 
                        auto const kdm = kdm_from_dkdm(
                                                        dkdm,
-                                                       i->recipient.get(),
-                                                       i->trusted_device_thumbprints(),
+                                                       screen_details.screen.recipient.get(),
+                                                       screen_details.screen.trusted_device_thumbprints(),
                                                        valid_from,
                                                        valid_to,
                                                        formulation,
@@ -387,14 +393,14 @@ from_dkdm (
                                                        );
 
                        dcp::NameFormat::Map name_values;
-                       name_values['c'] = i->cinema ? i->cinema->name : "";
-                       name_values['s'] = i->name;
+                       name_values['c'] = screen_details.cinema.name;
+                       name_values['s'] = screen_details.screen.name;
                        name_values['f'] = kdm.content_title_text();
                        name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
                        name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
                        name_values['i'] = kdm.cpl_id();
 
-                       kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm));
+                       kdms.push_back(make_shared<KDMWithMetadata>(name_values, screen_details.cinema_id, screen_details.cinema.emails, kdm));
                }
                write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
                if (email) {
@@ -451,12 +457,15 @@ try
        boost::filesystem::path output = dcp::filesystem::current_path();
        auto container_name_format = Config::instance()->kdm_container_name_format();
        auto filename_format = Config::instance()->kdm_filename_format();
+       /* either a cinema name to search for, or the name of a cinema to associate with certificate */
        optional<string> cinema_name;
-       shared_ptr<Cinema> cinema;
+       /* either a screen name to search for, or the name of a screen to associate with certificate */
+       optional<string> screen_name;
+       /* a certificate that we will use to make up a temporary cinema and screen */
        optional<boost::filesystem::path> projector_certificate;
        optional<boost::filesystem::path> decryption_key;
-       optional<string> screen;
-       vector<shared_ptr<Screen>> screens;
+       /* trusted devices that we will use to make up a temporary cinema and screen */
+       vector<TrustedDevice> trusted_devices;
        optional<dcp::EncryptedKDM> dkdm;
        optional<dcp::LocalTime> valid_from;
        optional<dcp::LocalTime> valid_to;
@@ -562,27 +571,16 @@ try
                        verbose = true;
                        break;
                case 'c':
-                       /* This could be a cinema to search for in the configured list or the name of a cinema being
-                          built up on-the-fly in the option.  Cater for both possilibities here by storing the name
-                          (for lookup) and by creating a Cinema which the next Screen will be added to.
-                       */
                        cinema_name = optarg;
-                       cinema = make_shared<Cinema>(optarg, vector<string>(), "", dcp::UTCOffset());
                        break;
                case 'S':
-                       /* Similarly, this could be the name of a new (temporary) screen or the name of a screen
-                        * to search for.
-                        */
-                       screen = optarg;
+                       screen_name = optarg;
                        break;
                case 'C':
                        projector_certificate = optarg;
                        break;
                case 'T':
-                       /* A trusted device ends up in the last screen we made */
-                       if (!screens.empty ()) {
-                               screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
-                       }
+                       trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
                        break;
                case 'G':
                        decryption_key = optarg;
@@ -618,20 +616,21 @@ try
                Config::instance()->set_cinemas_file(*cinemas_file);
        }
 
+       /* If we've been given a certificate we can make up a temporary cinema and screen (not written to the
+        * database) to then use for making KDMs.
+        */
+       optional<Cinema> temp_cinema;
+       optional<Screen> temp_screen;
        if (projector_certificate) {
-               /* Make a new screen and add it to the current cinema */
+               temp_cinema = Cinema(cinema_name.get_value_or(""), {}, "", dcp::UTCOffset());
                dcp::CertificateChain chain(dcp::file_to_string(*projector_certificate));
-               auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>());
-               if (cinema) {
-                       cinema->add_screen(screen_to_add);
-               }
-               screens.push_back(screen_to_add);
+               temp_screen = Screen(screen_name.get_value_or(""), "", chain.leaf(), boost::none, trusted_devices);
        }
 
        if (command == "list-cinemas") {
-               auto cinemas = Config::instance()->cinemas ();
-               for (auto i: cinemas) {
-                       out (String::compose("%1 (%2)", i->name, Email::address_list(i->emails)));
+               CinemaList cinemas;
+               for (auto const& cinema: cinemas.cinemas()) {
+                       out(String::compose("%1 (%2)", cinema.second.name, Email::address_list(cinema.second.emails)));
                }
                return {};
        }
@@ -660,15 +659,32 @@ try
                throw KDMCLIError ("you must specify --valid-from");
        }
 
-       if (screens.empty()) {
+       if (optind >= argc) {
+               throw KDMCLIError ("no film, CPL ID or DKDM specified");
+       }
+
+       vector<ScreenDetails> screens;
+
+       if (!temp_cinema) {
                if (!cinema_name) {
-                       throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files");
+                       throw KDMCLIError("you must specify either a cinema or one or more screens using certificate files");
                }
 
-               screens = find_cinema (*cinema_name)->screens ();
-               if (screen) {
-                       screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end());
+               CinemaList cinema_list;
+               if (auto cinema = cinema_list.cinema_by_name_or_email(*cinema_name)) {
+                       if (screen_name) {
+                               for (auto screen: cinema_list.screens_by_cinema_and_name(cinema->first, *screen_name)) {
+                                       screens.push_back({cinema->first, cinema->second, screen.second});
+                               }
+                       } else {
+                               for (auto screen: cinema_list.screens(cinema->first)) {
+                                       screens.push_back({cinema->first, cinema->second, screen.second});
+                               }
+                       }
                }
+       } else {
+               DCPOMATIC_ASSERT(temp_screen);
+               screens.push_back({CinemaID(0), *temp_cinema, *temp_screen});
        }
 
        if (duration_string) {
index fbeeffbc13403614ed5c1a7439975b5c16edefd9..6198564b1402a3557e4b0bca07766dc923d77348 100644 (file)
@@ -23,6 +23,7 @@
 #define DCPOMATIC_KDM_WITH_METADATA_H
 
 
+#include "id.h"
 #include <dcp/encrypted_kdm.h>
 #include <dcp/name_format.h>
 
@@ -33,7 +34,7 @@ class Cinema;
 class KDMWithMetadata
 {
 public:
-       KDMWithMetadata(dcp::NameFormat::Map const& name_values, void const* group, std::vector<std::string> emails, dcp::EncryptedKDM kdm)
+       KDMWithMetadata(dcp::NameFormat::Map const& name_values, ID group, std::vector<std::string> emails, dcp::EncryptedKDM kdm)
                : _name_values (name_values)
                , _group (group)
                , _emails (emails)
@@ -54,7 +55,7 @@ public:
 
        boost::optional<std::string> get (char k) const;
 
-       void const* group () const {
+       ID group() const {
                return _group;
        }
 
@@ -64,7 +65,7 @@ public:
 
 private:
        dcp::NameFormat::Map _name_values;
-       void const* _group;
+       ID _group;
        std::vector<std::string> _emails;
        dcp::EncryptedKDM _kdm;
 };
index 38c474850d64c2efe10d7b5322a95728a7e66ba4..b77eb6b52aee89659f9f985375f95f9550359b58 100644 (file)
@@ -20,6 +20,7 @@
 
 
 #include "cinema.h"
+#include "cinema_list.h"
 #include "config.h"
 #include "film.h"
 #include "kdm_util.h"
@@ -39,29 +40,6 @@ using boost::optional;
 using namespace dcpomatic;
 
 
-Screen::Screen (cxml::ConstNodePtr node)
-       : KDMRecipient (node)
-{
-       for (auto i: node->node_children ("TrustedDevice")) {
-               if (boost::algorithm::starts_with(i->content(), "-----BEGIN CERTIFICATE-----")) {
-                       trusted_devices.push_back (TrustedDevice(dcp::Certificate(i->content())));
-               } else {
-                       trusted_devices.push_back (TrustedDevice(i->content()));
-               }
-       }
-}
-
-
-void
-Screen::as_xml (xmlpp::Element* parent) const
-{
-       KDMRecipient::as_xml (parent);
-       for (auto i: trusted_devices) {
-               cxml::add_text_child(parent, "TrustedDevice", i.as_string());
-       }
-}
-
-
 vector<string>
 Screen::trusted_device_thumbprints () const
 {
@@ -76,7 +54,9 @@ Screen::trusted_device_thumbprints () const
 KDMWithMetadataPtr
 kdm_for_screen (
        std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm,
-       shared_ptr<const dcpomatic::Screen> screen,
+       CinemaID cinema_id,
+       Cinema const& cinema,
+       Screen const& screen,
        dcp::LocalTime valid_from,
        dcp::LocalTime valid_to,
        dcp::Formulation formulation,
@@ -85,13 +65,11 @@ kdm_for_screen (
        vector<KDMCertificatePeriod>& period_checks
        )
 {
-       if (!screen->recipient) {
+       if (!screen.recipient) {
                return {};
        }
 
-       auto cinema = screen->cinema;
-
-       period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema ? cinema->name : "", screen->name, screen->recipient.get(), valid_from, valid_to));
+       period_checks.push_back(check_kdm_and_certificate_validity_periods(cinema.name, screen.name, screen.recipient.get(), valid_from, valid_to));
 
        auto signer = Config::instance()->signer_chain();
        if (!signer->valid()) {
@@ -99,21 +77,17 @@ kdm_for_screen (
        }
 
        auto kdm = make_kdm(valid_from, valid_to).encrypt(
-               signer, screen->recipient.get(), screen->trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio
+               signer, screen.recipient.get(), screen.trusted_device_thumbprints(), formulation, disable_forensic_marking_picture, disable_forensic_marking_audio
                );
 
        dcp::NameFormat::Map name_values;
-       if (cinema) {
-               name_values['c'] = cinema->name;
-       } else {
-               name_values['c'] = "";
-       }
-       name_values['s'] = screen->name;
+       name_values['c'] = cinema.name;
+       name_values['s'] = screen.name;
        name_values['f'] = kdm.content_title_text();
        name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
        name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
        name_values['i'] = kdm.cpl_id();
 
-       return make_shared<KDMWithMetadata>(name_values, cinema.get(), cinema ? cinema->emails : vector<string>(), kdm);
+       return make_shared<KDMWithMetadata>(name_values, cinema_id, cinema.emails, kdm);
 }
 
index 0a275aa34c9407e3403935e21c0c6dfb9639e91c..89ebc3ab4c1bc8ce8ca7e0da27ebc53a060fbd60 100644 (file)
@@ -23,6 +23,7 @@
 #define DCPOMATIC_SCREEN_H
 
 
+#include "cinema_list.h"
 #include "kdm_recipient.h"
 #include "kdm_util.h"
 #include "kdm_with_metadata.h"
@@ -63,12 +64,7 @@ public:
                , trusted_devices (trusted_devices_)
        {}
 
-       explicit Screen (cxml::ConstNodePtr);
-
-       void as_xml (xmlpp::Element *) const override;
        std::vector<std::string> trusted_device_thumbprints () const;
-
-       std::shared_ptr<Cinema> cinema;
        std::vector<TrustedDevice> trusted_devices;
 };
 
@@ -78,7 +74,9 @@ public:
 KDMWithMetadataPtr
 kdm_for_screen (
        std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm,
-       std::shared_ptr<const dcpomatic::Screen> screen,
+       CinemaID cinema_id,
+       Cinema const& cinema,
+       dcpomatic::Screen const& screen,
        dcp::LocalTime valid_from,
        dcp::LocalTime valid_to,
        dcp::Formulation formulation,
diff --git a/src/lib/sqlite_statement.cc b/src/lib/sqlite_statement.cc
new file mode 100644 (file)
index 0000000..b3ec1fb
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "exceptions.h"
+#include "sqlite_statement.h"
+
+
+using std::function;
+using std::string;
+
+
+SQLiteStatement::SQLiteStatement(sqlite3* db, string const& statement)
+       : _db(db)
+{
+#ifdef DCPOMATIC_HAVE_SQLITE3_PREPARE_V3
+       auto rc = sqlite3_prepare_v3(_db, statement.c_str(), -1, 0, &_stmt, nullptr);
+#else
+       auto rc = sqlite3_prepare_v2(_db, statement.c_str(), -1, &_stmt, nullptr);
+#endif
+       if (rc != SQLITE_OK) {
+               throw SQLError(_db, rc, statement);
+       }
+}
+
+
+SQLiteStatement::~SQLiteStatement()
+{
+       sqlite3_finalize(_stmt);
+}
+
+
+void
+SQLiteStatement::bind_text(int index, string const& value)
+{
+       auto rc = sqlite3_bind_text(_stmt, index, value.c_str(), -1, SQLITE_TRANSIENT);
+       if (rc != SQLITE_OK) {
+               throw SQLError(_db, rc);
+       }
+}
+
+
+void
+SQLiteStatement::bind_int64(int index, int64_t value)
+{
+       auto rc = sqlite3_bind_int64(_stmt, index, value);
+       if (rc != SQLITE_OK) {
+               throw SQLError(_db, rc);
+       }
+}
+
+
+void
+SQLiteStatement::execute(function<void(SQLiteStatement&)> row, function<void()> busy)
+{
+       while (true) {
+               auto const rc = sqlite3_step(_stmt);
+               switch (rc) {
+               case SQLITE_BUSY:
+                       busy();
+                       break;
+               case SQLITE_DONE:
+                       return;
+               case SQLITE_ROW:
+                       row(*this);
+                       break;
+               case SQLITE_ERROR:
+               case SQLITE_MISUSE:
+                       throw SQLError(_db, sqlite3_errmsg(_db));
+               }
+       }
+}
+
+
+int
+SQLiteStatement::data_count()
+{
+       return sqlite3_data_count(_stmt);
+}
+
+
+int64_t
+SQLiteStatement::column_int64(int index)
+{
+       return sqlite3_column_int64(_stmt, index);
+}
+
+
+string
+SQLiteStatement::column_text(int index)
+{
+       return reinterpret_cast<const char*>(sqlite3_column_text(_stmt, index));
+}
+
diff --git a/src/lib/sqlite_statement.h b/src/lib/sqlite_statement.h
new file mode 100644 (file)
index 0000000..3c2246e
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include <sqlite3.h>
+#include <functional>
+#include <string>
+
+
+class SQLiteStatement
+{
+public:
+       SQLiteStatement(sqlite3* db, std::string const& statement);
+       ~SQLiteStatement();
+
+       SQLiteStatement(SQLiteStatement const&) = delete;
+       SQLiteStatement& operator=(SQLiteStatement const&) = delete;
+
+       void bind_text(int index, std::string const& value);
+       void bind_int64(int index, int64_t value);
+
+       int64_t column_int64(int index);
+       std::string column_text(int index);
+
+       void execute(std::function<void(SQLiteStatement&)> row = std::function<void(SQLiteStatement& statement)>(), std::function<void()> busy = std::function<void()>());
+
+       int data_count();
+
+private:
+       sqlite3* _db;
+       sqlite3_stmt* _stmt;
+};
+
diff --git a/src/lib/sqlite_table.cc b/src/lib/sqlite_table.cc
new file mode 100644 (file)
index 0000000..f00fa6b
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcpomatic_assert.h"
+#include "compose.hpp"
+#include "sqlite_table.h"
+#include "util.h"
+
+
+using std::string;
+using std::vector;
+
+
+void
+SQLiteTable::add_column(string const& name, string const& type)
+{
+       _columns.push_back(name);
+       _types.push_back(type);
+}
+
+
+string
+SQLiteTable::create() const
+{
+       DCPOMATIC_ASSERT(!_columns.empty());
+       DCPOMATIC_ASSERT(_columns.size() == _types.size());
+       vector<string> columns(_columns.size());
+       for (size_t i = 0; i < _columns.size(); ++i) {
+               columns[i] = _columns[i] + " " + _types[i];
+       }
+       return String::compose("CREATE TABLE IF NOT EXISTS %1 (id INTEGER PRIMARY KEY, %2)", _name, join_strings(columns, ", "));
+}
+
+
+string
+SQLiteTable::insert() const
+{
+       DCPOMATIC_ASSERT(!_columns.empty());
+       vector<string> placeholders(_columns.size(), "?");
+       return String::compose("INSERT INTO %1 (%2) VALUES (%3)", _name, join_strings(_columns, ", "), join_strings(placeholders, ", "));
+}
+
+
+string
+SQLiteTable::update(string const& condition) const
+{
+       DCPOMATIC_ASSERT(!_columns.empty());
+       vector<string> placeholders(_columns.size());
+       for (size_t i = 0; i < _columns.size(); ++i) {
+               placeholders[i] = _columns[i] + "=?";
+       }
+
+       return String::compose("UPDATE %1 SET %2 %3", _name, join_strings(placeholders, ", "), condition);
+}
+
+
+string
+SQLiteTable::select(string const& condition) const
+{
+       return String::compose("SELECT id,%1 FROM %2 %3", join_strings(_columns, ","), _name, condition);
+}
diff --git a/src/lib/sqlite_table.h b/src/lib/sqlite_table.h
new file mode 100644 (file)
index 0000000..43c9491
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_SQLITE_TABLE_H
+#define DCPOMATIC_SQLITE_TABLE_H
+
+#include <string>
+#include <vector>
+
+
+class SQLiteTable
+{
+public:
+       SQLiteTable(std::string name)
+               : _name(std::move(name))
+       {}
+
+       SQLiteTable(SQLiteTable const&) = default;
+       SQLiteTable(SQLiteTable&&) = default;
+
+       void add_column(std::string const& name, std::string const& type);
+
+       std::string create() const;
+       std::string insert() const;
+       std::string update(std::string const& condition) const;
+       std::string select(std::string const& condition) const;
+
+private:
+       std::string _name;
+       std::vector<std::string> _columns;
+       std::vector<std::string> _types;
+};
+
+
+#endif
+
diff --git a/src/lib/sqlite_transaction.cc b/src/lib/sqlite_transaction.cc
new file mode 100644 (file)
index 0000000..239d850
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "sqlite_statement.h"
+#include "sqlite_transaction.h"
+
+
+SQLiteTransaction::SQLiteTransaction(sqlite3* db)
+       : _db(db)
+{
+       SQLiteStatement statement(_db, "BEGIN TRANSACTION");
+       statement.execute();
+}
+
+
+SQLiteTransaction::~SQLiteTransaction()
+{
+       if (_rollback) {
+               SQLiteStatement rollback(_db, "ROLLBACK");
+               rollback.execute();
+       }
+}
+
+
+void
+SQLiteTransaction::commit()
+{
+       SQLiteStatement commit(_db, "COMMIT");
+       commit.execute();
+       _rollback = false;
+}
+
diff --git a/src/lib/sqlite_transaction.h b/src/lib/sqlite_transaction.h
new file mode 100644 (file)
index 0000000..0f63192
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include <sqlite3.h>
+
+
+class SQLiteTransaction
+{
+public:
+       SQLiteTransaction(sqlite3* db);
+       ~SQLiteTransaction();
+
+       SQLiteTransaction(SQLiteTransaction const&) = delete;
+       SQLiteTransaction& operator=(SQLiteTransaction const&) = delete;
+
+       void commit();
+
+private:
+       sqlite3* _db;
+       bool _rollback = true;
+};
+
index f0170e7e0646fdca6d2536123c8c287c759e0dad..8d468f24fbfe729fffcee3a10db8c4511a41fe7d 100644 (file)
@@ -56,8 +56,20 @@ Unzipper::~Unzipper()
 }
 
 
+bool
+Unzipper::contains(string const& filename) const
+{
+       auto file = zip_fopen(_zip, filename.c_str(), 0);
+       bool exists = file != nullptr;
+       if (file) {
+               zip_fclose(file);
+       }
+       return exists;
+}
+
+
 string
-Unzipper::get(string const& filename)
+Unzipper::get(string const& filename) const
 {
        auto file = zip_fopen(_zip, filename.c_str(), 0);
        if (!file) {
index 7cab6e5f450442918fa9a82ebd0ecd4369448968..76b2fe45ad08582032930725f535ccb2377788f5 100644 (file)
@@ -33,7 +33,8 @@ public:
        Unzipper(Unzipper const&) = delete;
        Unzipper& operator=(Unzipper const&) = delete;
 
-       std::string get(std::string const& filename);
+       std::string get(std::string const& filename) const;
+       bool contains(std::string const& filename) const;
 
 private:
        struct zip* _zip;
index 2f5c1ce49f545149afc474a6d1707dcfc2b9e731..282011d650e0da3865b2e53e27184f7396347ec6 100644 (file)
@@ -89,10 +89,11 @@ LIBDCP_ENABLE_WARNINGS
 #include <dbghelp.h>
 #endif
 #include <signal.h>
+#include <climits>
 #include <iomanip>
 #include <iostream>
 #include <fstream>
-#include <climits>
+#include <numeric>
 #include <stdexcept>
 #ifdef DCPOMATIC_POSIX
 #include <execinfo.h>
@@ -121,9 +122,6 @@ using std::vector;
 using std::wstring;
 using boost::thread;
 using boost::optional;
-using boost::lexical_cast;
-using boost::bad_lexical_cast;
-using boost::scoped_array;
 using dcp::Size;
 using dcp::raw_convert;
 using dcp::locale_convert;
@@ -1151,6 +1149,7 @@ setup_grok_library_path()
 }
 #endif
 
+
 string
 screen_names_to_string(vector<string> names)
 {
@@ -1185,3 +1184,16 @@ report_problem()
        return String::compose(_("Please report this problem by using Help -> Report a problem or via email to %1"), variant::report_problem_email());
 }
 
+
+string
+join_strings(vector<string> const& in, string const& separator)
+{
+       if (in.empty()) {
+               return {};
+       }
+
+       return std::accumulate(std::next(in.begin()), in.end(), in.front(), [separator](string a, string b) {
+               return a + separator + b;
+       });
+}
+
index eac855befcff4043e8b8bbc9ddb133f50bdf5cdc..7c40c5ce873aced777590abab48677f1de11aad2 100644 (file)
@@ -101,6 +101,7 @@ extern void capture_ffmpeg_logs();
 #ifdef DCPOMATIC_GROK
 extern void setup_grok_library_path();
 #endif
+extern std::string join_strings(std::vector<std::string> const& in, std::string const& separator = " ");
 
 
 template <class T>
index a4758c737c4420ff727ff302d782a19ba5bab732..68b988eb3faffdf3e812d1f2a67e2ddc012d4abc 100644 (file)
@@ -49,7 +49,7 @@ sources = """
           text_decoder.cc
           case_insensitive_sorter.cc
           check_content_job.cc
-          cinema.cc
+          cinema_list.cc
           cinema_sound_processor.cc
           change_signaller.cc
           collator.cc
@@ -85,6 +85,7 @@ sources = """
           decoder_part.cc
           digester.cc
           dkdm_recipient.cc
+          dkdm_recipient_list.cc
           dkdm_wrapper.cc
           dolby_cp750.cc
           email.cc
@@ -126,6 +127,7 @@ sources = """
           frame_rate_change.cc
           guess_crop.cc
           hints.cc
+          id.cc
           internet.cc
           image.cc
           image_content.cc
@@ -184,6 +186,9 @@ sources = """
           state.cc
           spl.cc
           spl_entry.cc
+          sqlite_statement.cc
+          sqlite_table.cc
+          sqlite_transaction.cc
           string_log_entry.cc
           string_text_file.cc
           string_text_file_content.cc
@@ -239,7 +244,7 @@ def build(bld):
                  BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2 BOOST_REGEX
                  SAMPLERATE POSTPROC TIFF SSH DCP CXML GLIB LZMA XML++
                  CURL ZIP BZ2 FONTCONFIG PANGOMM CAIROMM XMLSEC SUB ICU NETTLE PNG JPEG LEQM_NRT
-                 LIBZ
+                 LIBZ SQLITE3
                  """
 
     if bld.env.TARGET_OSX:
index 1c43d8a07a3a23213399255e7e21ba162fa0e129..85fc96c3ee6b8219b21e18a79ee3d46ae113f0be 100644 (file)
@@ -42,6 +42,7 @@
 #include "wx/id.h"
 #include "wx/job_manager_view.h"
 #include "wx/kdm_dialog.h"
+#include "wx/load_config_from_zip_dialog.h"
 #include "wx/nag_dialog.h"
 #include "wx/paste_dialog.h"
 #include "wx/recreate_chain_dialog.h"
@@ -94,6 +95,7 @@
 #include "lib/subtitle_film_encoder.h"
 #include "lib/text_content.h"
 #include "lib/transcode_job.h"
+#include "lib/unzipper.h"
 #include "lib/update_checker.h"
 #include "lib/variant.h"
 #include "lib/version.h"
@@ -328,7 +330,7 @@ public:
 #endif
 
                _config_changed_connection = Config::instance()->Changed.connect(boost::bind(&DOMFrame::config_changed, this, _1));
-               config_changed (Config::OTHER);
+               config_changed(Config::OTHER);
 
                _analytics_message_connection = Analytics::instance()->Message.connect(boost::bind(&DOMFrame::analytics_message, this, _1, _2));
 
@@ -795,9 +797,22 @@ private:
        {
                FileDialog dialog(this, _("Specify ZIP file"), wxT("ZIP files (*.zip)|*.zip"), wxFD_OPEN, "Preferences");
 
-               if (dialog.show()) {
-                       Config::instance()->load_from_zip(dialog.path());
+               if (!dialog.show()) {
+                       return;
+               }
+
+               auto action = Config::CinemasAction::WRITE_TO_CURRENT_PATH;
+
+               if (Config::zip_contains_cinemas(dialog.path()) && Config::cinemas_file_from_zip(dialog.path()) != Config::instance()->cinemas_file()) {
+                       LoadConfigFromZIPDialog how(this, dialog.path());
+                       if (how.ShowModal() == wxID_CANCEL) {
+                               return;
+                       }
+
+                       action = how.action();
                }
+
+               Config::instance()->load_from_zip(dialog.path(), action);
        }
 
        void jobs_make_dcp ()
@@ -1466,48 +1481,19 @@ private:
                m->Append (help, _("&Help"));
        }
 
-       void config_changed (Config::Property what)
+       void config_changed(Config::Property what)
        {
                /* Instantly save any config changes when using the DCP-o-matic GUI */
-               switch (what) {
-               case Config::CINEMAS:
-                       try {
-                               Config::instance()->write_cinemas();
-                       } catch (exception& e) {
-                               error_dialog (
-                                       this,
-                                       wxString::Format (
-                                               _("Could not write to cinemas file at %s.  Your changes have not been saved."),
-                                               std_to_wx (Config::instance()->cinemas_file().string()).data()
-                                               )
-                                       );
-                       }
-                       break;
-               case Config::DKDM_RECIPIENTS:
-                       try {
-                               Config::instance()->write_dkdm_recipients();
-                       } catch (exception& e) {
-                               error_dialog (
-                                       this,
-                                       wxString::Format (
-                                               _("Could not write to DKDM recipients file at %s.  Your changes have not been saved."),
-                                               std_to_wx(Config::instance()->dkdm_recipients_file().string()).data()
-                                               )
-                                       );
-                       }
-                       break;
-               default:
-                       try {
-                               Config::instance()->write_config();
-                       } catch (exception& e) {
-                               error_dialog (
-                                       this,
-                                       wxString::Format (
-                                               _("Could not write to config file at %s.  Your changes have not been saved."),
-                                               std_to_wx (Config::instance()->cinemas_file().string()).data()
-                                               )
-                                       );
-                       }
+               try {
+                       Config::instance()->write_config();
+               } catch (exception& e) {
+                       error_dialog (
+                               this,
+                               wxString::Format (
+                                       _("Could not write to config file at %s.  Your changes have not been saved."),
+                                       std_to_wx (Config::instance()->cinemas_file().string()).data()
+                                       )
+                               );
                }
 
                for (int i = 0; i < _history_items; ++i) {
@@ -1548,6 +1534,8 @@ private:
                if (what == Config::GROK) {
                        setup_grok_library_path();
                }
+#else
+               LIBDCP_UNUSED(what);
 #endif
        }
 
index 0a228b93324cbf5fbfe0783609d4f140688bfc52..32e8dec084656bae1dd118b070b894e341d08455 100644 (file)
@@ -358,36 +358,24 @@ private:
        void config_changed (Config::Property what)
        {
                /* Instantly save any config changes when using the DCP-o-matic GUI */
-               if (what == Config::CINEMAS) {
-                       try {
-                               Config::instance()->write_cinemas();
-                       } catch (exception& e) {
-                               error_dialog (
-                                       this,
-                                       wxString::Format(
-                                               _("Could not write to cinemas file at %s.  Your changes have not been saved."),
-                                               std_to_wx (Config::instance()->cinemas_file().string()).data()
-                                               )
-                                       );
-                       }
-               } else {
-                       try {
-                               Config::instance()->write_config();
-                       } catch (exception& e) {
-                               error_dialog (
-                                       this,
-                                       wxString::Format(
-                                               _("Could not write to config file at %s.  Your changes have not been saved."),
-                                               std_to_wx (Config::instance()->cinemas_file().string()).data()
-                                               )
-                                       );
-                       }
+               try {
+                       Config::instance()->write_config();
+               } catch (exception& e) {
+                       error_dialog (
+                               this,
+                               wxString::Format(
+                                       _("Could not write to config file at %s.  Your changes have not been saved."),
+                                       std_to_wx (Config::instance()->cinemas_file().string()).data()
+                                       )
+                               );
                }
 
 #ifdef DCPOMATIC_GROK
                if (what == Config::GROK) {
                        setup_grok_library_path();
                }
+#else
+               LIBDCP_UNUSED(what);
 #endif
        }
 
index 6f8c3c71e577d7b2f1c9825ae6d479122db8ffcc..af16f57aac52a6e179229241ff7afb5947bd299a 100644 (file)
@@ -405,11 +405,15 @@ private:
                                return kdm;
                        };
 
+                       CinemaList cinemas;
+
                        for (auto i: _screens->screens()) {
 
                                auto kdm = kdm_for_screen(
                                        make_kdm,
-                                       i,
+                                       i.first,
+                                       *cinemas.cinema(i.first),
+                                       *cinemas.screen(i.second),
                                        _timing->from(),
                                        _timing->until(),
                                        _output->formulation(),
index a7999548039d0f4983a79eab14ca87027b20b194..e6d7c4be150ef6a43a65cba05c9805235fa0d97e 100644 (file)
@@ -98,7 +98,7 @@ def configure(conf):
 def build(bld):
     uselib =  'BOOST_THREAD BOOST_DATETIME DCP XMLSEC CXML XMLPP AVFORMAT AVFILTER AVCODEC '
     uselib += 'AVUTIL SWSCALE SWRESAMPLE POSTPROC CURL BOOST_FILESYSTEM SSH ZIP CAIROMM FONTCONFIG PANGOMM SUB '
-    uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG JPEG LEQM_NRT '
+    uselib += 'SNDFILE SAMPLERATE BOOST_REGEX ICU NETTLE RTAUDIO PNG JPEG LEQM_NRT SQLITE3 '
 
     if bld.env.ENABLE_DISK:
         if bld.env.TARGET_LINUX:
index e6f4eca5ea5c60f6b7f85e699550c020ef0d1a14..1a9c564d44789c9afff085f31f2b1ad344afdfb1 100644 (file)
@@ -205,8 +205,21 @@ KDMDialog::make_clicked ()
                        return film->make_kdm(_cpl->cpl(), begin, end);
                };
 
-               for (auto i: _screens->screens()) {
-                       auto p = kdm_for_screen(make_kdm, i, _timing->from(), _timing->until(), _output->formulation(), !_output->forensic_mark_video(), for_audio, period_checks);
+               CinemaList cinemas;
+
+               for (auto screen: _screens->screens()) {
+                       auto p = kdm_for_screen(
+                               make_kdm,
+                               screen.first,
+                               *cinemas.cinema(screen.first),
+                               *cinemas.screen(screen.second),
+                               _timing->from(),
+                               _timing->until(),
+                               _output->formulation(),
+                               !_output->forensic_mark_video(),
+                               for_audio,
+                               period_checks
+                               );
                        if (p) {
                                kdms.push_back (p);
                        }
diff --git a/src/wx/load_config_from_zip_dialog.cc b/src/wx/load_config_from_zip_dialog.cc
new file mode 100644 (file)
index 0000000..a7d573d
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "load_config_from_zip_dialog.h"
+#include "wx_util.h"
+#include "lib/config.h"
+#include "lib/unzipper.h"
+
+
+LoadConfigFromZIPDialog::LoadConfigFromZIPDialog(wxWindow* parent, boost::filesystem::path zip_file)
+       : TableDialog(parent, _("Load configuration from ZIP file"), 1, 0, true)
+{
+       _use_current = add(new wxRadioButton(this, wxID_ANY, _("Copy the cinemas in the ZIP file over the current list at")));
+       auto current_path = add(new wxStaticText(this, wxID_ANY, std_to_wx(Config::instance()->cinemas_file().string())));
+       auto current_path_font = current_path->GetFont();
+       current_path_font.SetFamily(wxFONTFAMILY_TELETYPE);
+       current_path->SetFont(current_path_font);
+
+       _use_zip = add(new wxRadioButton(this, wxID_ANY, _("Copy the cinemas in the ZIP file to the original location at")));
+       auto zip_path = add(new wxStaticText(this, wxID_ANY, std_to_wx(Config::cinemas_file_from_zip(zip_file).string())));
+       auto zip_path_font = zip_path->GetFont();
+       zip_path_font.SetFamily(wxFONTFAMILY_TELETYPE);
+       zip_path->SetFont(zip_path_font);
+
+       _ignore = add(new wxRadioButton(this, wxID_ANY, _("Do not use the cinemas in the ZIP file")));
+
+       layout();
+}
+
+
+Config::CinemasAction
+LoadConfigFromZIPDialog::action() const
+{
+       if (_use_current->GetValue()) {
+               return Config::CinemasAction::WRITE_TO_CURRENT_PATH;
+       } else if (_use_zip->GetValue()) {
+               return Config::CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG;
+       }
+
+       return Config::CinemasAction::IGNORE;
+}
diff --git a/src/wx/load_config_from_zip_dialog.h b/src/wx/load_config_from_zip_dialog.h
new file mode 100644 (file)
index 0000000..f5f4ec6
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "table_dialog.h"
+#include "lib/config.h"
+#include <boost/filesystem.hpp>
+
+
+class LoadConfigFromZIPDialog : public TableDialog
+{
+public:
+       LoadConfigFromZIPDialog(wxWindow* parent, boost::filesystem::path zip_file);
+
+       Config::CinemasAction action() const;
+
+private:
+       wxRadioButton* _use_current;
+       wxRadioButton* _use_zip;
+       wxRadioButton* _ignore;
+};
+
+
+
index 04ad0dd6e4911950fbfff26575d3dcd74126fd34..9596c3cfd740ebb67f45b1c2142e48adf9730714 100644 (file)
@@ -23,7 +23,7 @@
 #include "wx_util.h"
 #include "recipient_dialog.h"
 #include "dcpomatic_button.h"
-#include "lib/config.h"
+#include "lib/dkdm_recipient_list.h"
 #include <list>
 #include <iostream>
 
@@ -103,15 +103,15 @@ RecipientsPanel::setup_sensitivity ()
 
 
 void
-RecipientsPanel::add_recipient (shared_ptr<DKDMRecipient> r)
+RecipientsPanel::add_recipient(DKDMRecipientID id, DKDMRecipient const& recipient)
 {
        string const search = wx_to_std(_search->GetValue());
 
-       if (!search.empty() && !_collator.find(search, r->name)) {
+       if (!search.empty() && !_collator.find(search, recipient.name)) {
                return;
        }
 
-       _recipients[_targets->AppendItem(_root, std_to_wx(r->name))] = r;
+       _recipients.emplace(make_pair(_targets->AppendItem(_root, std_to_wx(recipient.name)), id));
 
        _targets->SortChildren (_root);
 }
@@ -122,9 +122,10 @@ RecipientsPanel::add_recipient_clicked ()
 {
        RecipientDialog dialog(GetParent(), _("Add recipient"));
        if (dialog.ShowModal() == wxID_OK) {
-               auto r = std::make_shared<DKDMRecipient>(dialog.name(), dialog.notes(), dialog.recipient(), dialog.emails());
-               Config::instance()->add_dkdm_recipient (r);
-               add_recipient (r);
+               auto recipient = DKDMRecipient(dialog.name(), dialog.notes(), dialog.recipient(), dialog.emails());
+               DKDMRecipientList recipient_list;
+               auto const id = recipient_list.add_dkdm_recipient(recipient);
+               add_recipient(id, recipient);
        }
 }
 
@@ -136,18 +137,28 @@ RecipientsPanel::edit_recipient_clicked ()
                return;
        }
 
-       auto c = *_selected.begin();
+       DKDMRecipientList recipients;
+       auto selection = *_selected.begin();
+       auto const recipient_id = selection.second;
+       auto recipient = recipients.dkdm_recipient(recipient_id);
+       DCPOMATIC_ASSERT(recipient);
 
        RecipientDialog dialog(
-               GetParent(), _("Edit recipient"), c.second->name, c.second->notes, c.second->emails, c.second->recipient
+               GetParent(),
+               _("Edit recipient"),
+               recipient->name,
+               recipient->notes,
+               recipient->emails,
+               recipient->recipient
                );
 
        if (dialog.ShowModal() == wxID_OK) {
-               c.second->name = dialog.name();
-               c.second->emails = dialog.emails();
-               c.second->notes = dialog.notes();
-               _targets->SetItemText(c.first, std_to_wx(dialog.name()));
-               Config::instance()->changed (Config::DKDM_RECIPIENTS);
+               recipient->name = dialog.name();
+               recipient->emails = dialog.emails();
+               recipient->notes = dialog.notes();
+               recipient->recipient = dialog.recipient();
+               recipients.update_dkdm_recipient(recipient_id, *recipient);
+               _targets->SetItemText(selection.first, std_to_wx(dialog.name()));
        }
 }
 
@@ -156,7 +167,8 @@ void
 RecipientsPanel::remove_recipient_clicked ()
 {
        for (auto const& i: _selected) {
-               Config::instance()->remove_dkdm_recipient (i.second);
+               DKDMRecipientList recipient_list;
+               recipient_list.remove_dkdm_recipient(i.second);
                _targets->Delete (i.first);
        }
 
@@ -164,19 +176,15 @@ RecipientsPanel::remove_recipient_clicked ()
 }
 
 
-list<shared_ptr<DKDMRecipient>>
+list<DKDMRecipient>
 RecipientsPanel::recipients () const
 {
-       list<shared_ptr<DKDMRecipient>> r;
-
-       for (auto const& i: _selected) {
-               r.push_back (i.second);
+       list<DKDMRecipient> all;
+       DKDMRecipientList recipients;
+       for (auto const& recipient: recipients.dkdm_recipients()) {
+               all.push_back(recipient.second);
        }
-
-       r.sort ();
-       r.unique ();
-
-       return r;
+       return all;
 }
 
 
@@ -202,7 +210,7 @@ RecipientsPanel::selection_changed ()
        for (size_t i = 0; i < s.GetCount(); ++i) {
                RecipientMap::const_iterator j = _recipients.find (s[i]);
                if (j != _recipients.end ()) {
-                       _selected[j->first] = j->second;
+                       _selected.emplace(*j);
                }
        }
 
@@ -216,8 +224,9 @@ RecipientsPanel::add_recipients ()
 {
        _root = _targets->AddRoot ("Foo");
 
-       for (auto i: Config::instance()->dkdm_recipients()) {
-               add_recipient (i);
+       DKDMRecipientList recipients;
+       for (auto const& recipient: recipients.dkdm_recipients()) {
+               add_recipient(recipient.first, recipient.second);
        }
 }
 
index 6e1f1408ffb6603dd5b821501f5918d1e4bd8dc3..d252b8d061cbb7bcd1775c955fcbbf61a726abe5 100644 (file)
@@ -21,6 +21,7 @@
 
 #include "lib/collator.h"
 #include "lib/dkdm_recipient.h"
+#include "lib/dkdm_recipient_list.h"
 #include <dcp/warnings.h>
 LIBDCP_DISABLE_WARNINGS
 #include <wx/srchctrl.h>
@@ -43,12 +44,12 @@ public:
 
        void setup_sensitivity ();
 
-       std::list<std::shared_ptr<DKDMRecipient>> recipients () const;
+       std::list<DKDMRecipient> recipients() const;
        boost::signals2::signal<void ()> RecipientsChanged;
 
 private:
        void add_recipients ();
-       void add_recipient (std::shared_ptr<DKDMRecipient>);
+       void add_recipient(DKDMRecipientID id, DKDMRecipient const& recipient);
        void add_recipient_clicked ();
        void edit_recipient_clicked ();
        void remove_recipient_clicked ();
@@ -63,7 +64,7 @@ private:
        wxButton* _remove_recipient;
        wxTreeItemId _root;
 
-       typedef std::map<wxTreeItemId, std::shared_ptr<DKDMRecipient>> RecipientMap;
+       typedef std::map<wxTreeItemId, DKDMRecipientID> RecipientMap;
        RecipientMap _recipients;
        RecipientMap _selected;
 
index 768d250923f7d302973981da9f53a1453d3013d3..fdc4dacc376c05fde986a9dd886f5de4b1f481c4 100644 (file)
@@ -126,8 +126,6 @@ ScreensPanel::ScreensPanel (wxWindow* parent)
        _uncheck_all->Bind   (wxEVT_BUTTON, boost::bind(&ScreensPanel::uncheck_all, this));
 
        SetSizer(_overall_sizer);
-
-       _config_connection = Config::instance()->Changed.connect(boost::bind(&ScreensPanel::config_changed, this, _1));
 }
 
 
@@ -189,28 +187,37 @@ ScreensPanel::convert_to_lower(string& s)
 
 
 bool
-ScreensPanel::matches_search(shared_ptr<const Cinema> cinema, string search)
+ScreensPanel::matches_search(Cinema const& cinema, string search)
 {
        if (search.empty()) {
                return true;
        }
 
-       return _collator.find(search, cinema->name);
+       return _collator.find(search, cinema.name);
 }
 
 
+/** Add an existing cinema to the GUI */
 optional<wxTreeListItem>
-ScreensPanel::add_cinema (shared_ptr<Cinema> cinema, wxTreeListItem previous)
+ScreensPanel::add_cinema(CinemaID cinema_id, wxTreeListItem previous)
 {
+       CinemaList cinemas;
+       auto cinema = cinemas.cinema(cinema_id);
+       DCPOMATIC_ASSERT(cinema);
+
        auto const search = wx_to_std(_search->GetValue());
-       if (!matches_search(cinema, search)) {
+       if (!matches_search(*cinema, search)) {
                return {};
        }
 
+       auto screens = cinemas.screens(cinema_id);
+
        if (_show_only_checked->get()) {
-               auto screens = cinema->screens();
-               auto iter = std::find_if(screens.begin(), screens.end(), [this](shared_ptr<dcpomatic::Screen> screen) {
-                       return _checked_screens.find(screen) != _checked_screens.end();
+               auto iter = std::find_if(screens.begin(), screens.end(), [this](pair<ScreenID, dcpomatic::Screen> const& screen) {
+                       auto iter2 = std::find_if(_checked_screens.begin(), _checked_screens.end(), [screen](pair<CinemaID, ScreenID> const& checked) {
+                               return checked.second == screen.first;
+                       });
+                       return iter2 != _checked_screens.end();
                });
                if (iter == screens.end()) {
                        return {};
@@ -219,29 +226,34 @@ ScreensPanel::add_cinema (shared_ptr<Cinema> cinema, wxTreeListItem previous)
 
        auto id = _targets->InsertItem(_targets->GetRootItem(), previous, std_to_wx(cinema->name));
 
-       _item_to_cinema[id] = cinema;
-       _cinema_to_item[cinema] = id;
+       _item_to_cinema.emplace(make_pair(id, cinema_id));
+       _cinema_to_item[cinema_id] = id;
 
-       for (auto screen: cinema->screens()) {
-               add_screen (cinema, screen);
+       for (auto screen: screens) {
+               add_screen(cinema_id, screen.first);
        }
 
        return id;
 }
 
 
+/** Add an existing screen to the GUI */
 optional<wxTreeListItem>
-ScreensPanel::add_screen (shared_ptr<Cinema> cinema, shared_ptr<Screen> screen)
+ScreensPanel::add_screen(CinemaID cinema_id, ScreenID screen_id)
 {
-       auto item = cinema_to_item(cinema);
+       auto item = cinema_to_item(cinema_id);
        if (!item) {
                return {};
        }
 
+       CinemaList cinemas;
+       auto screen = cinemas.screen(screen_id);
+       DCPOMATIC_ASSERT(screen);
+
        auto id = _targets->AppendItem(*item, std_to_wx(screen->name));
 
-       _item_to_screen[id] = screen;
-       _screen_to_item[screen] = id;
+       _item_to_screen.emplace(make_pair(id, make_pair(cinema_id, screen_id)));
+       _screen_to_item[screen_id] = id;
 
        return item;
 }
@@ -253,44 +265,31 @@ ScreensPanel::add_cinema_clicked ()
        CinemaDialog dialog(GetParent(), _("Add Cinema"));
 
        if (dialog.ShowModal() == wxID_OK) {
-               auto cinema = make_shared<Cinema>(dialog.name(), dialog.emails(), dialog.notes(), dialog.utc_offset());
+               auto cinema = Cinema(dialog.name(), dialog.emails(), dialog.notes(), dialog.utc_offset());
 
-               auto cinemas = sorted_cinemas();
+               CinemaList cinemas;
+               auto existing_cinemas = cinemas.cinemas();
 
-               try {
-                       _ignore_cinemas_changed = true;
-                       dcp::ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
-                       Config::instance()->add_cinema(cinema);
-               } catch (FileError& e) {
-                       error_dialog(
-                               GetParent(),
-                               variant::wx::insert_dcpomatic(
-                                       _("Could not write cinema details to the cinemas.xml file.  Check that the location of "
-                                         "cinemas.xml is valid in %s's preferences.")
-                                       ),
-                               std_to_wx(e.what())
-                               );
-                       return;
-               }
+               auto const cinema_id = cinemas.add_cinema(cinema);
 
                wxTreeListItem previous = wxTLI_FIRST;
                bool found = false;
                auto const search = wx_to_std(_search->GetValue());
-               for (auto existing_cinema: cinemas) {
-                       if (!matches_search(existing_cinema, search)) {
+               for (auto existing_cinema: existing_cinemas) {
+                       if (!matches_search(existing_cinema.second, search)) {
                                continue;
                        }
-                       if (_collator.compare(dialog.name(), existing_cinema->name) < 0) {
+                       if (_collator.compare(dialog.name(), existing_cinema.second.name) < 0) {
                                /* existing_cinema should be after the one we're inserting */
                                found = true;
                                break;
                        }
-                       auto item = cinema_to_item(existing_cinema);
+                       auto item = cinema_to_item(existing_cinema.first);
                        DCPOMATIC_ASSERT(item);
                        previous = *item;
                }
 
-               auto item = add_cinema(cinema, found ? previous : wxTLI_LAST);
+               auto item = add_cinema(cinema_id, found ? previous : wxTLI_LAST);
 
                if (item) {
                        _targets->UnselectAll ();
@@ -302,13 +301,13 @@ ScreensPanel::add_cinema_clicked ()
 }
 
 
-shared_ptr<Cinema>
+optional<CinemaID>
 ScreensPanel::cinema_for_operation () const
 {
        if (_selected_cinemas.size() == 1) {
                return _selected_cinemas[0];
        } else if (_selected_screens.size() == 1) {
-               return _selected_screens[0]->cinema;
+               return _selected_screens[0].first;
        }
 
        return {};
@@ -318,25 +317,29 @@ ScreensPanel::cinema_for_operation () const
 void
 ScreensPanel::edit_cinema_clicked ()
 {
-       auto cinema = cinema_for_operation ();
-       if (cinema) {
-               edit_cinema(cinema);
+       auto cinema_id = cinema_for_operation();
+       if (cinema_id) {
+               edit_cinema(*cinema_id);
        }
 }
 
 
 void
-ScreensPanel::edit_cinema(shared_ptr<Cinema> cinema)
+ScreensPanel::edit_cinema(CinemaID cinema_id)
 {
+       CinemaList cinemas;
+       auto cinema = cinemas.cinema(cinema_id);
+       DCPOMATIC_ASSERT(cinema);
+
        CinemaDialog dialog(GetParent(), _("Edit cinema"), cinema->name, cinema->emails, cinema->notes, cinema->utc_offset);
 
        if (dialog.ShowModal() == wxID_OK) {
-               cinema->name = dialog.name();
                cinema->emails = dialog.emails();
+               cinema->name = dialog.name();
                cinema->notes = dialog.notes();
                cinema->utc_offset = dialog.utc_offset();
-               notify_cinemas_changed();
-               auto item = cinema_to_item(cinema);
+               cinemas.update_cinema(cinema_id, *cinema);
+               auto item = cinema_to_item(cinema_id);
                DCPOMATIC_ASSERT(item);
                _targets->SetItemText (*item, std_to_wx(dialog.name()));
        }
@@ -346,8 +349,11 @@ ScreensPanel::edit_cinema(shared_ptr<Cinema> cinema)
 void
 ScreensPanel::remove_cinema_clicked ()
 {
+       CinemaList cinemas;
+
        if (_selected_cinemas.size() == 1) {
-               if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the cinema '%s'?"), std_to_wx(_selected_cinemas[0]->name)))) {
+               auto cinema = cinemas.cinema(_selected_cinemas[0]);
+               if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the cinema '%s'?"), std_to_wx(cinema->name)))) {
                        return;
                }
        } else {
@@ -358,14 +364,12 @@ ScreensPanel::remove_cinema_clicked ()
 
        auto cinemas_to_remove = _selected_cinemas;
 
-       for (auto const& cinema: cinemas_to_remove) {
-               _ignore_cinemas_changed = true;
-               dcp::ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
-               for (auto screen: cinema->screens()) {
-                       _checked_screens.erase(screen);
+       for (auto const& cinema_id: cinemas_to_remove) {
+               for (auto screen: cinemas.screens(cinema_id)) {
+                       _checked_screens.erase({cinema_id, screen.first});
                }
-               Config::instance()->remove_cinema(cinema);
-               auto item = cinema_to_item(cinema);
+               cinemas.remove_cinema(cinema_id);
+               auto item = cinema_to_item(cinema_id);
                DCPOMATIC_ASSERT(item);
                _targets->DeleteItem(*item);
        }
@@ -378,8 +382,8 @@ ScreensPanel::remove_cinema_clicked ()
 void
 ScreensPanel::add_screen_clicked ()
 {
-       auto cinema = cinema_for_operation ();
-       if (!cinema) {
+       auto cinema_id = cinema_for_operation();
+       if (!cinema_id) {
                return;
        }
 
@@ -389,8 +393,10 @@ ScreensPanel::add_screen_clicked ()
                return;
        }
 
-       for (auto screen: cinema->screens()) {
-               if (screen->name == dialog.name()) {
+       CinemaList cinemas;
+
+       for (auto screen: cinemas.screens(*cinema_id)) {
+               if (screen.second.name == dialog.name()) {
                        error_dialog (
                                GetParent(),
                                wxString::Format (
@@ -402,11 +408,10 @@ ScreensPanel::add_screen_clicked ()
                }
        }
 
-       auto screen = std::make_shared<Screen>(dialog.name(), dialog.notes(), dialog.recipient(), dialog.recipient_file(), dialog.trusted_devices());
-       cinema->add_screen (screen);
-       notify_cinemas_changed();
+       auto screen = Screen(dialog.name(), dialog.notes(), dialog.recipient(), dialog.recipient_file(), dialog.trusted_devices());
+       auto const screen_id = cinemas.add_screen(*cinema_id, screen);
 
-       auto id = add_screen (cinema, screen);
+       auto id = add_screen(*cinema_id, screen_id);
        if (id) {
                _targets->Expand (id.get ());
        }
@@ -417,30 +422,33 @@ void
 ScreensPanel::edit_screen_clicked ()
 {
        if (_selected_screens.size() == 1) {
-               edit_screen(_selected_screens[0]);
+               edit_screen(_selected_screens[0].first, _selected_screens[0].second);
        }
 }
 
 
 void
-ScreensPanel::edit_screen(shared_ptr<Screen> edit_screen)
+ScreensPanel::edit_screen(CinemaID cinema_id, ScreenID screen_id)
 {
+       CinemaList cinemas;
+       auto screen = cinemas.screen(screen_id);
+       DCPOMATIC_ASSERT(screen);
+
        ScreenDialog dialog(
                GetParent(), _("Edit screen"),
-               edit_screen->name,
-               edit_screen->notes,
-               edit_screen->recipient,
-               edit_screen->recipient_file,
-               edit_screen->trusted_devices
+               screen->name,
+               screen->notes,
+               screen->recipient,
+               screen->recipient_file,
+               screen->trusted_devices
                );
 
        if (dialog.ShowModal() != wxID_OK) {
                return;
        }
 
-       auto cinema = edit_screen->cinema;
-       for (auto screen: cinema->screens()) {
-               if (screen != edit_screen && screen->name == dialog.name()) {
+       for (auto screen: cinemas.screens(cinema_id)) {
+               if (screen.first != screen_id && screen.second.name == dialog.name()) {
                        error_dialog (
                                GetParent(),
                                wxString::Format (
@@ -452,14 +460,14 @@ ScreensPanel::edit_screen(shared_ptr<Screen> edit_screen)
                }
        }
 
-       edit_screen->name = dialog.name();
-       edit_screen->notes = dialog.notes();
-       edit_screen->recipient = dialog.recipient();
-       edit_screen->recipient_file = dialog.recipient_file();
-       edit_screen->trusted_devices = dialog.trusted_devices();
-       notify_cinemas_changed();
+       screen->name = dialog.name();
+       screen->notes = dialog.notes();
+       screen->recipient = dialog.recipient();
+       screen->recipient_file = dialog.recipient_file();
+       screen->trusted_devices = dialog.trusted_devices();
+       cinemas.update_screen(screen_id, *screen);
 
-       auto item = screen_to_item(edit_screen);
+       auto item = screen_to_item(screen_id);
        DCPOMATIC_ASSERT (item);
        _targets->SetItemText(*item, std_to_wx(dialog.name()));
 }
@@ -468,8 +476,12 @@ ScreensPanel::edit_screen(shared_ptr<Screen> edit_screen)
 void
 ScreensPanel::remove_screen_clicked ()
 {
+       CinemaList cinemas;
+
        if (_selected_screens.size() == 1) {
-               if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the screen '%s'?"), std_to_wx(_selected_screens[0]->name)))) {
+               auto screen = cinemas.screen(_selected_screens[0].second);
+               DCPOMATIC_ASSERT(screen);
+               if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the screen '%s'?"), std_to_wx(screen->name)))) {
                        return;
                }
        } else {
@@ -478,10 +490,10 @@ ScreensPanel::remove_screen_clicked ()
                }
        }
 
-       for (auto screen: _selected_screens) {
-               _checked_screens.erase(screen);
-               screen->cinema->remove_screen(screen);
-               auto item = screen_to_item(screen);
+       for (auto screen_id: _selected_screens) {
+               _checked_screens.erase(screen_id);
+               cinemas.remove_screen(screen_id.second);
+               auto item = screen_to_item(screen_id.second);
                DCPOMATIC_ASSERT(item);
                _targets->DeleteItem(*item);
        }
@@ -490,17 +502,14 @@ ScreensPanel::remove_screen_clicked ()
         * as well.
         */
        selection_changed();
-       notify_cinemas_changed();
        setup_show_only_checked();
 }
 
 
-vector<shared_ptr<Screen>>
+std::set<pair<CinemaID, ScreenID>>
 ScreensPanel::screens () const
 {
-       vector<shared_ptr<Screen>> output;
-       std::copy (_checked_screens.begin(), _checked_screens.end(), std::back_inserter(output));
-       return output;
+       return _checked_screens;
 }
 
 
@@ -526,10 +535,10 @@ ScreensPanel::selection_changed ()
 
        for (size_t i = 0; i < selection.size(); ++i) {
                if (auto cinema = item_to_cinema(selection[i])) {
-                       _selected_cinemas.push_back(cinema);
+                       _selected_cinemas.push_back(*cinema);
                }
                if (auto screen = item_to_screen(selection[i])) {
-                       _selected_screens.push_back(screen);
+                       _selected_screens.push_back(*screen);
                }
        }
 
@@ -537,24 +546,12 @@ ScreensPanel::selection_changed ()
 }
 
 
-list<shared_ptr<Cinema>>
-ScreensPanel::sorted_cinemas() const
-{
-       auto cinemas = Config::instance()->cinemas();
-
-       cinemas.sort(
-               [this](shared_ptr<Cinema> a, shared_ptr<Cinema> b) { return _collator.compare(a->name, b->name) < 0; }
-               );
-
-       return cinemas;
-}
-
-
 void
 ScreensPanel::add_cinemas ()
 {
-       for (auto cinema: sorted_cinemas()) {
-               add_cinema (cinema, wxTLI_LAST);
+       CinemaList cinemas;
+       for (auto cinema: cinemas.cinemas()) {
+               add_cinema (cinema.first, wxTLI_LAST);
        }
 }
 
@@ -588,7 +585,7 @@ ScreensPanel::display_filter_changed()
        }
 
        for (auto const& selection: _selected_screens) {
-               if (auto item = screen_to_item(selection)) {
+               if (auto item = screen_to_item(selection.second)) {
                        _targets->Select (*item);
                }
        }
@@ -598,7 +595,7 @@ ScreensPanel::display_filter_changed()
        _ignore_check_change = true;
 
        for (auto const& checked: _checked_screens) {
-               if (auto item = screen_to_item(checked)) {
+               if (auto item = screen_to_item(checked.second)) {
                        _targets->CheckItem(*item, wxCHK_CHECKED);
                        setup_cinema_checked_state(*item);
                }
@@ -614,9 +611,9 @@ ScreensPanel::set_screen_checked (wxTreeListItem item, bool checked)
        auto screen = item_to_screen(item);
        DCPOMATIC_ASSERT(screen);
        if (checked) {
-               _checked_screens.insert(screen);
+               _checked_screens.insert({screen->first, screen->second});
        } else {
-               _checked_screens.erase(screen);
+               _checked_screens.erase({screen->first, screen->second});
        }
 
        setup_show_only_checked();
@@ -670,7 +667,7 @@ ScreensPanel::checkbox_changed (wxTreeListEvent& ev)
 }
 
 
-shared_ptr<Cinema>
+optional<CinemaID>
 ScreensPanel::item_to_cinema (wxTreeListItem item) const
 {
        auto iter = _item_to_cinema.find (item);
@@ -682,7 +679,7 @@ ScreensPanel::item_to_cinema (wxTreeListItem item) const
 }
 
 
-shared_ptr<Screen>
+optional<pair<CinemaID, ScreenID>>
 ScreensPanel::item_to_screen (wxTreeListItem item) const
 {
        auto iter = _item_to_screen.find (item);
@@ -695,7 +692,7 @@ ScreensPanel::item_to_screen (wxTreeListItem item) const
 
 
 optional<wxTreeListItem>
-ScreensPanel::cinema_to_item (shared_ptr<Cinema> cinema) const
+ScreensPanel::cinema_to_item(CinemaID cinema) const
 {
        auto iter = _cinema_to_item.find (cinema);
        if (iter == _cinema_to_item.end()) {
@@ -707,7 +704,7 @@ ScreensPanel::cinema_to_item (shared_ptr<Cinema> cinema) const
 
 
 optional<wxTreeListItem>
-ScreensPanel::screen_to_item (shared_ptr<Screen> screen) const
+ScreensPanel::screen_to_item(ScreenID screen) const
 {
        auto iter = _screen_to_item.find (screen);
        if (iter == _screen_to_item.end()) {
@@ -718,39 +715,6 @@ ScreensPanel::screen_to_item (shared_ptr<Screen> screen) const
 }
 
 
-bool
-ScreensPanel::notify_cinemas_changed()
-{
-       _ignore_cinemas_changed = true;
-       dcp::ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
-
-       try {
-               Config::instance()->changed(Config::CINEMAS);
-       } catch (FileError& e) {
-               error_dialog(
-                       GetParent(),
-                       variant::wx::insert_dcpomatic(
-                               _("Could not write cinema details to the cinemas.xml file.  Check that the location of "
-                                 "cinemas.xml is valid in %s's preferences.")
-                               ),
-                       std_to_wx(e.what())
-                       );
-               return false;
-       }
-
-       return true;
-}
-
-
-void
-ScreensPanel::config_changed(Config::Property property)
-{
-       if (property == Config::Property::CINEMAS && !_ignore_cinemas_changed) {
-               clear_and_re_add();
-       }
-}
-
-
 void
 ScreensPanel::item_activated(wxTreeListEvent& ev)
 {
@@ -760,7 +724,7 @@ ScreensPanel::item_activated(wxTreeListEvent& ev)
        } else {
                auto iter = _item_to_screen.find(ev.GetItem());
                if (iter != _item_to_screen.end()) {
-                       edit_screen(iter->second);
+                       edit_screen(iter->second.first, iter->second.second);
                }
        }
 }
@@ -783,20 +747,12 @@ ScreensPanel::setup_show_only_checked()
 dcp::UTCOffset
 ScreensPanel::best_utc_offset() const
 {
-       auto all_screens = screens();
-       if (all_screens.empty()) {
-               return {};
-       }
-
-       dcp::UTCOffset const first = all_screens[0]->cinema->utc_offset;
-
-       for (auto screen = std::next(all_screens.begin()); screen != all_screens.end(); ++screen) {
-               if ((*screen)->cinema->utc_offset != first) {
-                       /* Not unique */
-                       return dcp::UTCOffset();
-               }
+       std::set<CinemaID> unique_cinema_ids;
+       for (auto const& screen: screens()) {
+               unique_cinema_ids.insert(screen.first);
        }
 
-       return first;
+       CinemaList cinema_list;
+       return cinema_list.unique_utc_offset(unique_cinema_ids).get_value_or(dcp::UTCOffset());
 }
 
index 1e07b62360d2d5645a912220c1e690b134d6cfad..98ec2c631b3fdb030e46fdfa9c6cea2df1f690ea 100644 (file)
@@ -19,6 +19,7 @@
 */
 
 
+#include "lib/cinema_list.h"
 #include "lib/collator.h"
 #include "lib/config.h"
 #include <dcp/warnings.h>
@@ -38,7 +39,6 @@ namespace dcpomatic {
 }
 
 
-class Cinema;
 class CheckBox;
 
 
@@ -48,7 +48,7 @@ public:
        explicit ScreensPanel (wxWindow* parent);
        ~ScreensPanel ();
 
-       std::vector<std::shared_ptr<dcpomatic::Screen>> screens () const;
+       std::set<std::pair<CinemaID, ScreenID>> screens() const;
        void setup_sensitivity ();
 
        dcp::UTCOffset best_utc_offset() const;
@@ -57,38 +57,35 @@ public:
 
 private:
        void add_cinemas ();
-       boost::optional<wxTreeListItem> add_cinema (std::shared_ptr<Cinema>, wxTreeListItem previous);
-       boost::optional<wxTreeListItem> add_screen (std::shared_ptr<Cinema>, std::shared_ptr<dcpomatic::Screen>);
+       boost::optional<wxTreeListItem> add_cinema(CinemaID cinema, wxTreeListItem previous);
+       boost::optional<wxTreeListItem> add_screen(CinemaID cinema, ScreenID screen);
        void add_cinema_clicked ();
        void edit_cinema_clicked ();
-       void edit_cinema(std::shared_ptr<Cinema> cinema);
+       void edit_cinema(CinemaID cinema_id);
        void remove_cinema_clicked ();
        void add_screen_clicked ();
        void edit_screen_clicked ();
-       void edit_screen(std::shared_ptr<dcpomatic::Screen> screen);
+       void edit_screen(CinemaID cinema_id, ScreenID screen_id);
        void remove_screen_clicked ();
        void selection_changed_shim (wxTreeListEvent &);
        void selection_changed ();
        void display_filter_changed();
        void checkbox_changed (wxTreeListEvent& ev);
        void item_activated(wxTreeListEvent& ev);
-       std::shared_ptr<Cinema> cinema_for_operation () const;
+       boost::optional<CinemaID> cinema_for_operation() const;
        void set_screen_checked (wxTreeListItem item, bool checked);
        void setup_cinema_checked_state (wxTreeListItem screen);
        void check_all ();
        void uncheck_all ();
-       bool notify_cinemas_changed();
        void clear_and_re_add();
-       void config_changed(Config::Property);
        void convert_to_lower(std::string& s);
-       bool matches_search(std::shared_ptr<const Cinema> cinema, std::string search);
-       std::list<std::shared_ptr<Cinema>> sorted_cinemas() const;
+       bool matches_search(Cinema const& cinema, std::string search);
        void setup_show_only_checked();
 
-       std::shared_ptr<Cinema> item_to_cinema (wxTreeListItem item) const;
-       std::shared_ptr<dcpomatic::Screen> item_to_screen (wxTreeListItem item) const;
-       boost::optional<wxTreeListItem> cinema_to_item (std::shared_ptr<Cinema> cinema) const;
-       boost::optional<wxTreeListItem> screen_to_item (std::shared_ptr<dcpomatic::Screen> screen) const;
+       boost::optional<CinemaID> item_to_cinema(wxTreeListItem item) const;
+       boost::optional<std::pair<CinemaID, ScreenID>> item_to_screen(wxTreeListItem item) const;
+       boost::optional<wxTreeListItem> cinema_to_item(CinemaID cinema) const;
+       boost::optional<wxTreeListItem> screen_to_item(ScreenID screen) const;
 
        wxBoxSizer* _overall_sizer;
        wxSearchCtrl* _search;
@@ -106,24 +103,19 @@ private:
        /* We want to be able to search (and so remove selected things from the view)
         * but not deselect them, so we maintain lists of selected cinemas and screens.
         */
-       std::vector<std::shared_ptr<Cinema>> _selected_cinemas;
-       std::vector<std::shared_ptr<dcpomatic::Screen>> _selected_screens;
-       /* Likewise with checked screens, except that we can work out which cinemas
-        * are checked from which screens are checked, so we don't need to store the
-        * cinemas.
-        */
-       std::set<std::shared_ptr<dcpomatic::Screen>> _checked_screens;
+       std::vector<CinemaID> _selected_cinemas;
+       /* List of cinema_id, screen_id */
+       std::vector<std::pair<CinemaID, ScreenID>> _selected_screens;
+       /* Likewise with checked screens */
+       std::set<std::pair<CinemaID, ScreenID>> _checked_screens;
 
-       std::map<wxTreeListItem, std::shared_ptr<Cinema>> _item_to_cinema;
-       std::map<wxTreeListItem, std::shared_ptr<dcpomatic::Screen>> _item_to_screen;
-       std::map<std::shared_ptr<Cinema>, wxTreeListItem> _cinema_to_item;
-       std::map<std::shared_ptr<dcpomatic::Screen>, wxTreeListItem> _screen_to_item;
+       std::map<wxTreeListItem, CinemaID> _item_to_cinema;
+       std::map<wxTreeListItem, std::pair<CinemaID, ScreenID>> _item_to_screen;
+       std::map<CinemaID, wxTreeListItem> _cinema_to_item;
+       std::map<ScreenID, wxTreeListItem> _screen_to_item;
 
        bool _ignore_selection_change = false;
        bool _ignore_check_change = false;
 
        Collator _collator;
-
-       boost::signals2::scoped_connection _config_connection;
-       bool _ignore_cinemas_changed = false;
 };
index b37d26d6d48bd7af09d76fd24bb269fbe853a453..a41e3827e38bc2700f1cb4fb839951f22247c556 100644 (file)
@@ -113,6 +113,7 @@ sources = """
           language_subtag_panel.cc
           language_tag_dialog.cc
           language_tag_widget.cc
+          load_config_from_zip_dialog.cc
           kdm_choice.cc
           make_chain_dialog.cc
           markers.cc
index 7d8fb0a76cbfd77e9adbecc276bd786b01eb7920..1c04a4755a85be5384ca54c8af5f05a79386f11a 100644 (file)
@@ -810,20 +810,6 @@ report_config_load_failure(wxWindow* parent, Config::LoadFailure what)
        case Config::LoadFailure::CONFIG:
                message_dialog(parent, _("The existing configuration failed to load.  Default values will be used instead.  These may take a short time to create."));
                break;
-       case Config::LoadFailure::CINEMAS:
-               message_dialog(
-                       parent,
-                       _(wxString::Format("The cinemas list for creating KDMs (cinemas.xml) failed to load.  Please check the numbered backup files in %s",
-                                          std_to_wx(Config::instance()->cinemas_file().parent_path().string())))
-                       );
-               break;
-       case Config::LoadFailure::DKDM_RECIPIENTS:
-               message_dialog(
-                       parent,
-                       _(wxString::Format("The recipients list for creating DKDMs (dkdm_recipients.xml) failed to load.  Please check the numbered backup files in %s",
-                                          std_to_wx(Config::instance()->dkdm_recipients_file().parent_path().string())))
-                       );
-               break;
        }
 }
 
diff --git a/test/cinema_list_test.cc b/test/cinema_list_test.cc
new file mode 100644 (file)
index 0000000..4aa9fa4
--- /dev/null
@@ -0,0 +1,225 @@
+/*
+    Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/cinema.h"
+#include "lib/cinema_list.h"
+#include "lib/config.h"
+#include "lib/screen.h"
+#include <dcp/certificate.h>
+#include <dcp/filesystem.h>
+#include <dcp/util.h>
+#include <boost/filesystem.hpp>
+#include <boost/test/unit_test.hpp>
+#include <list>
+#include <string>
+
+
+using std::pair;
+using std::string;
+using std::vector;
+
+
+static
+boost::filesystem::path
+setup(string name)
+{
+       boost::filesystem::path db = boost::filesystem::path("build") / "test" / (name + ".db");
+       boost::system::error_code ec;
+       boost::filesystem::remove(db, ec);
+       return db;
+}
+
+
+BOOST_AUTO_TEST_CASE(add_cinema_test)
+{
+       auto const db = setup("add_cinema_test");
+
+       auto const name = "Bob's Zero-G Cinema";
+       auto const emails = vector<string>{"zerogbob@hotmail.com"};
+       auto const notes = "Nice enough place but the popcorn keeps floating away";
+       auto const utc_offset = dcp::UTCOffset{5, 0};
+
+       CinemaList cinemas(db);
+       cinemas.add_cinema({name, emails, notes, utc_offset});
+
+       CinemaList cinemas2(db);
+       auto const check = cinemas2.cinemas();
+       BOOST_REQUIRE_EQUAL(check.size(), 1U);
+       BOOST_CHECK(check[0].second.name == name);
+       BOOST_CHECK(check[0].second.emails == emails);
+       BOOST_CHECK_EQUAL(check[0].second.notes, notes);
+       BOOST_CHECK(check[0].second.utc_offset == utc_offset);
+}
+
+
+BOOST_AUTO_TEST_CASE(remove_cinema_test)
+{
+       auto const db = setup("remove_cinema_test");
+
+       auto const name1 = "Bob's Zero-G Cinema";
+       auto const emails1 = vector<string>{"zerogbob@hotmail.com"};
+       auto const notes1 = "Nice enough place but the popcorn keeps floating away";
+       auto const utc_offset1 = dcp::UTCOffset{-4, -30};
+
+       auto const name2 = "Angie's Infinite-Screen Cinema";
+       auto const emails2 = vector<string>{"angie@infinitium.com", "projection-screen912341235@infinitium.com"};
+       auto const notes2 = "Nice enough place but it's very hard to find the right screen";
+       auto const utc_offset2 = dcp::UTCOffset{9, 0};
+
+       CinemaList cinemas(db);
+       auto const id1 = cinemas.add_cinema({name1, emails1, notes1, utc_offset1});
+       cinemas.add_cinema({name2, emails2, notes2, utc_offset2});
+
+       auto const check = cinemas.cinemas();
+       BOOST_REQUIRE_EQUAL(check.size(), 2U);
+       BOOST_CHECK(check[0].second.name == name2);
+       BOOST_CHECK(check[0].second.emails == emails2);
+       BOOST_CHECK_EQUAL(check[0].second.notes, notes2);
+       BOOST_CHECK(check[0].second.utc_offset == utc_offset2);
+       BOOST_CHECK(check[1].second.name == name1);
+       BOOST_CHECK(check[1].second.emails == emails1);
+       BOOST_CHECK_EQUAL(check[1].second.notes, notes1);
+       BOOST_CHECK(check[1].second.utc_offset == utc_offset1);
+
+       cinemas.remove_cinema(id1);
+
+       auto const check2 = cinemas.cinemas();
+       BOOST_REQUIRE_EQUAL(check2.size(), 1U);
+       BOOST_CHECK(check2[0].second.name == name2);
+       BOOST_CHECK(check2[0].second.emails == emails2);
+       BOOST_CHECK_EQUAL(check2[0].second.notes, notes2);
+}
+
+
+BOOST_AUTO_TEST_CASE(update_cinema_test)
+{
+       auto const db = setup("update_cinema_test");
+
+       auto const name1 = "Bob's Zero-G Cinema";
+       auto const emails1 = vector<string>{"zerogbob@hotmail.com"};
+       auto const notes1 = "Nice enough place but the popcorn keeps floating away";
+       auto const utc_offset1 = dcp::UTCOffset{-4, -30};
+
+       auto const name2 = "Angie's Infinite-Screen Cinema";
+       auto const emails2 = vector<string>{"angie@infinitium.com", "projection-screen912341235@infinitium.com"};
+       auto const notes2 = "Nice enough place but it's very hard to find the right screen";
+       auto const utc_offset2 = dcp::UTCOffset{9, 0};
+
+       CinemaList cinemas(db);
+       auto const id = cinemas.add_cinema({name1, emails1, notes1, utc_offset1});
+       cinemas.add_cinema({name2, emails2, notes2, utc_offset2});
+
+       auto check = cinemas.cinemas();
+       BOOST_REQUIRE_EQUAL(check.size(), 2U);
+       /* Alphabetically ordered so first is 2 */
+       BOOST_CHECK_EQUAL(check[0].second.name, name2);
+       BOOST_CHECK(check[0].second.emails == emails2);
+       BOOST_CHECK_EQUAL(check[0].second.notes, notes2);
+       BOOST_CHECK(check[0].second.utc_offset == utc_offset2);
+       /* Then 1 */
+       BOOST_CHECK_EQUAL(check[1].second.name, name1);
+       BOOST_CHECK(check[1].second.emails == emails1);
+       BOOST_CHECK_EQUAL(check[1].second.notes, notes1);
+       BOOST_CHECK(check[1].second.utc_offset == utc_offset1);
+
+       cinemas.update_cinema(id, Cinema{name1, vector<string>{"bob@zerogkino.com"}, notes1, utc_offset1});
+
+       check = cinemas.cinemas();
+       BOOST_REQUIRE_EQUAL(check.size(), 2U);
+       BOOST_CHECK_EQUAL(check[0].second.name, name2);
+       BOOST_CHECK(check[0].second.emails == emails2);
+       BOOST_CHECK_EQUAL(check[0].second.notes, notes2);
+       BOOST_CHECK(check[0].second.utc_offset == utc_offset2);
+       BOOST_CHECK_EQUAL(check[1].second.name, name1);
+       BOOST_CHECK(check[1].second.emails == vector<string>{"bob@zerogkino.com"});
+       BOOST_CHECK_EQUAL(check[1].second.notes, notes1);
+       BOOST_CHECK(check[1].second.utc_offset == utc_offset1);
+}
+
+
+BOOST_AUTO_TEST_CASE(add_screen_test)
+{
+       auto const db = setup("add_screen_test");
+
+       CinemaList cinemas(db);
+       auto const cinema_id = cinemas.add_cinema({"Name", { "foo@bar.com" }, "", dcp::UTCOffset()});
+       auto const screen_id = cinemas.add_screen(
+               cinema_id,
+               dcpomatic::Screen(
+                       "Screen 1",
+                       "Smells of popcorn",
+                       dcp::Certificate(dcp::file_to_string("test/data/cert.pem")),
+                       string("test/data/cert.pem"),
+                       vector<TrustedDevice>{}
+                       ));
+
+       auto check = cinemas.screens(cinema_id);
+       BOOST_REQUIRE_EQUAL(check.size(), 1U);
+       BOOST_CHECK(check[0].first == screen_id);
+       BOOST_CHECK_EQUAL(check[0].second.name, "Screen 1");
+       BOOST_CHECK_EQUAL(check[0].second.notes, "Smells of popcorn");
+       BOOST_CHECK(check[0].second.recipient == dcp::Certificate(dcp::file_to_string("test/data/cert.pem")));
+       BOOST_CHECK(check[0].second.recipient_file == string("test/data/cert.pem"));
+}
+
+
+BOOST_AUTO_TEST_CASE(cinemas_list_copy_from_xml_test)
+{
+       Config::override_path = "build/test/cinemas_list_copy_config";
+       dcp::filesystem::remove_all(*Config::override_path);
+       dcp::filesystem::create_directories(*Config::override_path);
+       dcp::filesystem::copy_file("test/data/cinemas2.xml", *Config::override_path / "cinemas2.xml");
+       Config::drop();
+
+       CinemaList cinema_list;
+       cinema_list.read_legacy_file(Config::instance()->read_path("cinemas2.xml"));
+       auto cinemas = cinema_list.cinemas();
+       BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+
+       auto cinema_iter = cinemas.begin();
+       BOOST_CHECK_EQUAL(cinema_iter->second.name, "Great");
+       BOOST_CHECK_EQUAL(cinema_iter->second.emails.size(), 1U);
+       BOOST_CHECK_EQUAL(cinema_iter->second.emails[0], "julie@tinyscreen.com");
+       BOOST_CHECK(cinema_iter->second.utc_offset == dcp::UTCOffset(1, 0));
+       ++cinema_iter;
+
+       BOOST_CHECK_EQUAL(cinema_iter->second.name, "classy joint");
+       BOOST_CHECK_EQUAL(cinema_iter->second.notes, "Can't stand this place");
+       ++cinema_iter;
+
+       BOOST_CHECK_EQUAL(cinema_iter->second.name, "stinking dump");
+       BOOST_CHECK_EQUAL(cinema_iter->second.emails.size(), 2U);
+       BOOST_CHECK_EQUAL(cinema_iter->second.emails[0], "bob@odourscreen.com");
+       BOOST_CHECK_EQUAL(cinema_iter->second.emails[1], "alice@whiff.com");
+       BOOST_CHECK_EQUAL(cinema_iter->second.notes, "Great cinema, smells of roses");
+       BOOST_CHECK(cinema_iter->second.utc_offset == dcp::UTCOffset(-7, 0));
+       auto screens = cinema_list.screens(cinema_iter->first);
+       BOOST_CHECK_EQUAL(screens.size(), 2U);
+       auto screen_iter = screens.begin();
+       BOOST_CHECK_EQUAL(screen_iter->second.name, "1");
+       BOOST_CHECK(screen_iter->second.recipient);
+       BOOST_CHECK_EQUAL(screen_iter->second.recipient->subject_dn_qualifier(), "CVsuuv9eYsQZSl8U4fDpvOmzZhI=");
+       ++screen_iter;
+       BOOST_CHECK_EQUAL(screen_iter->second.name, "2");
+       BOOST_CHECK(screen_iter->second.recipient);
+       BOOST_CHECK_EQUAL(screen_iter->second.recipient->subject_dn_qualifier(), "CVsuuv9eYsQZSl8U4fDpvOmzZhI=");
+}
+
index d50c3d6f34dfee98b1fce2d57caf039ac92af9b9..1c35f0a789f8874ac0586edde39a1457e784ea49 100644 (file)
 
 
 #include "lib/cinema.h"
+#include "lib/cinema_list.h"
 #include "lib/config.h"
+#include "lib/dkdm_recipient.h"
+#include "lib/dkdm_recipient_list.h"
+#include "lib/unzipper.h"
+#include "lib/zipper.h"
 #include "test.h"
 #include <boost/test/unit_test.hpp>
 #include <fstream>
@@ -178,19 +183,20 @@ BOOST_AUTO_TEST_CASE (config_upgrade_test1)
 
        boost::filesystem::copy_file ("test/data/2.14.config.xml", dir / "config.xml");
        boost::filesystem::copy_file ("test/data/2.14.cinemas.xml", dir / "cinemas.xml");
-       Config::instance();
        try {
-               /* This will fail to write cinemas.xml since the link is to a non-existent directory */
-               Config::instance()->write();
+               /* This will fail to read cinemas.xml since the link is to a non-existent directory */
+               Config::instance();
        } catch (...) {}
 
+       Config::instance()->write();
+
        check_xml (dir / "config.xml", "test/data/2.14.config.xml", {});
        check_xml (dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
 #ifdef DCPOMATIC_WINDOWS
        /* This file has the windows path for dkdm_recipients.xml (with backslashes) */
-       check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.xml", {});
+       check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.windows.sqlite.xml", {});
 #else
-       check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.xml", {});
+       check_xml(dir / "2.18" / "config.xml", "test/data/2.18.config.sqlite.xml", {});
 #endif
        /* cinemas.xml is not copied into 2.18 as its format has not changed */
        BOOST_REQUIRE (!boost::filesystem::exists(dir / "2.18" / "cinemas.xml"));
@@ -214,12 +220,13 @@ BOOST_AUTO_TEST_CASE (config_upgrade_test2)
        boost::filesystem::copy_file("test/data/2.16.config.xml", dir / "config.xml");
 #endif
        boost::filesystem::copy_file("test/data/2.14.cinemas.xml", dir / "cinemas.xml");
-       Config::instance();
        try {
-               /* This will fail to write cinemas.xml since the link is to a non-existent directory */
-               Config::instance()->write();
+               /* This will fail to read cinemas.xml since the link is to a non-existent directory */
+               Config::instance();
        } catch (...) {}
 
+       Config::instance()->write();
+
        check_xml(dir / "cinemas.xml", "test/data/2.14.cinemas.xml", {});
 #ifdef DCPOMATIC_WINDOWS
        /* This file has the windows path for dkdm_recipients.xml (with backslashes) */
@@ -246,16 +253,16 @@ BOOST_AUTO_TEST_CASE (config_keep_cinemas_if_making_new_config)
 
        Config::instance()->write();
 
-       Config::instance()->add_cinema(make_shared<Cinema>("My Great Cinema", vector<string>(), "", dcp::UTCOffset()));
-       Config::instance()->write();
+       CinemaList cinemas;
+       cinemas.add_cinema({"My Great Cinema", {}, "", dcp::UTCOffset()});
 
-       boost::filesystem::copy_file (dir / "cinemas.xml", dir / "backup_for_test.xml");
+       boost::filesystem::copy_file(dir / "cinemas.sqlite3", dir / "backup_for_test.sqlite3");
 
        Config::drop ();
        boost::filesystem::remove (dir / "2.18" / "config.xml");
        Config::instance();
 
-       check_text_file(dir / "backup_for_test.xml", dir / "cinemas.xml");
+       check_file(dir / "backup_for_test.sqlite3", dir / "cinemas.sqlite3");
 }
 
 
@@ -271,11 +278,14 @@ BOOST_AUTO_TEST_CASE(keep_config_if_cinemas_fail_to_load)
        boost::filesystem::create_directories(dir);
        Config::instance()->write();
 
-       auto const cinemas = dir / "cinemas.xml";
+       CinemaList cinema_list;
+       cinema_list.add_cinema(Cinema("Foo", {}, "Bar", dcp::UTCOffset()));
+
+       auto const cinemas = dir / "cinemas.sqlite3";
 
        /* Back things up */
        boost::filesystem::copy_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml");
-       boost::filesystem::copy_file(cinemas, dir / "cinemas_backup_for_test.xml");
+       boost::filesystem::copy_file(cinemas, dir / "cinemas_backup_for_test.sqlite3");
 
        /* Corrupt the cinemas */
        Config::drop();
@@ -284,8 +294,241 @@ BOOST_AUTO_TEST_CASE(keep_config_if_cinemas_fail_to_load)
        corrupt.close();
        Config::instance();
 
-       /* We should have a new cinemas.xml and the old config.xml */
+       /* We should have the old config.xml */
        check_text_file(dir / "2.18" / "config.xml", dir / "config_backup_for_test.xml");
-       check_text_file(cinemas, dir / "cinemas_backup_for_test.xml");
 }
 
+
+BOOST_AUTO_TEST_CASE(read_cinemas_xml_and_write_sqlite)
+{
+       ConfigRestorer cr;
+
+       /* Set up a config with an XML cinemas file */
+       boost::filesystem::path dir = "build/test/read_cinemas_xml_and_write_sqlite";
+       boost::filesystem::remove_all(dir);
+       boost::filesystem::create_directories(dir);
+       boost::filesystem::create_directories(dir / "2.18");
+
+       boost::filesystem::copy_file("test/data/cinemas.xml", dir / "cinemas.xml");
+       boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "2.18" / "config.xml");
+       {
+               Editor editor(dir / "2.18" / "config.xml");
+               editor.replace(
+                       "/home/realldoesnt/exist/this/path/is/nonsense.xml",
+                       boost::filesystem::canonical(dir / "cinemas.xml").string()
+                       );
+       }
+
+       Config::override_path = dir;
+       Config::drop();
+
+       /* This should make a sqlite3 file containing the recipients from cinemas.xml */
+       Config::instance();
+
+       {
+               CinemaList test(dir / "cinemas.sqlite3");
+
+               /* The detailed creation of sqlite3 from XML is tested in cinema_list_test.cc */
+               auto cinemas = test.cinemas();
+               BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+               BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
+               BOOST_CHECK_EQUAL(cinemas[1].second.name, "classy joint");
+               BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
+
+               /* Add another recipient to the sqlite */
+               test.add_cinema({"The ol' 1-seater", {}, "Quiet but lonely", dcp::UTCOffset()});
+       }
+
+       /* Reload the config; the old XML should not clobber the new sqlite3 */
+       Config::drop();
+       Config::instance();
+
+       {
+               CinemaList test(dir / "cinemas.sqlite3");
+
+               auto cinemas = test.cinemas();
+               BOOST_REQUIRE_EQUAL(cinemas.size(), 4U);
+               BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
+               BOOST_CHECK_EQUAL(cinemas[1].second.name, "The ol' 1-seater");
+               BOOST_CHECK_EQUAL(cinemas[2].second.name, "classy joint");
+               BOOST_CHECK_EQUAL(cinemas[3].second.name, "stinking dump");
+       }
+}
+
+
+BOOST_AUTO_TEST_CASE(read_dkdm_recipients_xml_and_write_sqlite)
+{
+       ConfigRestorer cr;
+
+       /* Set up a config with an XML cinemas file */
+       boost::filesystem::path dir = "build/test/read_dkdm_recipients_xml_and_write_sqlite";
+       boost::filesystem::remove_all(dir);
+       boost::filesystem::create_directories(dir);
+       boost::filesystem::create_directories(dir / "2.18");
+
+       boost::filesystem::copy_file("test/data/dkdm_recipients.xml", dir / "dkdm_recipients.xml");
+       boost::filesystem::copy_file("test/data/2.18.config.xml", dir / "2.18" / "config.xml");
+       {
+               Editor editor(dir / "2.18" / "config.xml");
+               editor.replace(
+                       "build/test/config_upgrade_test/dkdm_recipients.xml",
+                       boost::filesystem::canonical(dir / "dkdm_recipients.xml").string()
+                       );
+       }
+
+       Config::override_path = dir;
+       Config::drop();
+
+       /* This should make a sqlite3 file containing the recipients from dkdm_recipients.xml */
+       Config::instance();
+
+       {
+               DKDMRecipientList test(dir / "dkdm_recipients.sqlite3");
+
+               /* The detailed creation of sqlite3 from XML is tested in dkdm_recipient_list_test.cc */
+               auto recipients = test.dkdm_recipients();
+               BOOST_REQUIRE_EQUAL(recipients.size(), 2U);
+               BOOST_CHECK_EQUAL(recipients[0].second.name, "Bob's Epics");
+               BOOST_CHECK_EQUAL(recipients[1].second.name, "Sharon's Shorts");
+
+               /* Add another recipient to the sqlite */
+               test.add_dkdm_recipient({"Carl's Classics", "Oldies but goodies", {}, {}});
+       }
+
+       /* Reload the config; the old XML should not clobber the new sqlite3 */
+       Config::drop();
+       Config::instance();
+
+       {
+               DKDMRecipientList test(dir / "dkdm_recipients.sqlite3");
+
+               auto recipients = test.dkdm_recipients();
+               BOOST_REQUIRE_EQUAL(recipients.size(), 3U);
+               BOOST_CHECK_EQUAL(recipients[0].second.name, "Bob's Epics");
+               BOOST_CHECK_EQUAL(recipients[1].second.name, "Carl's Classics");
+               BOOST_CHECK_EQUAL(recipients[2].second.name, "Sharon's Shorts");
+       }
+}
+
+
+BOOST_AUTO_TEST_CASE(save_config_as_zip_test)
+{
+       ConfigRestorer cr;
+
+       CinemaList cinemas;
+       cinemas.add_cinema({"My Great Cinema", {}, "", dcp::UTCOffset()});
+       DKDMRecipientList recipients;
+       recipients.add_dkdm_recipient({"Carl's Classics", "Oldies but goodies", {}, {}});
+
+       boost::filesystem::path const zip = "build/test/save.zip";
+       boost::system::error_code ec;
+       boost::filesystem::remove(zip, ec);
+       save_all_config_as_zip(zip);
+       Unzipper unzipper(zip);
+
+       BOOST_CHECK(unzipper.contains("config.xml"));
+       BOOST_CHECK(unzipper.contains("cinemas.sqlite3"));
+       BOOST_CHECK(unzipper.contains("dkdm_recipients.sqlite3"));
+}
+
+
+/** Load a config ZIP file, which contains an XML cinemas file, and ask to overwrite
+ *  the existing cinemas file that we had.
+ */
+BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_current)
+{
+       ConfigRestorer cr;
+
+       auto cinemas_file = Config::instance()->cinemas_file();
+
+       boost::filesystem::path const zip = "build/test/load.zip";
+       boost::system::error_code ec;
+       boost::filesystem::remove(zip, ec);
+
+       Zipper zipper(zip);
+       zipper.add(
+               "config.xml",
+               boost::algorithm::replace_all_copy(
+                       dcp::file_to_string("test/data/2.18.config.xml"),
+                       "/home/realldoesnt/exist/this/path/is/nonsense.xml",
+                       ""
+                       )
+               );
+
+       zipper.add("cinemas.xml", dcp::file_to_string("test/data/cinemas.xml"));
+       zipper.close();
+
+       Config::instance()->load_from_zip(zip, Config::CinemasAction::WRITE_TO_CURRENT_PATH);
+
+       CinemaList cinema_list(cinemas_file);
+       auto cinemas = cinema_list.cinemas();
+       BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+       BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
+       BOOST_CHECK_EQUAL(cinemas[1].second.name, "classy joint");
+       BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
+}
+
+
+/** Load a config ZIP file, which contains an XML cinemas file, and ask to write it to
+ *  the location specified by the zipped config.xml.
+ */
+BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_zip)
+{
+       ConfigRestorer cr;
+
+       boost::filesystem::path const zip = "build/test/load.zip";
+       boost::system::error_code ec;
+       boost::filesystem::remove(zip, ec);
+
+       Zipper zipper(zip);
+       zipper.add(
+               "config.xml",
+               boost::algorithm::replace_all_copy(
+                       dcp::file_to_string("test/data/2.18.config.xml"),
+                       "/home/realldoesnt/exist/this/path/is/nonsense.xml",
+                       "build/test/hide/it/here/cinemas.sqlite3"
+                       )
+               );
+
+       zipper.add("cinemas.xml", dcp::file_to_string("test/data/cinemas.xml"));
+       zipper.close();
+
+       Config::instance()->load_from_zip(zip, Config::CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG);
+
+       CinemaList cinema_list("build/test/hide/it/here/cinemas.sqlite3");
+       auto cinemas = cinema_list.cinemas();
+       BOOST_REQUIRE_EQUAL(cinemas.size(), 3U);
+       BOOST_CHECK_EQUAL(cinemas[0].second.name, "Great");
+       BOOST_CHECK_EQUAL(cinemas[1].second.name, "classy joint");
+       BOOST_CHECK_EQUAL(cinemas[2].second.name, "stinking dump");
+}
+
+
+/** Load a config ZIP file, which contains an XML cinemas file, and ask to ignore it */
+BOOST_AUTO_TEST_CASE(load_config_from_zip_with_only_xml_ignore)
+{
+       ConfigRestorer cr;
+
+       boost::filesystem::path const zip = "build/test/load.zip";
+       boost::system::error_code ec;
+       boost::filesystem::remove(zip, ec);
+
+       Zipper zipper(zip);
+       zipper.add(
+               "config.xml",
+               boost::algorithm::replace_all_copy(
+                       dcp::file_to_string("test/data/2.18.config.xml"),
+                       "/home/realldoesnt/exist/this/path/is/nonsense.xml",
+                       "build/test/hide/it/here/cinemas.sqlite3"
+                       )
+               );
+
+       zipper.add("cinemas.xml", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><Cinemas/>");
+       zipper.close();
+
+       Config::instance()->load_from_zip(zip, Config::CinemasAction::IGNORE);
+
+       CinemaList cinema_list("build/test/hide/it/here/cinemas.sqlite3");
+       auto cinemas = cinema_list.cinemas();
+       BOOST_CHECK(!cinemas.empty());
+}
index 351419eebadc7d0a9aafc84f45e77218783e4b3f..b4fe926644bb49e442a3a920cb4601be8959bfb2 160000 (submodule)
--- a/test/data
+++ b/test/data
@@ -1 +1 @@
-Subproject commit 351419eebadc7d0a9aafc84f45e77218783e4b3f
+Subproject commit b4fe926644bb49e442a3a920cb4601be8959bfb2
diff --git a/test/dkdm_recipient_list_test.cc b/test/dkdm_recipient_list_test.cc
new file mode 100644 (file)
index 0000000..20f669e
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    Copyright (C) 2024 Carl Hetherington <cth@carlh.net>
+
+    This file is part of DCP-o-matic.
+
+    DCP-o-matic 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 2 of the License, or
+    (at your option) any later version.
+
+    DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "lib/config.h"
+#include "lib/dkdm_recipient.h"
+#include "lib/dkdm_recipient_list.h"
+#include <dcp/filesystem.h>
+#include <boost/test/unit_test.hpp>
+
+
+BOOST_AUTO_TEST_CASE(dkdm_receipient_list_copy_from_xml_test)
+{
+       Config::override_path = "build/test/dkdm_recipient_list_copy_config";
+       dcp::filesystem::remove_all(*Config::override_path);
+       dcp::filesystem::create_directories(*Config::override_path);
+       dcp::filesystem::copy_file("test/data/dkdm_recipients.xml", *Config::override_path / "dkdm_recipients.xml");
+       Config::drop();
+
+       DKDMRecipientList dkdm_recipient_list;
+       dkdm_recipient_list.read_legacy_file(Config::instance()->read_path("dkdm_recipients.xml"));
+       auto dkdm_recipients = dkdm_recipient_list.dkdm_recipients();
+       BOOST_REQUIRE_EQUAL(dkdm_recipients.size(), 2U);
+
+       auto dkdm_recipient_iter = dkdm_recipients.begin();
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.name, "Bob's Epics");
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.emails.size(), 2U);
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.emails[0], "epicbob@gmail.com");
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.emails[1], "boblikesemlong@cinema-bob.com");
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.recipient->subject_dn_qualifier(), "r5/Q5f3UTm7qzoF5QzNZP6aEuvI=");
+       ++dkdm_recipient_iter;
+
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.name, "Sharon's Shorts");
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.notes, "Even if it sucks, at least it's over quickly");
+       BOOST_CHECK_EQUAL(dkdm_recipient_iter->second.recipient->subject_dn_qualifier(), "FHerM3Us/DWuqD1MnztStSlFJO0=");
+       ++dkdm_recipient_iter;
+}
+
+
index 1c590cdf717e910560e20b78f20228415f3d104d..3fd20d47820b7a2f029e138172a7e2aee8761264 100644 (file)
@@ -20,6 +20,7 @@
 
 
 #include "lib/cinema.h"
+#include "lib/cinema_list.h"
 #include "lib/config.h"
 #include "lib/content_factory.h"
 #include "lib/cross.h"
@@ -154,16 +155,16 @@ setup_test_config()
        auto config = Config::instance();
        auto const cert = dcp::Certificate(dcp::file_to_string("test/data/cert.pem"));
 
-       auto cinema_a = std::make_shared<Cinema>("Dean's Screens", vector<string>(), "", dcp::UTCOffset());
-       cinema_a->add_screen(std::make_shared<dcpomatic::Screen>("Screen 1", "", cert, boost::none, std::vector<TrustedDevice>()));
-       cinema_a->add_screen(std::make_shared<dcpomatic::Screen>("Screen 2", "", cert, boost::none, std::vector<TrustedDevice>()));
-       cinema_a->add_screen(std::make_shared<dcpomatic::Screen>("Screen 3", "", cert, boost::none, std::vector<TrustedDevice>()));
-       config->add_cinema(cinema_a);
+       CinemaList cinemas(config->cinemas_file());
 
-       auto cinema_b = std::make_shared<Cinema>("Floyd's Celluloid", vector<string>(), "", dcp::UTCOffset());
-       cinema_b->add_screen(std::make_shared<dcpomatic::Screen>("Foo", "", cert, boost::none, std::vector<TrustedDevice>()));
-       cinema_b->add_screen(std::make_shared<dcpomatic::Screen>("Bar", "", cert, boost::none, std::vector<TrustedDevice>()));
-       config->add_cinema(cinema_b);
+       auto cinema_a = cinemas.add_cinema({"Dean's Screens", {}, "", dcp::UTCOffset()});
+       cinemas.add_screen(cinema_a, {"Screen 1", "", cert, boost::none, {}});
+       cinemas.add_screen(cinema_a, {"Screen 2", "", cert, boost::none, {}});
+       cinemas.add_screen(cinema_a, {"Screen 3", "", cert, boost::none, {}});
+
+       auto cinema_b = cinemas.add_cinema({"Floyd's Celluloid", {}, "", dcp::UTCOffset()});
+       cinemas.add_screen(cinema_b, {"Foo", "", cert, boost::none, std::vector<TrustedDevice>()});
+       cinemas.add_screen(cinema_b, {"Bar", "", cert, boost::none, std::vector<TrustedDevice>()});
 }
 
 
@@ -250,7 +251,7 @@ BOOST_AUTO_TEST_CASE(kdm_cli_specify_cinemas_file)
        vector<string> args = {
                "kdm_cli",
                "--cinemas-file",
-               "test/data/cinemas.xml",
+               "test/data/cinemas.sqlite3",
                "list-cinemas"
        };
 
@@ -259,9 +260,9 @@ BOOST_AUTO_TEST_CASE(kdm_cli_specify_cinemas_file)
        BOOST_CHECK(!error);
 
        BOOST_REQUIRE_EQUAL(output.size(), 3U);
-       BOOST_CHECK_EQUAL(output[0], "stinking dump ()");
+       BOOST_CHECK_EQUAL(output[0], "Great (julie@tinyscreen.com)");
        BOOST_CHECK_EQUAL(output[1], "classy joint ()");
-       BOOST_CHECK_EQUAL(output[2], "Great ()");
+       BOOST_CHECK_EQUAL(output[2], "stinking dump (bob@odourscreen.com, alice@whiff.com)");
 }
 
 
index f73e4295e445458968e7e83bf1379bc0401015ae..c5c7c2678a7a89b480dde3c320de0154b94fa76f 100644 (file)
@@ -20,6 +20,7 @@
 
 
 #include "lib/cinema.h"
+#include "lib/cinema_list.h"
 #include "lib/config.h"
 #include "lib/content_factory.h"
 #include "lib/film.h"
@@ -32,6 +33,7 @@
 using std::dynamic_pointer_cast;
 using std::list;
 using std::make_shared;
+using std::pair;
 using std::shared_ptr;
 using std::string;
 using std::vector;
@@ -46,34 +48,40 @@ confirm_overwrite (boost::filesystem::path)
 }
 
 
-static shared_ptr<dcpomatic::Screen> cinema_a_screen_1;
-static shared_ptr<dcpomatic::Screen> cinema_a_screen_2;
-static shared_ptr<dcpomatic::Screen> cinema_b_screen_x;
-static shared_ptr<dcpomatic::Screen> cinema_b_screen_y;
-static shared_ptr<dcpomatic::Screen> cinema_b_screen_z;
+struct Context
+{
+       Context()
+       {
+               CinemaList cinemas;
+
+               auto crypt_cert = Config::instance()->decryption_chain()->leaf();
+
+               cinema_a = cinemas.add_cinema({"Cinema A", {}, "", dcp::UTCOffset(4, 30)});
+               cinema_a_screen_1 = cinemas.add_screen(cinema_a, {"Screen 1", "", crypt_cert, boost::none, {}});
+               cinema_a_screen_2 = cinemas.add_screen(cinema_a, {"Screen 2", "", crypt_cert, boost::none, {}});
+
+               cinema_b = cinemas.add_cinema({"Cinema B", {}, "", dcp::UTCOffset(-1, 0)});
+               cinema_b_screen_x = cinemas.add_screen(cinema_b, {"Screen X", "", crypt_cert, boost::none, {}});
+               cinema_b_screen_y = cinemas.add_screen(cinema_b, {"Screen Y", "", crypt_cert, boost::none, {}});
+               cinema_b_screen_z = cinemas.add_screen(cinema_b, {"Screen Z", "", crypt_cert, boost::none, {}});
+       }
+
+       CinemaID cinema_a = 0;
+       CinemaID cinema_b = 0;
+       ScreenID cinema_a_screen_1 = 0;
+       ScreenID cinema_a_screen_2 = 0;
+       ScreenID cinema_b_screen_x = 0;
+       ScreenID cinema_b_screen_y = 0;
+       ScreenID cinema_b_screen_z = 0;
+};
 
 
 BOOST_AUTO_TEST_CASE (single_kdm_naming_test)
 {
        auto c = Config::instance();
 
-       auto crypt_cert = c->decryption_chain()->leaf();
-
-       auto cinema_a = make_shared<Cinema>("Cinema A", vector<string>(), "", dcp::UTCOffset{4, 30});
-       cinema_a_screen_1 = std::make_shared<dcpomatic::Screen>("Screen 1", "", crypt_cert, boost::none, vector<TrustedDevice>());
-       cinema_a->add_screen (cinema_a_screen_1);
-       cinema_a_screen_2 = std::make_shared<dcpomatic::Screen>("Screen 2", "", crypt_cert, boost::none, vector<TrustedDevice>());
-       cinema_a->add_screen (cinema_a_screen_2);
-       c->add_cinema (cinema_a);
-
-       auto cinema_b = make_shared<Cinema>("Cinema B", vector<string>(), "", dcp::UTCOffset{-1, 0});
-       cinema_b_screen_x = std::make_shared<dcpomatic::Screen>("Screen X", "", crypt_cert, boost::none, vector<TrustedDevice>());
-       cinema_b->add_screen (cinema_b_screen_x);
-       cinema_b_screen_y = std::make_shared<dcpomatic::Screen>("Screen Y", "", crypt_cert, boost::none, vector<TrustedDevice>());
-       cinema_b->add_screen (cinema_b_screen_y);
-       cinema_b_screen_z = std::make_shared<dcpomatic::Screen>("Screen Z", "", crypt_cert, boost::none, vector<TrustedDevice>());
-       cinema_b->add_screen (cinema_b_screen_z);
-       c->add_cinema (cinema_a);
+       Context context;
+       CinemaList cinemas;
 
        /* Film */
        boost::filesystem::remove_all ("build/test/single_kdm_naming_test");
@@ -101,7 +109,9 @@ BOOST_AUTO_TEST_CASE (single_kdm_naming_test)
        };
        auto kdm = kdm_for_screen (
                        make_kdm,
-                       cinema_a_screen_1,
+                       context.cinema_a,
+                       *cinemas.cinema(context.cinema_a),
+                       *cinemas.screen(context.cinema_a_screen_1),
                        from,
                        until,
                        dcp::Formulation::MODIFIED_TRANSITIONAL_1,
@@ -128,10 +138,13 @@ BOOST_AUTO_TEST_CASE (single_kdm_naming_test)
 }
 
 
-BOOST_AUTO_TEST_CASE (directory_kdm_naming_test, * boost::unit_test::depends_on("single_kdm_naming_test"))
+BOOST_AUTO_TEST_CASE(directory_kdm_naming_test)
 {
        using boost::filesystem::path;
 
+       Context context;
+       CinemaList cinemas;
+
        /* Film */
        boost::filesystem::remove_all ("build/test/directory_kdm_naming_test");
        auto film = new_test_film2 (
@@ -152,8 +165,11 @@ BOOST_AUTO_TEST_CASE (directory_kdm_naming_test, * boost::unit_test::depends_on(
        dcp::LocalTime until (sign_cert.not_after());
        until.add_months (-2);
 
-       vector<shared_ptr<dcpomatic::Screen>> screens = {
-               cinema_a_screen_2, cinema_b_screen_x, cinema_a_screen_1, (cinema_b_screen_z)
+       vector<pair<CinemaID, ScreenID>> screens = {
+               { context.cinema_a, context.cinema_a_screen_2 },
+               { context.cinema_b, context.cinema_b_screen_x },
+               { context.cinema_a, context.cinema_a_screen_1 },
+               { context.cinema_b, context.cinema_b_screen_z }
        };
 
        auto const cpl = cpls.front().cpl_file;
@@ -166,10 +182,12 @@ BOOST_AUTO_TEST_CASE (directory_kdm_naming_test, * boost::unit_test::depends_on(
                return film->make_kdm(cpls.front().cpl_file, begin, end);
        };
 
-       for (auto i: screens) {
+       for (auto screen: screens) {
                auto kdm = kdm_for_screen (
                                make_kdm,
-                               i,
+                               screen.first,
+                               *cinemas.cinema(screen.first),
+                               *cinemas.screen(screen.second),
                                from,
                                until,
                                dcp::Formulation::MODIFIED_TRANSITIONAL_1,
index 01b71a05e5bde26006fa5eab1eea14df096313bf..696a2c36a38c6afb59d2df8d7b2e7a31b6061123 100644 (file)
@@ -63,6 +63,7 @@ BOOST_AUTO_TEST_CASE (recover_test_2d)
        film->set_dcp_content_type (DCPContentType::from_isdcf_name ("FTR"));
        film->set_container (Ratio::from_id ("185"));
        film->set_name ("recover_test");
+       film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
 
        auto content = make_shared<FFmpegContent>("test/data/count300bd24.m2ts");
        film->examine_and_add_content (content);
@@ -110,6 +111,7 @@ BOOST_AUTO_TEST_CASE (recover_test_3d, * boost::unit_test::depends_on("recover_t
        film->set_container (Ratio::from_id ("185"));
        film->set_name ("recover_test");
        film->set_three_d (true);
+       film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
 
        auto content = make_shared<ImageContent>("test/data/3d_test");
        content->video->set_frame_type (VideoFrameType::THREE_D_LEFT_RIGHT);
@@ -154,6 +156,7 @@ BOOST_AUTO_TEST_CASE (recover_test_2d_encrypted, * boost::unit_test::depends_on(
        film->set_name ("recover_test");
        film->set_encrypted (true);
        film->_key = dcp::Key("eafcb91c9f5472edf01f3a2404c57258");
+       film->set_video_bit_rate(VideoEncoding::JPEG2000, 100000000);
 
        auto content = make_shared<FFmpegContent>("test/data/count300bd24.m2ts");
        film->examine_and_add_content (content);
index ddd3d26f0ef60d76a2a61f603895433988155b42..d214f1d39561c99f2cfd7bf9408976868c9a4352 100644 (file)
@@ -138,7 +138,8 @@ setup_test_config ()
        decryption->set_key(dcp::file_to_string("test/data/decryption_key"));
        Config::instance()->set_decryption_chain (decryption);
        Config::instance()->set_dcp_asset_filename_format(dcp::NameFormat("%t"));
-       Config::instance()->set_cinemas_file("test/data/empty_cinemas.xml");
+       Config::instance()->set_cinemas_file("build/test/cinemas.sqlite3");
+       Config::instance()->set_dkdm_recipients_file("build/test/dkdm_recipients.sqlite3");
 }
 
 
index 3acca09e895be2feaedf3a3e824c0dab254475b0..7555f76db8d07773ebb00d3b753c5aaf2548e070 100644 (file)
@@ -37,7 +37,7 @@ def build(bld):
     obj.name   = 'unit-tests'
     obj.uselib =  'BOOST_TEST BOOST_THREAD BOOST_FILESYSTEM BOOST_DATETIME SNDFILE SAMPLERATE DCP FONTCONFIG CAIROMM PANGOMM XMLPP '
     obj.uselib += 'AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE SWRESAMPLE POSTPROC CXML SUB GLIB CURL SSH XMLSEC BOOST_REGEX ICU NETTLE PNG JPEG '
-    obj.uselib += 'LEQM_NRT ZIP '
+    obj.uselib += 'LEQM_NRT ZIP SQLITE3 '
     if bld.env.TARGET_WINDOWS_64 or bld.env.TARGET_WINDOWS_32:
         obj.uselib += 'WINSOCK2 DBGHELP SHLWAPI MSWSOCK BOOST_LOCALE '
     if bld.env.TARGET_LINUX:
@@ -60,6 +60,7 @@ def build(bld):
                  burnt_subtitle_test.cc
                  butler_test.cc
                  bv20_test.cc
+                 cinema_list_test.cc
                  cinema_sound_processor_test.cc
                  client_server_test.cc
                  closed_caption_test.cc
@@ -78,6 +79,7 @@ def build(bld):
                  dcp_playback_test.cc
                  dcp_subtitle_test.cc
                  digest_test.cc
+                 dkdm_recipient_list_test.cc
                  empty_caption_test.cc
                  empty_test.cc
                  encryption_test.cc
diff --git a/wscript b/wscript
index 65f2e843fd7908d81dd62e985f91be1c8ec14b95..c5996a3610c79ba8e49380a2dc6e04333e2582fa 100644 (file)
--- a/wscript
+++ b/wscript
@@ -629,6 +629,18 @@ def configure(conf):
                            lib=deps,
                            uselib_store='BOOST_PROCESS')
 
+    # sqlite3
+    conf.check_cfg(package="sqlite3", args='--cflags --libs', uselib_store='SQLITE3', mandatory=True)
+    conf.check_cxx(fragment="""
+                       #include <sqlite3.h>
+                       int main() { sqlite3_prepare_v3(nullptr, "", -1, 0, nullptr, nullptr); }
+                       """,
+                   msg='Checking for sqlite3_prepare_v3',
+                   uselib='SQLITE3',
+                   define_name="DCPOMATIC_HAVE_SQLITE3_PREPARE_V3",
+                   mandatory=False)
+
+
     # Other stuff
 
     conf.find_program('msgfmt', var='MSGFMT')