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