WIP: stop using video directory and hard-linking (#2756).
[dcpomatic.git] / src / lib / cinema_list.cc
1 /*
2     Copyright (C) 2023 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
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.
10
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.
15
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/>.
18
19 */
20
21
22 #include "cinema.h"
23 #include "cinema_list.h"
24 #include "config.h"
25 #include "dcpomatic_assert.h"
26 #include "exceptions.h"
27 #include "screen.h"
28 #include "sqlite_statement.h"
29 #include "sqlite_transaction.h"
30 #include "util.h"
31 #include <dcp/certificate.h>
32 #include <sqlite3.h>
33 #include <boost/algorithm/string.hpp>
34 #include <iostream>
35 #include <numeric>
36
37
38 using std::pair;
39 using std::make_pair;
40 using std::string;
41 using std::vector;
42 using boost::optional;
43
44
45 CinemaList::CinemaList()
46         : _cinemas("cinemas")
47         , _screens("screens")
48         , _trusted_devices("trusted_devices")
49 {
50         setup_tables();
51         setup(Config::instance()->cinemas_file());
52 }
53
54
55 CinemaList::CinemaList(boost::filesystem::path db_file)
56         : _cinemas("cinemas")
57         , _screens("screens")
58         , _trusted_devices("trusted_devices")
59 {
60         setup_tables();
61         setup(db_file);
62 }
63
64
65 void
66 CinemaList::setup_tables()
67 {
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");
73
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");
79
80         _trusted_devices.add_column("screen", "INTEGER");
81         _trusted_devices.add_column("certificate_or_thumbprint", "TEXT");
82 }
83
84
85 void
86 CinemaList::read_legacy_file(boost::filesystem::path xml_file)
87 {
88         cxml::Document doc("Cinemas");
89         doc.read_file(xml_file);
90         read_legacy_document(doc);
91 }
92
93
94 void
95 CinemaList::read_legacy_string(std::string const& xml)
96 {
97         cxml::Document doc("Cinemas");
98         doc.read_string(xml);
99         read_legacy_document(doc);
100 }
101
102
103 void
104 CinemaList::read_legacy_document(cxml::Document const& doc)
105 {
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());
110                 }
111
112                 int hour = 0;
113                 if (cinema_node->optional_number_child<int>("UTCOffset")) {
114                         hour = cinema_node->number_child<int>("UTCOffset");
115                 } else {
116                         hour = cinema_node->optional_number_child<int>("UTCOffsetHour").get_value_or(0);
117                 }
118
119                 int minute = cinema_node->optional_number_child<int>("UTCOffsetMinute").get_value_or(0);
120
121                 Cinema cinema(
122                         cinema_node->string_child("Name"),
123                         emails,
124                         cinema_node->string_child("Notes"),
125                         dcp::UTCOffset(hour, minute)
126                         );
127
128                 auto cinema_id = add_cinema(cinema);
129
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);
134                         }
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()));
138                         }
139                         dcpomatic::Screen screen(
140                                 screen_node->string_child("Name"),
141                                 screen_node->string_child("Notes"),
142                                 recipient,
143                                 screen_node->optional_string_child("RecipientFile"),
144                                 trusted_devices
145                                 );
146                         add_screen(cinema_id, screen);
147                 }
148         }
149 }
150
151
152 void
153 CinemaList::clear()
154 {
155         for (auto table: { "cinemas", "screens", "trusted_devices" }) {
156                 SQLiteStatement sql(_db, String::compose("DELETE FROM %1", table));
157                 sql.execute();
158         }
159 }
160
161
162 void
163 CinemaList::setup(boost::filesystem::path db_file)
164 {
165 #ifdef DCPOMATIC_WINDOWS
166         auto rc = sqlite3_open16(db_file.c_str(), &_db);
167 #else
168         auto rc = sqlite3_open(db_file.c_str(), &_db);
169 #endif
170         if (rc != SQLITE_OK) {
171                 throw FileError("Could not open SQLite database", db_file);
172         }
173
174         sqlite3_busy_timeout(_db, 500);
175
176         SQLiteStatement cinemas(_db, _cinemas.create());
177         cinemas.execute();
178
179         SQLiteStatement screens(_db, _screens.create());
180         screens.execute();
181
182         SQLiteStatement devices(_db, _trusted_devices.create());
183         devices.execute();
184 }
185
186
187 CinemaList::CinemaList(CinemaList&& other)
188         : _db(other._db)
189         , _cinemas(std::move(other._cinemas))
190         , _screens(std::move(other._screens))
191         , _trusted_devices(std::move(other._trusted_devices))
192 {
193         other._db = nullptr;
194 }
195
196
197 CinemaList&
198 CinemaList::operator=(CinemaList&& other)
199 {
200         if (this != &other) {
201                 _db = other._db;
202                 other._db = nullptr;
203         }
204         return *this;
205 }
206
207
208 CinemaID
209 CinemaList::add_cinema(Cinema const& cinema)
210 {
211         SQLiteStatement statement(_db, _cinemas.insert());
212
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());
218
219         statement.execute();
220
221         return sqlite3_last_insert_rowid(_db);
222 }
223
224
225 void
226 CinemaList::update_cinema(CinemaID id, Cinema const& cinema)
227 {
228         SQLiteStatement statement(_db, _cinemas.update("WHERE id=?"));
229
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());
236
237         statement.execute();
238 }
239
240
241 void
242 CinemaList::remove_cinema(CinemaID id)
243 {
244         SQLiteStatement statement(_db, "DELETE FROM cinemas WHERE ID=?");
245         statement.bind_int64(1, id.get());
246         statement.execute();
247 }
248
249
250 CinemaList::~CinemaList()
251 {
252         if (_db) {
253                 sqlite3_close(_db);
254         }
255 }
256
257
258 static
259 vector<pair<CinemaID, Cinema>>
260 cinemas_from_result(SQLiteStatement& statement)
261 {
262         vector<pair<CinemaID, Cinema>> output;
263
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})));
275         });
276
277         return output;
278 }
279
280
281 vector<pair<CinemaID, Cinema>>
282 CinemaList::cinemas() const
283 {
284         SQLiteStatement statement(_db, _cinemas.select("ORDER BY name ASC"));
285         return cinemas_from_result(statement);
286 }
287
288
289 optional<Cinema>
290 CinemaList::cinema(CinemaID id) const
291 {
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()) {
296                 return {};
297         }
298         return result[0].second;
299 }
300
301 ScreenID
302 CinemaList::add_screen(CinemaID cinema_id, dcpomatic::Screen const& screen)
303 {
304         SQLiteTransaction transaction(_db);
305
306         SQLiteStatement add_screen(_db, _screens.insert());
307
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(""));
313
314         add_screen.execute();
315
316         auto const screen_id = sqlite3_last_insert_rowid(_db);
317
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());
322         }
323
324         transaction.commit();
325
326         return screen_id;
327 }
328
329
330 dcpomatic::Screen
331 CinemaList::screen_from_result(SQLiteStatement& statement, ScreenID screen_id) const
332 {
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;
337
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)));
346                 } else {
347                         trusted_devices.push_back(TrustedDevice(description));
348                 }
349         });
350
351         return dcpomatic::Screen(statement.column_text(2), statement.column_text(3), certificate, recipient_file, trusted_devices);
352 }
353
354
355 optional<dcpomatic::Screen>
356 CinemaList::screen(ScreenID screen_id) const
357 {
358         SQLiteStatement statement(_db, _screens.select("WHERE id=?"));
359         statement.bind_int64(1, screen_id.get());
360
361         optional<dcpomatic::Screen> output;
362
363         statement.execute([this, &output, screen_id](SQLiteStatement& statement) {
364                 DCPOMATIC_ASSERT(statement.data_count() == 6);
365                 output = screen_from_result(statement, screen_id);
366         });
367
368         return output;
369 }
370
371
372
373 vector<pair<ScreenID, dcpomatic::Screen>>
374 CinemaList::screens_from_result(SQLiteStatement& statement) const
375 {
376         vector<pair<ScreenID, dcpomatic::Screen>> output;
377
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)});
382         });
383
384         return output;
385 }
386
387
388 vector<pair<ScreenID, dcpomatic::Screen>>
389 CinemaList::screens(CinemaID cinema_id) const
390 {
391         SQLiteStatement statement(_db, _screens.select("WHERE cinema=?"));
392         statement.bind_int64(1, cinema_id.get());
393         return screens_from_result(statement);
394 }
395
396
397 vector<pair<ScreenID, dcpomatic::Screen>>
398 CinemaList::screens_by_cinema_and_name(CinemaID id, std::string const& name) const
399 {
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);
404 }
405
406
407 optional<std::pair<CinemaID, Cinema>>
408 CinemaList::cinema_by_name_or_email(std::string const& text) const
409 {
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);
414
415         auto all = cinemas_from_result(statement);
416         if (all.empty()) {
417                 return {};
418         }
419         return all[0];
420 }
421
422
423 void
424 CinemaList::update_screen(ScreenID id, dcpomatic::Screen const& screen)
425 {
426         SQLiteStatement statement(_db, _screens.update("WHERE id=?"));
427
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());
433
434         statement.execute();
435 }
436
437
438 void
439 CinemaList::remove_screen(ScreenID id)
440 {
441         SQLiteStatement statement(_db, "DELETE FROM screens WHERE ID=?");
442         statement.bind_int64(1, id.get());
443         statement.execute();
444 }
445
446
447 optional<dcp::UTCOffset>
448 CinemaList::unique_utc_offset(std::set<CinemaID> const& cinemas_to_check)
449 {
450         optional<dcp::UTCOffset> offset;
451
452         for (auto const& cinema: cinemas()) {
453                 if (cinemas_to_check.find(cinema.first) == cinemas_to_check.end()) {
454                         continue;
455                 }
456
457                 if (!offset) {
458                         offset = cinema.second.utc_offset;
459                 } else if (cinema.second.utc_offset != *offset) {
460                         return dcp::UTCOffset();
461                 }
462         }
463
464         return offset;
465 }
466