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