From: Carl Hetherington Date: Sat, 20 May 2023 20:51:49 +0000 (+0200) Subject: Use sqlite for cinema and DKDM recipient lists. X-Git-Tag: v2.17.17~4 X-Git-Url: https://git.carlh.net/gitweb/?a=commitdiff_plain;h=a3fcbb3a76e079a5485a0552ea5d35b8d6739116;p=dcpomatic.git Use sqlite for cinema and DKDM recipient lists. --- diff --git a/platform/osx/make_dmg.sh b/platform/osx/make_dmg.sh index 633b830b7..dd42ebe32 100644 --- a/platform/osx/make_dmg.sh +++ b/platform/osx/make_dmg.sh @@ -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 diff --git a/platform/windows/wscript b/platform/windows/wscript index 2f0bf2c22..2c53a1422 100644 --- a/platform/windows/wscript +++ b/platform/windows/wscript @@ -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 index b1681fc28..000000000 --- a/src/lib/cinema.cc +++ /dev/null @@ -1,103 +0,0 @@ -/* - Copyright (C) 2013-2021 Carl Hetherington - - 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 . - -*/ - - -#include "cinema.h" -#include "screen.h" -#include "dcpomatic_assert.h" -#include -#include -#include - - -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("UTCOffset")) { - hour = node->number_child("UTCOffset"); - } else { - hour = node->optional_number_child("UTCOffsetHour").get_value_or(0); - } - - int minute = node->optional_number_child("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(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(utc_offset.hour())); - cxml::add_text_child(parent, "UTCOffsetMinute", raw_convert(utc_offset.minute())); - - for (auto i: _screens) { - i->as_xml(cxml::add_child(parent, "Screen")); - } -} - -void -Cinema::add_screen (shared_ptr s) -{ - s->cinema = shared_from_this (); - _screens.push_back (s); -} - -void -Cinema::remove_screen (shared_ptr s) -{ - auto iter = std::find(_screens.begin(), _screens.end(), s); - if (iter != _screens.end()) { - _screens.erase(iter); - } -} - diff --git a/src/lib/cinema.h b/src/lib/cinema.h index 05f6fb7fc..44f232f91 100644 --- a/src/lib/cinema.h +++ b/src/lib/cinema.h @@ -18,31 +18,24 @@ */ + /** @file src/lib/cinema.h * @brief Cinema class. */ #include -#include #include +#include +#include -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 +class Cinema { public: Cinema(std::string const & name_, std::vector 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); - void remove_screen (std::shared_ptr); - std::string name; std::vector emails; std::string notes; dcp::UTCOffset utc_offset; - - std::vector> screens() const { - return _screens; - } - -private: - std::vector> _screens; }; diff --git a/src/lib/cinema_list.cc b/src/lib/cinema_list.cc new file mode 100644 index 000000000..41b9dbab3 --- /dev/null +++ b/src/lib/cinema_list.cc @@ -0,0 +1,466 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#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 +#include +#include +#include +#include + + +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 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("UTCOffset")) { + hour = cinema_node->number_child("UTCOffset"); + } else { + hour = cinema_node->optional_number_child("UTCOffsetHour").get_value_or(0); + } + + int minute = cinema_node->optional_number_child("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 recipient; + if (auto recipient_string = screen_node->optional_string_child("Recipient")) { + recipient = dcp::Certificate(*recipient_string); + } + vector 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> +cinemas_from_result(SQLiteStatement& statement) +{ + vector> 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 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(statement.column_int64(4)); + auto const utc_offset_minute = static_cast(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> +CinemaList::cinemas() const +{ + SQLiteStatement statement(_db, _cinemas.select("ORDER BY name ASC")); + return cinemas_from_result(statement); +} + + +optional +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 certificate = certificate_string.empty() ? optional() : dcp::Certificate(certificate_string); + auto recipient_file_string = statement.column_text(5); + optional recipient_file = recipient_file_string.empty() ? optional() : recipient_file_string; + + SQLiteStatement trusted_devices_statement(_db, _trusted_devices.select("WHERE screen=?")); + trusted_devices_statement.bind_int64(1, screen_id.get()); + vector 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 +CinemaList::screen(ScreenID screen_id) const +{ + SQLiteStatement statement(_db, _screens.select("WHERE id=?")); + statement.bind_int64(1, screen_id.get()); + + optional 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> +CinemaList::screens_from_result(SQLiteStatement& statement) const +{ + vector> 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> +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> +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> +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 +CinemaList::unique_utc_offset(std::set const& cinemas_to_check) +{ + optional 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 index 000000000..c91f29476 --- /dev/null +++ b/src/lib/cinema_list.h @@ -0,0 +1,126 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#ifndef DCPOMATIC_CINEMA_LIST_H +#define DCPOMATIC_CINEMA_LIST_H + + +#include "id.h" +#include "sqlite_table.h" +#include +#include +#include +#include +#include +#include + + +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> cinemas() const; + boost::optional cinema(CinemaID id) const; + boost::optional> cinema_by_partial_name(std::string const& text) const; + boost::optional> 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 screen(ScreenID screen_id) const; + std::vector> screens(CinemaID cinema_id) const; + std::vector> screens_by_cinema_and_name(CinemaID id, std::string const& name) const; + + boost::optional unique_utc_offset(std::set const& cinemas); + +private: + dcpomatic::Screen screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const; + std::vector> 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 + diff --git a/src/lib/config.cc b/src/lib/config.cc index 33b1a8656..c2c2cc244 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -19,14 +19,14 @@ */ -#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 ("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> 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(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(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) diff --git a/src/lib/config.h b/src/lib/config.h index a7b238c04..67c784620 100644 --- a/src/lib/config.h +++ b/src/lib/config.h @@ -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> cinemas () const { - return _cinemas; - } - - std::list> dkdm_recipients () const { - return _dkdm_recipients; - } - std::list 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 c) { - _cinemas.push_back (c); - changed (CINEMAS); - } - - void remove_cinema (std::shared_ptr c) { - _cinemas.remove (c); - changed (CINEMAS); - } - - void add_dkdm_recipient (std::shared_ptr c) { - _dkdm_recipients.push_back (c); - changed (DKDM_RECIPIENTS); - } - - void remove_dkdm_recipient (std::shared_ptr c) { - _dkdm_recipients.remove (c); - changed (DKDM_RECIPIENTS); - } - void set_allowed_dcp_frame_rates (std::list 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 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 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 create_certificate_chain (); boost::filesystem::path directory_or (boost::optional dir, boost::filesystem::path a) const; void add_to_history_internal (std::vector& h, boost::filesystem::path p); @@ -1394,8 +1379,6 @@ private: */ boost::optional _default_kdm_directory; bool _upload_after_make_dcp; - std::list> _cinemas; - std::list> _dkdm_recipients; std::string _mail_server; int _mail_port; EmailProtocol _mail_protocol; diff --git a/src/lib/dkdm_recipient.cc b/src/lib/dkdm_recipient.cc index 4e56a37c4..83ba96de6 100644 --- a/src/lib/dkdm_recipient.cc +++ b/src/lib/dkdm_recipient.cc @@ -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 film, boost::filesystem::path cpl, - shared_ptr 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(name_values, nullptr, recipient->emails, kdm); + return make_shared(name_values, CinemaID(0), recipient.emails, kdm); } diff --git a/src/lib/dkdm_recipient.h b/src/lib/dkdm_recipient.h index 3317ae6f9..64da41cff 100644 --- a/src/lib/dkdm_recipient.h +++ b/src/lib/dkdm_recipient.h @@ -41,10 +41,6 @@ public: } - explicit DKDMRecipient (cxml::ConstNodePtr); - - void as_xml (xmlpp::Element *) const override; - std::vector emails; }; @@ -53,7 +49,7 @@ KDMWithMetadataPtr kdm_for_dkdm_recipient ( std::shared_ptr film, boost::filesystem::path cpl, - std::shared_ptr 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 index 000000000..34179337e --- /dev/null +++ b/src/lib/dkdm_recipient_list.cc @@ -0,0 +1,243 @@ +/* + Copyright (C) 2024 Carl Hetherington + + 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 . + +*/ + + +#include "config.h" +#include "dkdm_recipient.h" +#include "dkdm_recipient_list.h" +#include "sqlite_statement.h" +#include "sqlite_transaction.h" +#include "util.h" +#include + + +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 emails; + for (auto email_node: recipient_node->node_children("Email")) { + emails.push_back(email_node->content()); + } + + optional 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> +dkdm_recipients_from_result(SQLiteStatement& statement) +{ + vector> 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 certificate = certificate_string.empty() ? optional() : dcp::Certificate(certificate_string); + auto const join_with_spaces = statement.column_text(4); + vector 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> +DKDMRecipientList::dkdm_recipients() const +{ + SQLiteStatement statement(_db, _dkdm_recipients.select("ORDER BY name ASC")); + return dkdm_recipients_from_result(statement); +} + + +boost::optional +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 index 000000000..fc4d84b60 --- /dev/null +++ b/src/lib/dkdm_recipient_list.h @@ -0,0 +1,90 @@ +/* + Copyright (C) 2024 Carl Hetherington + + 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 . + +*/ + + +#ifndef DCPOMATIC_DKDM_RECIPIENT_LIST_H +#define DCPOMATIC_DKDM_RECIPIENT_LIST_H + + +#include "id.h" +#include "sqlite_table.h" +#include +#include +#include + + +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> dkdm_recipients() const; + boost::optional 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 + diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h index 10231f59a..e08392689 100644 --- a/src/lib/exceptions.h +++ b/src/lib/exceptions.h @@ -34,6 +34,7 @@ extern "C" { } #include #include +#include #include #include @@ -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 index 000000000..2891fb4ab --- /dev/null +++ b/src/lib/id.cc @@ -0,0 +1,30 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#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 index 000000000..ca4721039 --- /dev/null +++ b/src/lib/id.h @@ -0,0 +1,48 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#ifndef DCPOMATIC_ID_H +#define DCPOMATIC_ID_H + + +#include + + +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 diff --git a/src/lib/kdm_cli.cc b/src/lib/kdm_cli.cc index 21e8c75d3..9d5b54b9a 100644 --- a/src/lib/kdm_cli.cc +++ b/src/lib/kdm_cli.cc @@ -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 -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> screens, + vector const& screens, boost::filesystem::path film_dir, bool verbose, boost::filesystem::path output, @@ -241,11 +236,22 @@ from_film ( try { list kdms; - for (auto i: screens) { + for (auto screen_details: screens) { std::function 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> screens, + vector const& screens, dcp::DecryptedKDM dkdm, bool verbose, boost::filesystem::path output, @@ -370,15 +376,15 @@ from_dkdm ( try { list 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(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector(), kdm)); + kdms.push_back(make_shared(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 cinema_name; - shared_ptr cinema; + /* either a screen name to search for, or the name of a screen to associate with certificate */ + optional screen_name; + /* a certificate that we will use to make up a temporary cinema and screen */ optional projector_certificate; optional decryption_key; - optional screen; - vector> screens; + /* trusted devices that we will use to make up a temporary cinema and screen */ + vector trusted_devices; optional dkdm; optional valid_from; optional 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(optarg, vector(), "", 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 temp_cinema; + optional 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.get_value_or(""), "", chain.leaf(), boost::none, vector()); - 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 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 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) { diff --git a/src/lib/kdm_with_metadata.h b/src/lib/kdm_with_metadata.h index fbeeffbc1..6198564b1 100644 --- a/src/lib/kdm_with_metadata.h +++ b/src/lib/kdm_with_metadata.h @@ -23,6 +23,7 @@ #define DCPOMATIC_KDM_WITH_METADATA_H +#include "id.h" #include #include @@ -33,7 +34,7 @@ class Cinema; class KDMWithMetadata { public: - KDMWithMetadata(dcp::NameFormat::Map const& name_values, void const* group, std::vector emails, dcp::EncryptedKDM kdm) + KDMWithMetadata(dcp::NameFormat::Map const& name_values, ID group, std::vector emails, dcp::EncryptedKDM kdm) : _name_values (name_values) , _group (group) , _emails (emails) @@ -54,7 +55,7 @@ public: boost::optional 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 _emails; dcp::EncryptedKDM _kdm; }; diff --git a/src/lib/screen.cc b/src/lib/screen.cc index 38c474850..b77eb6b52 100644 --- a/src/lib/screen.cc +++ b/src/lib/screen.cc @@ -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 Screen::trusted_device_thumbprints () const { @@ -76,7 +54,9 @@ Screen::trusted_device_thumbprints () const KDMWithMetadataPtr kdm_for_screen ( std::function make_kdm, - shared_ptr 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& 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(name_values, cinema.get(), cinema ? cinema->emails : vector(), kdm); + return make_shared(name_values, cinema_id, cinema.emails, kdm); } diff --git a/src/lib/screen.h b/src/lib/screen.h index 0a275aa34..89ebc3ab4 100644 --- a/src/lib/screen.h +++ b/src/lib/screen.h @@ -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 trusted_device_thumbprints () const; - - std::shared_ptr cinema; std::vector trusted_devices; }; @@ -78,7 +74,9 @@ public: KDMWithMetadataPtr kdm_for_screen ( std::function make_kdm, - std::shared_ptr 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 index 000000000..b3ec1fb81 --- /dev/null +++ b/src/lib/sqlite_statement.cc @@ -0,0 +1,111 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#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 row, function 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(sqlite3_column_text(_stmt, index)); +} + diff --git a/src/lib/sqlite_statement.h b/src/lib/sqlite_statement.h new file mode 100644 index 000000000..3c2246efb --- /dev/null +++ b/src/lib/sqlite_statement.h @@ -0,0 +1,50 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#include +#include +#include + + +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 row = std::function(), std::function busy = std::function()); + + 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 index 000000000..f00fa6b74 --- /dev/null +++ b/src/lib/sqlite_table.cc @@ -0,0 +1,79 @@ +/* + Copyright (C) 2024 Carl Hetherington + + 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 . + +*/ + + +#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 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 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 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 index 000000000..43c9491ed --- /dev/null +++ b/src/lib/sqlite_table.h @@ -0,0 +1,54 @@ +/* + Copyright (C) 2024 Carl Hetherington + + 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 . + +*/ + + +#ifndef DCPOMATIC_SQLITE_TABLE_H +#define DCPOMATIC_SQLITE_TABLE_H + +#include +#include + + +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 _columns; + std::vector _types; +}; + + +#endif + diff --git a/src/lib/sqlite_transaction.cc b/src/lib/sqlite_transaction.cc new file mode 100644 index 000000000..239d85020 --- /dev/null +++ b/src/lib/sqlite_transaction.cc @@ -0,0 +1,50 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#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 index 000000000..0f6319243 --- /dev/null +++ b/src/lib/sqlite_transaction.h @@ -0,0 +1,40 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#include + + +class SQLiteTransaction +{ +public: + SQLiteTransaction(sqlite3* db); + ~SQLiteTransaction(); + + SQLiteTransaction(SQLiteTransaction const&) = delete; + SQLiteTransaction& operator=(SQLiteTransaction const&) = delete; + + void commit(); + +private: + sqlite3* _db; + bool _rollback = true; +}; + diff --git a/src/lib/unzipper.cc b/src/lib/unzipper.cc index f0170e7e0..8d468f24f 100644 --- a/src/lib/unzipper.cc +++ b/src/lib/unzipper.cc @@ -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) { diff --git a/src/lib/unzipper.h b/src/lib/unzipper.h index 7cab6e5f4..76b2fe45a 100644 --- a/src/lib/unzipper.h +++ b/src/lib/unzipper.h @@ -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; diff --git a/src/lib/util.cc b/src/lib/util.cc index 2f5c1ce49..282011d65 100644 --- a/src/lib/util.cc +++ b/src/lib/util.cc @@ -89,10 +89,11 @@ LIBDCP_ENABLE_WARNINGS #include #endif #include +#include #include #include #include -#include +#include #include #ifdef DCPOMATIC_POSIX #include @@ -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 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 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; + }); +} + diff --git a/src/lib/util.h b/src/lib/util.h index eac855bef..7c40c5ce8 100644 --- a/src/lib/util.h +++ b/src/lib/util.h @@ -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 const& in, std::string const& separator = " "); template diff --git a/src/lib/wscript b/src/lib/wscript index a4758c737..68b988eb3 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -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: diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc index 1c43d8a07..85fc96c3e 100644 --- a/src/tools/dcpomatic.cc +++ b/src/tools/dcpomatic.cc @@ -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 } diff --git a/src/tools/dcpomatic_batch.cc b/src/tools/dcpomatic_batch.cc index 0a228b933..32e8dec08 100644 --- a/src/tools/dcpomatic_batch.cc +++ b/src/tools/dcpomatic_batch.cc @@ -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 } diff --git a/src/tools/dcpomatic_kdm.cc b/src/tools/dcpomatic_kdm.cc index 6f8c3c71e..af16f57aa 100644 --- a/src/tools/dcpomatic_kdm.cc +++ b/src/tools/dcpomatic_kdm.cc @@ -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(), diff --git a/src/tools/wscript b/src/tools/wscript index a79995480..e6d7c4be1 100644 --- a/src/tools/wscript +++ b/src/tools/wscript @@ -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: diff --git a/src/wx/kdm_dialog.cc b/src/wx/kdm_dialog.cc index e6f4eca5e..1a9c564d4 100644 --- a/src/wx/kdm_dialog.cc +++ b/src/wx/kdm_dialog.cc @@ -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 index 000000000..a7d573ded --- /dev/null +++ b/src/wx/load_config_from_zip_dialog.cc @@ -0,0 +1,59 @@ +/* + Copyright (C) 2024 Carl Hetherington + + 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 . + +*/ + + +#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 index 000000000..f5f4ec6ea --- /dev/null +++ b/src/wx/load_config_from_zip_dialog.h @@ -0,0 +1,41 @@ +/* + Copyright (C) 2024 Carl Hetherington + + 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 . + +*/ + + +#include "table_dialog.h" +#include "lib/config.h" +#include + + +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; +}; + + + diff --git a/src/wx/recipients_panel.cc b/src/wx/recipients_panel.cc index 04ad0dd6e..9596c3cfd 100644 --- a/src/wx/recipients_panel.cc +++ b/src/wx/recipients_panel.cc @@ -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 #include @@ -103,15 +103,15 @@ RecipientsPanel::setup_sensitivity () void -RecipientsPanel::add_recipient (shared_ptr 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(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> +list RecipientsPanel::recipients () const { - list> r; - - for (auto const& i: _selected) { - r.push_back (i.second); + list 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); } } diff --git a/src/wx/recipients_panel.h b/src/wx/recipients_panel.h index 6e1f1408f..d252b8d06 100644 --- a/src/wx/recipients_panel.h +++ b/src/wx/recipients_panel.h @@ -21,6 +21,7 @@ #include "lib/collator.h" #include "lib/dkdm_recipient.h" +#include "lib/dkdm_recipient_list.h" #include LIBDCP_DISABLE_WARNINGS #include @@ -43,12 +44,12 @@ public: void setup_sensitivity (); - std::list> recipients () const; + std::list recipients() const; boost::signals2::signal RecipientsChanged; private: void add_recipients (); - void add_recipient (std::shared_ptr); + 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> RecipientMap; + typedef std::map RecipientMap; RecipientMap _recipients; RecipientMap _selected; diff --git a/src/wx/screens_panel.cc b/src/wx/screens_panel.cc index 768d25092..fdc4dacc3 100644 --- a/src/wx/screens_panel.cc +++ b/src/wx/screens_panel.cc @@ -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 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 -ScreensPanel::add_cinema (shared_ptr 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 screen) { - return _checked_screens.find(screen) != _checked_screens.end(); + auto iter = std::find_if(screens.begin(), screens.end(), [this](pair const& screen) { + auto iter2 = std::find_if(_checked_screens.begin(), _checked_screens.end(), [screen](pair 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, 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 -ScreensPanel::add_screen (shared_ptr cinema, shared_ptr 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(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 +optional 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) +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) 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(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 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 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 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> +std::set> ScreensPanel::screens () const { - vector> 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> -ScreensPanel::sorted_cinemas() const -{ - auto cinemas = Config::instance()->cinemas(); - - cinemas.sort( - [this](shared_ptr a, shared_ptr 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 +optional 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 +optional> 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 -ScreensPanel::cinema_to_item (shared_ptr 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) const optional -ScreensPanel::screen_to_item (shared_ptr 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) 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 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()); } diff --git a/src/wx/screens_panel.h b/src/wx/screens_panel.h index 1e07b6236..98ec2c631 100644 --- a/src/wx/screens_panel.h +++ b/src/wx/screens_panel.h @@ -19,6 +19,7 @@ */ +#include "lib/cinema_list.h" #include "lib/collator.h" #include "lib/config.h" #include @@ -38,7 +39,6 @@ namespace dcpomatic { } -class Cinema; class CheckBox; @@ -48,7 +48,7 @@ public: explicit ScreensPanel (wxWindow* parent); ~ScreensPanel (); - std::vector> screens () const; + std::set> screens() const; void setup_sensitivity (); dcp::UTCOffset best_utc_offset() const; @@ -57,38 +57,35 @@ public: private: void add_cinemas (); - boost::optional add_cinema (std::shared_ptr, wxTreeListItem previous); - boost::optional add_screen (std::shared_ptr, std::shared_ptr); + boost::optional add_cinema(CinemaID cinema, wxTreeListItem previous); + boost::optional add_screen(CinemaID cinema, ScreenID screen); void add_cinema_clicked (); void edit_cinema_clicked (); - void edit_cinema(std::shared_ptr 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 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_for_operation () const; + boost::optional 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 cinema, std::string search); - std::list> sorted_cinemas() const; + bool matches_search(Cinema const& cinema, std::string search); void setup_show_only_checked(); - std::shared_ptr item_to_cinema (wxTreeListItem item) const; - std::shared_ptr item_to_screen (wxTreeListItem item) const; - boost::optional cinema_to_item (std::shared_ptr cinema) const; - boost::optional screen_to_item (std::shared_ptr screen) const; + boost::optional item_to_cinema(wxTreeListItem item) const; + boost::optional> item_to_screen(wxTreeListItem item) const; + boost::optional cinema_to_item(CinemaID cinema) const; + boost::optional 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> _selected_cinemas; - std::vector> _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> _checked_screens; + std::vector _selected_cinemas; + /* List of cinema_id, screen_id */ + std::vector> _selected_screens; + /* Likewise with checked screens */ + std::set> _checked_screens; - std::map> _item_to_cinema; - std::map> _item_to_screen; - std::map, wxTreeListItem> _cinema_to_item; - std::map, wxTreeListItem> _screen_to_item; + std::map _item_to_cinema; + std::map> _item_to_screen; + std::map _cinema_to_item; + std::map _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; }; diff --git a/src/wx/wscript b/src/wx/wscript index b37d26d6d..a41e3827e 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -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 diff --git a/src/wx/wx_util.cc b/src/wx/wx_util.cc index 7d8fb0a76..1c04a4755 100644 --- a/src/wx/wx_util.cc +++ b/src/wx/wx_util.cc @@ -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 index 000000000..4aa9fa4ed --- /dev/null +++ b/test/cinema_list_test.cc @@ -0,0 +1,225 @@ +/* + Copyright (C) 2023 Carl Hetherington + + 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 . + +*/ + + +#include "lib/cinema.h" +#include "lib/cinema_list.h" +#include "lib/config.h" +#include "lib/screen.h" +#include +#include +#include +#include +#include +#include +#include + + +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{"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{"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{"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{"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{"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{"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{"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{} + )); + + 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="); +} + diff --git a/test/config_test.cc b/test/config_test.cc index d50c3d6f3..1c35f0a78 100644 --- a/test/config_test.cc +++ b/test/config_test.cc @@ -20,7 +20,12 @@ #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 #include @@ -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("My Great Cinema", vector(), "", 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", ""); + 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()); +} diff --git a/test/data b/test/data index 351419eeb..b4fe92664 160000 --- 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 index 000000000..20f669e76 --- /dev/null +++ b/test/dkdm_recipient_list_test.cc @@ -0,0 +1,56 @@ +/* + Copyright (C) 2024 Carl Hetherington + + 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 . + +*/ + + +#include "lib/config.h" +#include "lib/dkdm_recipient.h" +#include "lib/dkdm_recipient_list.h" +#include +#include + + +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; +} + + diff --git a/test/kdm_cli_test.cc b/test/kdm_cli_test.cc index 1c590cdf7..3fd20d478 100644 --- a/test/kdm_cli_test.cc +++ b/test/kdm_cli_test.cc @@ -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("Dean's Screens", vector(), "", dcp::UTCOffset()); - cinema_a->add_screen(std::make_shared("Screen 1", "", cert, boost::none, std::vector())); - cinema_a->add_screen(std::make_shared("Screen 2", "", cert, boost::none, std::vector())); - cinema_a->add_screen(std::make_shared("Screen 3", "", cert, boost::none, std::vector())); - config->add_cinema(cinema_a); + CinemaList cinemas(config->cinemas_file()); - auto cinema_b = std::make_shared("Floyd's Celluloid", vector(), "", dcp::UTCOffset()); - cinema_b->add_screen(std::make_shared("Foo", "", cert, boost::none, std::vector())); - cinema_b->add_screen(std::make_shared("Bar", "", cert, boost::none, std::vector())); - 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()}); + cinemas.add_screen(cinema_b, {"Bar", "", cert, boost::none, std::vector()}); } @@ -250,7 +251,7 @@ BOOST_AUTO_TEST_CASE(kdm_cli_specify_cinemas_file) vector 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)"); } diff --git a/test/kdm_naming_test.cc b/test/kdm_naming_test.cc index f73e4295e..c5c7c2678 100644 --- a/test/kdm_naming_test.cc +++ b/test/kdm_naming_test.cc @@ -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 cinema_a_screen_1; -static shared_ptr cinema_a_screen_2; -static shared_ptr cinema_b_screen_x; -static shared_ptr cinema_b_screen_y; -static shared_ptr 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 A", vector(), "", dcp::UTCOffset{4, 30}); - cinema_a_screen_1 = std::make_shared("Screen 1", "", crypt_cert, boost::none, vector()); - cinema_a->add_screen (cinema_a_screen_1); - cinema_a_screen_2 = std::make_shared("Screen 2", "", crypt_cert, boost::none, vector()); - cinema_a->add_screen (cinema_a_screen_2); - c->add_cinema (cinema_a); - - auto cinema_b = make_shared("Cinema B", vector(), "", dcp::UTCOffset{-1, 0}); - cinema_b_screen_x = std::make_shared("Screen X", "", crypt_cert, boost::none, vector()); - cinema_b->add_screen (cinema_b_screen_x); - cinema_b_screen_y = std::make_shared("Screen Y", "", crypt_cert, boost::none, vector()); - cinema_b->add_screen (cinema_b_screen_y); - cinema_b_screen_z = std::make_shared("Screen Z", "", crypt_cert, boost::none, vector()); - 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> screens = { - cinema_a_screen_2, cinema_b_screen_x, cinema_a_screen_1, (cinema_b_screen_z) + vector> 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, diff --git a/test/recover_test.cc b/test/recover_test.cc index 01b71a05e..696a2c36a 100644 --- a/test/recover_test.cc +++ b/test/recover_test.cc @@ -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("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("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("test/data/count300bd24.m2ts"); film->examine_and_add_content (content); diff --git a/test/test.cc b/test/test.cc index ddd3d26f0..d214f1d39 100644 --- a/test/test.cc +++ b/test/test.cc @@ -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"); } diff --git a/test/wscript b/test/wscript index 3acca09e8..7555f76db 100644 --- a/test/wscript +++ b/test/wscript @@ -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 65f2e843f..c5996a361 100644 --- 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 + 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')