Move UTC offset for KDMs from the cinema to the point of KDM creation (#2300).
[dcpomatic.git] / src / lib / kdm_cli.cc
1 /*
2     Copyright (C) 2013-2022 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 /** @file  src/tools/dcpomatic_kdm_cli.cc
23  *  @brief Command-line program to generate KDMs.
24  */
25
26
27 #include "cinema.h"
28 #include "config.h"
29 #include "dkdm_wrapper.h"
30 #include "emailer.h"
31 #include "exceptions.h"
32 #include "film.h"
33 #include "kdm_with_metadata.h"
34 #include "screen.h"
35 #include <dcp/certificate.h>
36 #include <dcp/decrypted_kdm.h>
37 #include <dcp/encrypted_kdm.h>
38 #include <getopt.h>
39
40
41 using std::dynamic_pointer_cast;
42 using std::list;
43 using std::make_shared;
44 using std::runtime_error;
45 using std::shared_ptr;
46 using std::string;
47 using std::vector;
48 using boost::optional;
49 using boost::bind;
50 #if BOOST_VERSION >= 106100
51 using namespace boost::placeholders;
52 #endif
53 using namespace dcpomatic;
54
55
56 static void
57 help (std::function<void (string)> out)
58 {
59         out (String::compose("Syntax: %1 [OPTION] <FILM|CPL-ID|DKDM>", program_name));
60         out ("  -h, --help                               show this help");
61         out ("  -o, --output                             output file or directory");
62         out ("  -K, --filename-format                    filename format for KDMs");
63         out ("  -Z, --container-name-format              filename format for ZIP containers");
64         out ("  -f, --valid-from                         valid from time (e.g. \"2013-09-28T01:41:51+04:00\", \"2018-01-01T12:00:30\") or \"now\"");
65         out ("  -t, --valid-to                           valid to time (e.g. \"2014-09-28T01:41:51\")");
66         out ("  -d, --valid-duration                     valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")");
67         out ("  -F, --formulation                        modified-transitional-1, multiple-modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]");
68         out ("  -p, --disable-forensic-marking-picture   disable forensic marking of pictures essences");
69         out ("  -a, --disable-forensic-marking-audio     disable forensic marking of audio essences (optionally above a given channel, e.g 12)");
70         out ("  -e, --email                              email KDMs to cinemas");
71         out ("  -z, --zip                                ZIP each cinema's KDMs into its own file");
72         out ("  -v, --verbose                            be verbose");
73         out ("  -c, --cinema                             cinema name (when using -C) or name/email (to filter cinemas)");
74         out ("  -S, --screen                             screen name (when using -C) or screen name (to filter screens when using -c)");
75         out ("  -C, --certificate                        file containing projector certificate");
76         out ("  -T, --trusted-device                     file containing a trusted device's certificate");
77         out ("      --list-cinemas                       list known cinemas from the DCP-o-matic settings");
78         out ("      --list-dkdm-cpls                     list CPLs for which DCP-o-matic has DKDMs");
79         out ("");
80         out ("CPL-ID must be the ID of a CPL that is mentioned in DCP-o-matic's DKDM list.");
81         out ("");
82         out ("For example:");
83         out ("");
84         out ("Create KDMs for my_great_movie to play in all of Fred's Cinema's screens for the next two weeks and zip them up.");
85         out ("(Fred's Cinema must have been set up in DCP-o-matic's KDM window)");
86         out ("");
87         out (String::compose("\t%1 -c \"Fred's Cinema\" -f now -d \"2 weeks\" -z my_great_movie", program_name));
88 }
89
90
91 class KDMCLIError : public std::runtime_error
92 {
93 public:
94         KDMCLIError (std::string message)
95                 : std::runtime_error (String::compose("%1: %2", program_name, message).c_str())
96         {}
97 };
98
99
100 static boost::posix_time::time_duration
101 duration_from_string (string d)
102 {
103         int N;
104         char unit_buf[64] = "\0";
105         sscanf (d.c_str(), "%d %63s", &N, unit_buf);
106         string const unit (unit_buf);
107
108         if (N == 0) {
109                 throw KDMCLIError (String::compose("could not understand duration \"%1\"", d));
110         }
111
112         if (unit == "year" || unit == "years") {
113                 return boost::posix_time::time_duration (N * 24 * 365, 0, 0, 0);
114         } else if (unit == "week" || unit == "weeks") {
115                 return boost::posix_time::time_duration (N * 24 * 7, 0, 0, 0);
116         } else if (unit == "day" || unit == "days") {
117                 return boost::posix_time::time_duration (N * 24, 0, 0, 0);
118         } else if (unit == "hour" || unit == "hours") {
119                 return boost::posix_time::time_duration (N, 0, 0, 0);
120         }
121
122         throw KDMCLIError (String::compose("could not understand duration \"%1\"", d));
123 }
124
125
126 static bool
127 always_overwrite ()
128 {
129         return true;
130 }
131
132
133 static
134 void
135 write_files (
136         list<KDMWithMetadataPtr> kdms,
137         bool zip,
138         boost::filesystem::path output,
139         dcp::NameFormat container_name_format,
140         dcp::NameFormat filename_format,
141         bool verbose,
142         std::function<void (string)> out
143         )
144 {
145         if (zip) {
146                 int const N = write_zip_files (
147                         collect (kdms),
148                         output,
149                         container_name_format,
150                         filename_format,
151                         bind (&always_overwrite)
152                         );
153
154                 if (verbose) {
155                         out (String::compose("Wrote %1 ZIP files to %2", N, output));
156                 }
157         } else {
158                 int const N = write_files (
159                         kdms, output, filename_format,
160                         bind (&always_overwrite)
161                         );
162
163                 if (verbose) {
164                         out (String::compose("Wrote %1 KDM files to %2", N, output));
165                 }
166         }
167 }
168
169
170 static
171 shared_ptr<Cinema>
172 find_cinema (string cinema_name)
173 {
174         auto cinemas = Config::instance()->cinemas ();
175         auto i = cinemas.begin();
176         while (
177                 i != cinemas.end() &&
178                 (*i)->name != cinema_name &&
179                 find ((*i)->emails.begin(), (*i)->emails.end(), cinema_name) == (*i)->emails.end()) {
180
181                 ++i;
182         }
183
184         if (i == cinemas.end ()) {
185                 throw KDMCLIError (String::compose("could not find cinema \"%1\"", cinema_name));
186         }
187
188         return *i;
189 }
190
191
192 static
193 void
194 from_film (
195         list<shared_ptr<Screen>> screens,
196         boost::filesystem::path film_dir,
197         bool verbose,
198         boost::filesystem::path output,
199         dcp::NameFormat container_name_format,
200         dcp::NameFormat filename_format,
201         dcp::LocalTime valid_from,
202         dcp::LocalTime valid_to,
203         dcp::Formulation formulation,
204         bool disable_forensic_marking_picture,
205         optional<int> disable_forensic_marking_audio,
206         bool email,
207         bool zip,
208         std::function<void (string)> out
209         )
210 {
211         shared_ptr<Film> film;
212         try {
213                 film = make_shared<Film>(film_dir);
214                 film->read_metadata ();
215                 if (verbose) {
216                         out (String::compose("Read film %1", film->name()));
217                 }
218         } catch (std::exception& e) {
219                 throw KDMCLIError (String::compose("error reading film \"%1\" (%2)", film_dir.string(), e.what()));
220         }
221
222         /* XXX: allow specification of this */
223         vector<CPLSummary> cpls = film->cpls ();
224         if (cpls.empty ()) {
225                 throw KDMCLIError ("no CPLs found in film");
226         } else if (cpls.size() > 1) {
227                 throw KDMCLIError ("more than one CPL found in film");
228         }
229
230         auto cpl = cpls.front().cpl_file;
231
232         std::vector<KDMCertificatePeriod> period_checks;
233
234         try {
235                 list<KDMWithMetadataPtr> kdms;
236                 for (auto i: screens) {
237                         std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm = [film, cpl](dcp::LocalTime begin, dcp::LocalTime end) {
238                                 return film->make_kdm(cpl, begin, end);
239                         };
240                         auto p = kdm_for_screen(make_kdm, i, valid_from, valid_to, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio, period_checks);
241                         if (p) {
242                                 kdms.push_back (p);
243                         }
244                 }
245
246
247                 if (find(period_checks.begin(), period_checks.end(), KDMCertificatePeriod::KDM_OUTSIDE_CERTIFICATE) != period_checks.end()) {
248                         throw KDMCLIError(
249                                 "Some KDMs would have validity periods which are completely outside the recipient certificate periods.  Such KDMs are very unlikely to work, so will not be created."
250                                 );
251                 }
252
253                 if (find(period_checks.begin(), period_checks.end(), KDMCertificatePeriod::KDM_OVERLAPS_CERTIFICATE) != period_checks.end()) {
254                         out("For some of these KDMs the recipient certificate's validity period will not cover the whole of the KDM validity period.  This might cause problems with the KDMs.");
255                 }
256
257                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
258                 if (email) {
259                         send_emails ({kdms}, container_name_format, filename_format, film->dcp_name(), {});
260                 }
261         } catch (FileError& e) {
262                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
263         }
264 }
265
266
267 static
268 optional<dcp::EncryptedKDM>
269 sub_find_dkdm (shared_ptr<DKDMGroup> group, string cpl_id)
270 {
271         for (auto i: group->children()) {
272                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
273                 if (g) {
274                         auto dkdm = sub_find_dkdm (g, cpl_id);
275                         if (dkdm) {
276                                 return dkdm;
277                         }
278                 } else {
279                         auto d = dynamic_pointer_cast<DKDM>(i);
280                         assert (d);
281                         if (d->dkdm().cpl_id() == cpl_id) {
282                                 return d->dkdm();
283                         }
284                 }
285         }
286
287         return {};
288 }
289
290
291 static
292 optional<dcp::EncryptedKDM>
293 find_dkdm (string cpl_id)
294 {
295         return sub_find_dkdm (Config::instance()->dkdms(), cpl_id);
296 }
297
298
299 static
300 dcp::EncryptedKDM
301 kdm_from_dkdm (
302         dcp::DecryptedKDM dkdm,
303         dcp::Certificate target,
304         vector<string> trusted_devices,
305         dcp::LocalTime valid_from,
306         dcp::LocalTime valid_to,
307         dcp::Formulation formulation,
308         bool disable_forensic_marking_picture,
309         optional<int> disable_forensic_marking_audio
310         )
311 {
312         /* Signer for new KDM */
313         auto signer = Config::instance()->signer_chain ();
314         if (!signer->valid ()) {
315                 throw KDMCLIError ("signing certificate chain is invalid.");
316         }
317
318         /* Make a new empty KDM and add the keys from the DKDM to it */
319         dcp::DecryptedKDM kdm (
320                 valid_from,
321                 valid_to,
322                 dkdm.annotation_text().get_value_or(""),
323                 dkdm.content_title_text(),
324                 dcp::LocalTime().as_string()
325                 );
326
327         for (auto const& j: dkdm.keys()) {
328                 kdm.add_key(j);
329         }
330
331         return kdm.encrypt (signer, target, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio);
332 }
333
334
335 static
336 void
337 from_dkdm (
338         list<shared_ptr<Screen>> screens,
339         dcp::DecryptedKDM dkdm,
340         bool verbose,
341         boost::filesystem::path output,
342         dcp::NameFormat container_name_format,
343         dcp::NameFormat filename_format,
344         dcp::LocalTime valid_from,
345         dcp::LocalTime valid_to,
346         dcp::Formulation formulation,
347         bool disable_forensic_marking_picture,
348         optional<int> disable_forensic_marking_audio,
349         bool email,
350         bool zip,
351         std::function<void (string)> out
352         )
353 {
354         dcp::NameFormat::Map values;
355
356         try {
357                 list<KDMWithMetadataPtr> kdms;
358                 for (auto i: screens) {
359                         if (!i->recipient) {
360                                 continue;
361                         }
362
363                         auto const kdm = kdm_from_dkdm(
364                                                         dkdm,
365                                                         i->recipient.get(),
366                                                         i->trusted_device_thumbprints(),
367                                                         valid_from,
368                                                         valid_to,
369                                                         formulation,
370                                                         disable_forensic_marking_picture,
371                                                         disable_forensic_marking_audio
372                                                         );
373
374                         dcp::NameFormat::Map name_values;
375                         name_values['c'] = i->cinema ? i->cinema->name : "";
376                         name_values['s'] = i->name;
377                         name_values['f'] = kdm.content_title_text();
378                         name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
379                         name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
380                         name_values['i'] = kdm.cpl_id();
381
382                         kdms.push_back (make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : list<string>(), kdm));
383                 }
384                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
385                 if (email) {
386                         send_emails ({kdms}, container_name_format, filename_format, dkdm.annotation_text().get_value_or(""), {});
387                 }
388         } catch (FileError& e) {
389                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
390         }
391 }
392
393
394 static
395 void
396 dump_dkdm_group (shared_ptr<DKDMGroup> group, int indent, std::function<void (string)> out)
397 {
398         auto const indent_string = string(indent, ' ');
399
400         if (indent > 0) {
401                 out (indent_string + group->name());
402         }
403         for (auto i: group->children()) {
404                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
405                 if (g) {
406                         dump_dkdm_group (g, indent + 2, out);
407                 } else {
408                         auto d = dynamic_pointer_cast<DKDM>(i);
409                         assert(d);
410                         out (indent_string + d->dkdm().cpl_id());
411                 }
412         }
413 }
414
415
416 optional<string>
417 kdm_cli (int argc, char* argv[], std::function<void (string)> out)
418 try
419 {
420         boost::filesystem::path output = boost::filesystem::current_path();
421         auto container_name_format = Config::instance()->kdm_container_name_format();
422         auto filename_format = Config::instance()->kdm_filename_format();
423         optional<string> cinema_name;
424         shared_ptr<Cinema> cinema;
425         optional<boost::filesystem::path> certificate;
426         optional<string> screen;
427         list<shared_ptr<Screen>> screens;
428         optional<dcp::EncryptedKDM> dkdm;
429         optional<dcp::LocalTime> valid_from;
430         optional<dcp::LocalTime> valid_to;
431         bool zip = false;
432         bool list_cinemas = false;
433         bool list_dkdm_cpls = false;
434         optional<string> duration_string;
435         bool verbose = false;
436         dcp::Formulation formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
437         bool disable_forensic_marking_picture = false;
438         optional<int> disable_forensic_marking_audio;
439         bool email = false;
440
441         program_name = argv[0];
442
443         /* Reset getopt() so we can call this method several times in one test process */
444         optind = 1;
445
446         int option_index = 0;
447         while (true) {
448                 static struct option long_options[] = {
449                         { "help", no_argument, 0, 'h'},
450                         { "output", required_argument, 0, 'o'},
451                         { "filename-format", required_argument, 0, 'K'},
452                         { "container-name-format", required_argument, 0, 'Z'},
453                         { "valid-from", required_argument, 0, 'f'},
454                         { "valid-to", required_argument, 0, 't'},
455                         { "valid-duration", required_argument, 0, 'd'},
456                         { "formulation", required_argument, 0, 'F' },
457                         { "disable-forensic-marking-picture", no_argument, 0, 'p' },
458                         { "disable-forensic-marking-audio", optional_argument, 0, 'a' },
459                         { "email", no_argument, 0, 'e' },
460                         { "zip", no_argument, 0, 'z' },
461                         { "verbose", no_argument, 0, 'v' },
462                         { "cinema", required_argument, 0, 'c' },
463                         { "screen", required_argument, 0, 'S' },
464                         { "certificate", required_argument, 0, 'C' },
465                         { "trusted-device", required_argument, 0, 'T' },
466                         { "list-cinemas", no_argument, 0, 'B' },
467                         { "list-dkdm-cpls", no_argument, 0, 'D' },
468                         { 0, 0, 0, 0 }
469                 };
470
471                 int c = getopt_long (argc, argv, "ho:K:Z:f:t:d:F:pae::zvc:S:C:T:BD", long_options, &option_index);
472
473                 if (c == -1) {
474                         break;
475                 }
476
477                 switch (c) {
478                 case 'h':
479                         help (out);
480                         exit (EXIT_SUCCESS);
481                 case 'o':
482                         output = optarg;
483                         break;
484                 case 'K':
485                         filename_format = dcp::NameFormat (optarg);
486                         break;
487                 case 'Z':
488                         container_name_format = dcp::NameFormat (optarg);
489                         break;
490                 case 'f':
491                         if (string(optarg) == "now") {
492                                 valid_from = dcp::LocalTime();
493                         } else {
494                                 valid_from = dcp::LocalTime(optarg);
495                         }
496                         break;
497                 case 't':
498                         valid_to = dcp::LocalTime(optarg);
499                         break;
500                 case 'd':
501                         duration_string = optarg;
502                         break;
503                 case 'F':
504                         if (string(optarg) == "modified-transitional-1") {
505                                 formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
506                         } else if (string(optarg) == "multiple-modified-transitional-1") {
507                                 formulation = dcp::Formulation::MULTIPLE_MODIFIED_TRANSITIONAL_1;
508                         } else if (string(optarg) == "dci-any") {
509                                 formulation = dcp::Formulation::DCI_ANY;
510                         } else if (string(optarg) == "dci-specific") {
511                                 formulation = dcp::Formulation::DCI_SPECIFIC;
512                         } else {
513                                 throw KDMCLIError ("unrecognised KDM formulation " + string (optarg));
514                         }
515                         break;
516                 case 'p':
517                         disable_forensic_marking_picture = true;
518                         break;
519                 case 'a':
520                         disable_forensic_marking_audio = 0;
521                         if (optarg == 0 && argv[optind] != 0 && argv[optind][0] != '-') {
522                                 disable_forensic_marking_audio = atoi (argv[optind++]);
523                         } else if (optarg) {
524                                 disable_forensic_marking_audio = atoi (optarg);
525                         }
526                         break;
527                 case 'e':
528                         email = true;
529                         break;
530                 case 'z':
531                         zip = true;
532                         break;
533                 case 'v':
534                         verbose = true;
535                         break;
536                 case 'c':
537                         /* This could be a cinema to search for in the configured list or the name of a cinema being
538                            built up on-the-fly in the option.  Cater for both possilibities here by storing the name
539                            (for lookup) and by creating a Cinema which the next Screen will be added to.
540                         */
541                         cinema_name = optarg;
542                         cinema = make_shared<Cinema>(optarg, list<string>(), "");
543                         break;
544                 case 'S':
545                         /* Similarly, this could be the name of a new (temporary) screen or the name of a screen
546                          * to search for.
547                          */
548                         screen = optarg;
549                         break;
550                 case 'C':
551                         certificate = optarg;
552                         break;
553                 case 'T':
554                         /* A trusted device ends up in the last screen we made */
555                         if (!screens.empty ()) {
556                                 screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
557                         }
558                         break;
559                 case 'B':
560                         list_cinemas = true;
561                         break;
562                 case 'D':
563                         list_dkdm_cpls = true;
564                         break;
565                 }
566         }
567
568         if (certificate) {
569                 /* Make a new screen and add it to the current cinema */
570                 dcp::CertificateChain chain(dcp::file_to_string(*certificate));
571                 auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>());
572                 if (cinema) {
573                         cinema->add_screen(screen_to_add);
574                 }
575                 screens.push_back(screen_to_add);
576         }
577
578         if (list_cinemas) {
579                 auto cinemas = Config::instance()->cinemas ();
580                 for (auto i: cinemas) {
581                         out (String::compose("%1 (%2)", i->name, Emailer::address_list (i->emails)));
582                 }
583                 exit (EXIT_SUCCESS);
584         }
585
586         if (list_dkdm_cpls) {
587                 dump_dkdm_group (Config::instance()->dkdms(), 0, out);
588                 exit (EXIT_SUCCESS);
589         }
590
591         if (!duration_string && !valid_to) {
592                 throw KDMCLIError ("you must specify a --valid-duration or --valid-to");
593         }
594
595         if (!valid_from) {
596                 throw KDMCLIError ("you must specify --valid-from");
597         }
598
599         if (optind >= argc) {
600                 throw KDMCLIError ("no film, CPL ID or DKDM specified");
601         }
602
603         if (screens.empty()) {
604                 if (!cinema_name) {
605                         throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files");
606                 }
607
608                 screens = find_cinema (*cinema_name)->screens ();
609                 if (screen) {
610                         screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end());
611                 }
612         }
613
614         if (duration_string) {
615                 valid_to = valid_from.get();
616                 valid_to->add(duration_from_string(*duration_string));
617         }
618
619         if (verbose) {
620                 out(String::compose("Making KDMs valid from %1 to %2", valid_from->as_string(), valid_to->as_string()));
621         }
622
623         string const thing = argv[optind];
624         if (boost::filesystem::is_directory(thing) && boost::filesystem::is_regular_file(boost::filesystem::path(thing) / "metadata.xml")) {
625                 from_film (
626                         screens,
627                         thing,
628                         verbose,
629                         output,
630                         container_name_format,
631                         filename_format,
632                         *valid_from,
633                         *valid_to,
634                         formulation,
635                         disable_forensic_marking_picture,
636                         disable_forensic_marking_audio,
637                         email,
638                         zip,
639                         out
640                         );
641         } else {
642                 if (boost::filesystem::is_regular_file(thing)) {
643                         dkdm = dcp::EncryptedKDM (dcp::file_to_string (thing));
644                 } else {
645                         dkdm = find_dkdm (thing);
646                 }
647
648                 if (!dkdm) {
649                         throw KDMCLIError ("could not find film or CPL ID corresponding to " + thing);
650                 }
651
652                 from_dkdm (
653                         screens,
654                         dcp::DecryptedKDM (*dkdm, Config::instance()->decryption_chain()->key().get()),
655                         verbose,
656                         output,
657                         container_name_format,
658                         filename_format,
659                         *valid_from,
660                         *valid_to,
661                         formulation,
662                         disable_forensic_marking_picture,
663                         disable_forensic_marking_audio,
664                         email,
665                         zip,
666                         out
667                         );
668         }
669
670         return {};
671 } catch (std::exception& e) {
672         return string(e.what());
673 }
674