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