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