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