2 Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
6 DCP-o-matic is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 DCP-o-matic is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
23 #include "cinema_list.h"
25 #include "dcpomatic_assert.h"
26 #include "exceptions.h"
28 #include "sqlite_statement.h"
29 #include "sqlite_transaction.h"
31 #include <dcp/certificate.h>
33 #include <boost/algorithm/string.hpp>
42 using boost::optional;
45 CinemaList::CinemaList()
48 , _trusted_devices("trusted_devices")
51 setup(Config::instance()->cinemas_file());
55 CinemaList::CinemaList(boost::filesystem::path db_file)
58 , _trusted_devices("trusted_devices")
66 CinemaList::setup_tables()
68 _cinemas.add_column("name", "TEXT");
69 _cinemas.add_column("emails", "TEXT");
70 _cinemas.add_column("notes", "TEXT");
71 _cinemas.add_column("utc_offset_hour", "INTEGER");
72 _cinemas.add_column("utc_offset_minute", "INTEGER");
74 _screens.add_column("cinema", "INTEGER");
75 _screens.add_column("name", "TEXT");
76 _screens.add_column("notes", "TEXT");
77 _screens.add_column("recipient", "TEXT");
78 _screens.add_column("recipient_file", "TEXT");
80 _trusted_devices.add_column("screen", "INTEGER");
81 _trusted_devices.add_column("certificate_or_thumbprint", "TEXT");
86 CinemaList::read_legacy_file(boost::filesystem::path xml_file)
88 cxml::Document doc("Cinemas");
89 doc.read_file(xml_file);
90 read_legacy_document(doc);
95 CinemaList::read_legacy_string(std::string const& xml)
97 cxml::Document doc("Cinemas");
99 read_legacy_document(doc);
104 CinemaList::read_legacy_document(cxml::Document const& doc)
106 for (auto cinema_node: doc.node_children("Cinema")) {
107 vector<string> emails;
108 for (auto email_node: cinema_node->node_children("Email")) {
109 emails.push_back(email_node->content());
113 if (cinema_node->optional_number_child<int>("UTCOffset")) {
114 hour = cinema_node->number_child<int>("UTCOffset");
116 hour = cinema_node->optional_number_child<int>("UTCOffsetHour").get_value_or(0);
119 int minute = cinema_node->optional_number_child<int>("UTCOffsetMinute").get_value_or(0);
122 cinema_node->string_child("Name"),
124 cinema_node->string_child("Notes"),
125 dcp::UTCOffset(hour, minute)
128 auto cinema_id = add_cinema(cinema);
130 for (auto screen_node: cinema_node->node_children("Screen")) {
131 optional<dcp::Certificate> recipient;
132 if (auto recipient_string = screen_node->optional_string_child("Recipient")) {
133 recipient = dcp::Certificate(*recipient_string);
135 vector<TrustedDevice> trusted_devices;
136 for (auto trusted_device_node: screen_node->node_children("TrustedDevice")) {
137 trusted_devices.push_back(TrustedDevice(trusted_device_node->content()));
139 dcpomatic::Screen screen(
140 screen_node->string_child("Name"),
141 screen_node->string_child("Notes"),
143 screen_node->optional_string_child("RecipientFile"),
146 add_screen(cinema_id, screen);
155 for (auto table: { "cinemas", "screens", "trusted_devices" }) {
156 SQLiteStatement sql(_db, String::compose("DELETE FROM %1", table));
163 CinemaList::setup(boost::filesystem::path db_file)
165 #ifdef DCPOMATIC_WINDOWS
166 auto rc = sqlite3_open16(db_file.c_str(), &_db);
168 auto rc = sqlite3_open(db_file.c_str(), &_db);
170 if (rc != SQLITE_OK) {
171 throw FileError("Could not open SQLite database", db_file);
174 sqlite3_busy_timeout(_db, 500);
176 SQLiteStatement cinemas(_db, _cinemas.create());
179 SQLiteStatement screens(_db, _screens.create());
182 SQLiteStatement devices(_db, _trusted_devices.create());
187 CinemaList::CinemaList(CinemaList&& other)
189 , _cinemas(std::move(other._cinemas))
190 , _screens(std::move(other._screens))
191 , _trusted_devices(std::move(other._trusted_devices))
198 CinemaList::operator=(CinemaList&& other)
200 if (this != &other) {
209 CinemaList::add_cinema(Cinema const& cinema)
211 SQLiteStatement statement(_db, _cinemas.insert());
213 statement.bind_text(1, cinema.name);
214 statement.bind_text(2, join_strings(cinema.emails));
215 statement.bind_text(3, cinema.notes);
216 statement.bind_int64(4, cinema.utc_offset.hour());
217 statement.bind_int64(5, cinema.utc_offset.minute());
221 return sqlite3_last_insert_rowid(_db);
226 CinemaList::update_cinema(CinemaID id, Cinema const& cinema)
228 SQLiteStatement statement(_db, _cinemas.update("WHERE id=?"));
230 statement.bind_text(1, cinema.name);
231 statement.bind_text(2, join_strings(cinema.emails));
232 statement.bind_text(3, cinema.notes);
233 statement.bind_int64(4, cinema.utc_offset.hour());
234 statement.bind_int64(5, cinema.utc_offset.minute());
235 statement.bind_int64(6, id.get());
242 CinemaList::remove_cinema(CinemaID id)
244 SQLiteStatement statement(_db, "DELETE FROM cinemas WHERE ID=?");
245 statement.bind_int64(1, id.get());
250 CinemaList::~CinemaList()
259 vector<pair<CinemaID, Cinema>>
260 cinemas_from_result(SQLiteStatement& statement)
262 vector<pair<CinemaID, Cinema>> output;
264 statement.execute([&output](SQLiteStatement& statement) {
265 DCPOMATIC_ASSERT(statement.data_count() == 6);
266 CinemaID const id = statement.column_int64(0);
267 auto const name = statement.column_text(1);
268 auto const join_strings = statement.column_text(2);
269 vector<string> emails;
270 boost::algorithm::split(emails, join_strings, boost::is_any_of(" "));
271 auto const notes = statement.column_text(3);
272 auto const utc_offset_hour = static_cast<int>(statement.column_int64(4));
273 auto const utc_offset_minute = static_cast<int>(statement.column_int64(5));
274 output.push_back(make_pair(id, Cinema(name, { emails }, notes, dcp::UTCOffset{utc_offset_hour, utc_offset_minute})));
281 vector<pair<CinemaID, Cinema>>
282 CinemaList::cinemas() const
284 SQLiteStatement statement(_db, _cinemas.select("ORDER BY name ASC"));
285 return cinemas_from_result(statement);
290 CinemaList::cinema(CinemaID id) const
292 SQLiteStatement statement(_db, _cinemas.select("WHERE id=?"));
293 statement.bind_int64(1, id.get());
294 auto result = cinemas_from_result(statement);
295 if (result.empty()) {
298 return result[0].second;
302 CinemaList::add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen)
304 SQLiteTransaction transaction(_db);
306 SQLiteStatement add_screen(_db, _screens.insert());
308 add_screen.bind_int64(1, cinema_id.get());
309 add_screen.bind_text(2, screen.name);
310 add_screen.bind_text(3, screen.notes);
311 add_screen.bind_text(4, screen.recipient->certificate(true));
312 add_screen.bind_text(5, screen.recipient_file.get_value_or(""));
314 add_screen.execute();
316 auto const screen_id = sqlite3_last_insert_rowid(_db);
318 for (auto device: screen.trusted_devices) {
319 SQLiteStatement add_device(_db, _trusted_devices.insert());
320 add_device.bind_int64(1, screen_id);
321 add_device.bind_text(2, device.as_string());
324 transaction.commit();
331 CinemaList::screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const
333 auto certificate_string = statement.column_text(4);
334 optional<dcp::Certificate> certificate = certificate_string.empty() ? optional<dcp::Certificate>() : dcp::Certificate(certificate_string);
335 auto recipient_file_string = statement.column_text(5);
336 optional<string> recipient_file = recipient_file_string.empty() ? optional<string>() : recipient_file_string;
338 SQLiteStatement trusted_devices_statement(_db, _trusted_devices.select("WHERE screen=?"));
339 trusted_devices_statement.bind_int64(1, screen_id.get());
340 vector<TrustedDevice> trusted_devices;
341 trusted_devices_statement.execute([&trusted_devices](SQLiteStatement& statement) {
342 DCPOMATIC_ASSERT(statement.data_count() == 1);
343 auto description = statement.column_text(1);
344 if (boost::algorithm::starts_with(description, "-----BEGIN CERTIFICATE")) {
345 trusted_devices.push_back(TrustedDevice(dcp::Certificate(description)));
347 trusted_devices.push_back(TrustedDevice(description));
351 return dcpomatic::Screen(statement.column_text(2), statement.column_text(3), certificate, recipient_file, trusted_devices);
355 optional<dcpomatic::Screen>
356 CinemaList::screen(ScreenID screen_id) const
358 SQLiteStatement statement(_db, _screens.select("WHERE id=?"));
359 statement.bind_int64(1, screen_id.get());
361 optional<dcpomatic::Screen> output;
363 statement.execute([this, &output, screen_id](SQLiteStatement& statement) {
364 DCPOMATIC_ASSERT(statement.data_count() == 6);
365 output = screen_from_result(statement, screen_id);
373 vector<pair<ScreenID, dcpomatic::Screen>>
374 CinemaList::screens_from_result(SQLiteStatement& statement) const
376 vector<pair<ScreenID, dcpomatic::Screen>> output;
378 statement.execute([this, &output](SQLiteStatement& statement) {
379 DCPOMATIC_ASSERT(statement.data_count() == 6);
380 ScreenID const screen_id = statement.column_int64(0);
381 output.push_back({screen_id, screen_from_result(statement, screen_id)});
388 vector<pair<ScreenID, dcpomatic::Screen>>
389 CinemaList::screens(CinemaID cinema_id) const
391 SQLiteStatement statement(_db, _screens.select("WHERE cinema=?"));
392 statement.bind_int64(1, cinema_id.get());
393 return screens_from_result(statement);
397 vector<pair<ScreenID, dcpomatic::Screen>>
398 CinemaList::screens_by_cinema_and_name(CinemaID id, std::string const& name) const
400 SQLiteStatement statement(_db, _screens.select("WHERE cinema=? AND name=?"));
401 statement.bind_int64(1, id.get());
402 statement.bind_text(2, name);
403 return screens_from_result(statement);
407 optional<std::pair<CinemaID, Cinema>>
408 CinemaList::cinema_by_name_or_email(std::string const& text) const
410 SQLiteStatement statement(_db, _cinemas.select("WHERE name LIKE ? OR EMAILS LIKE ?"));
411 auto const wildcard = string("%") + text + "%";
412 statement.bind_text(1, wildcard);
413 statement.bind_text(2, wildcard);
415 auto all = cinemas_from_result(statement);
424 CinemaList::update_screen(ScreenID id, dcpomatic::Screen const& screen)
426 SQLiteStatement statement(_db, _screens.update("WHERE id=?"));
428 statement.bind_text(1, screen.name);
429 statement.bind_text(2, screen.notes);
430 statement.bind_text(3, screen.recipient->certificate(true));
431 statement.bind_text(4, screen.recipient_file.get_value_or(""));
432 statement.bind_int64(5, id.get());
439 CinemaList::remove_screen(ScreenID id)
441 SQLiteStatement statement(_db, "DELETE FROM screens WHERE ID=?");
442 statement.bind_int64(1, id.get());
447 optional<dcp::UTCOffset>
448 CinemaList::unique_utc_offset(std::set<CinemaID> const& cinemas_to_check)
450 optional<dcp::UTCOffset> offset;
452 for (auto const& cinema: cinemas()) {
453 if (cinemas_to_check.find(cinema.first) == cinemas_to_check.end()) {
458 offset = cinema.second.utc_offset;
459 } else if (cinema.second.utc_offset != *offset) {
460 return dcp::UTCOffset();