5ea8089674c051377cd045afaf6cc8a95f6d7b12
[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 (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(period_checks.begin(), period_checks.end(), KDMCertificatePeriod::KDM_OUTSIDE_CERTIFICATE) != period_checks.end()) {
261                         throw KDMCLIError(
262                                 "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."
263                                 );
264                 }
265
266                 if (find(period_checks.begin(), period_checks.end(), KDMCertificatePeriod::KDM_OVERLAPS_CERTIFICATE) != period_checks.end()) {
267                         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.");
268                 }
269
270                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
271                 if (email) {
272                         send_emails ({kdms}, container_name_format, filename_format, film->dcp_name(), {});
273                 }
274         } catch (FileError& e) {
275                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
276         }
277 }
278
279
280 static
281 optional<dcp::EncryptedKDM>
282 sub_find_dkdm (shared_ptr<DKDMGroup> group, string cpl_id)
283 {
284         for (auto i: group->children()) {
285                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
286                 if (g) {
287                         auto dkdm = sub_find_dkdm (g, cpl_id);
288                         if (dkdm) {
289                                 return dkdm;
290                         }
291                 } else {
292                         auto d = dynamic_pointer_cast<DKDM>(i);
293                         assert (d);
294                         if (d->dkdm().cpl_id() == cpl_id) {
295                                 return d->dkdm();
296                         }
297                 }
298         }
299
300         return {};
301 }
302
303
304 static
305 optional<dcp::EncryptedKDM>
306 find_dkdm (string cpl_id)
307 {
308         return sub_find_dkdm (Config::instance()->dkdms(), cpl_id);
309 }
310
311
312 static
313 dcp::EncryptedKDM
314 kdm_from_dkdm (
315         dcp::DecryptedKDM dkdm,
316         dcp::Certificate target,
317         vector<string> trusted_devices,
318         dcp::LocalTime valid_from,
319         dcp::LocalTime valid_to,
320         dcp::Formulation formulation,
321         bool disable_forensic_marking_picture,
322         optional<int> disable_forensic_marking_audio
323         )
324 {
325         /* Signer for new KDM */
326         auto signer = Config::instance()->signer_chain ();
327         if (!signer->valid ()) {
328                 throw KDMCLIError ("signing certificate chain is invalid.");
329         }
330
331         /* Make a new empty KDM and add the keys from the DKDM to it */
332         dcp::DecryptedKDM kdm (
333                 valid_from,
334                 valid_to,
335                 dkdm.annotation_text().get_value_or(""),
336                 dkdm.content_title_text(),
337                 dcp::LocalTime().as_string()
338                 );
339
340         for (auto const& j: dkdm.keys()) {
341                 kdm.add_key(j);
342         }
343
344         return kdm.encrypt (signer, target, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio);
345 }
346
347
348 static
349 void
350 from_dkdm (
351         vector<shared_ptr<Screen>> screens,
352         dcp::DecryptedKDM dkdm,
353         bool verbose,
354         boost::filesystem::path output,
355         dcp::NameFormat container_name_format,
356         dcp::NameFormat filename_format,
357         boost::posix_time::ptime valid_from,
358         boost::posix_time::ptime valid_to,
359         dcp::Formulation formulation,
360         bool disable_forensic_marking_picture,
361         optional<int> disable_forensic_marking_audio,
362         bool email,
363         bool zip,
364         std::function<void (string)> out
365         )
366 {
367         dcp::NameFormat::Map values;
368
369         try {
370                 list<KDMWithMetadataPtr> kdms;
371                 for (auto i: screens) {
372                         if (!i->recipient) {
373                                 continue;
374                         }
375
376                         int const offset_hour = i->cinema ? i->cinema->utc_offset_hour() : 0;
377                         int const offset_minute = i->cinema ? i->cinema->utc_offset_minute() : 0;
378
379                         dcp::LocalTime begin(valid_from, dcp::UTCOffset(offset_hour, offset_minute));
380                         dcp::LocalTime end(valid_to, dcp::UTCOffset(offset_hour, offset_minute));
381
382                         auto const kdm = kdm_from_dkdm(
383                                                         dkdm,
384                                                         i->recipient.get(),
385                                                         i->trusted_device_thumbprints(),
386                                                         begin,
387                                                         end,
388                                                         formulation,
389                                                         disable_forensic_marking_picture,
390                                                         disable_forensic_marking_audio
391                                                         );
392
393                         dcp::NameFormat::Map name_values;
394                         name_values['c'] = i->cinema ? i->cinema->name : "";
395                         name_values['s'] = i->name;
396                         name_values['f'] = kdm.content_title_text();
397                         name_values['b'] = begin.date() + " " + begin.time_of_day(true, false);
398                         name_values['e'] = end.date() + " " + end.time_of_day(true, false);
399                         name_values['i'] = kdm.cpl_id();
400
401                         kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm));
402                 }
403                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
404                 if (email) {
405                         send_emails ({kdms}, container_name_format, filename_format, dkdm.annotation_text().get_value_or(""), {});
406                 }
407         } catch (FileError& e) {
408                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
409         }
410 }
411
412
413 static
414 void
415 dump_dkdm_group (shared_ptr<DKDMGroup> group, int indent, std::function<void (string)> out)
416 {
417         auto const indent_string = string(indent, ' ');
418
419         if (indent > 0) {
420                 out (indent_string + group->name());
421         }
422         for (auto i: group->children()) {
423                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
424                 if (g) {
425                         dump_dkdm_group (g, indent + 2, out);
426                 } else {
427                         auto d = dynamic_pointer_cast<DKDM>(i);
428                         assert(d);
429                         out (indent_string + d->dkdm().cpl_id());
430                 }
431         }
432 }
433
434
435 optional<string>
436 kdm_cli (int argc, char* argv[], std::function<void (string)> out)
437 try
438 {
439         boost::filesystem::path output = dcp::filesystem::current_path();
440         auto container_name_format = Config::instance()->kdm_container_name_format();
441         auto filename_format = Config::instance()->kdm_filename_format();
442         optional<string> cinema_name;
443         shared_ptr<Cinema> cinema;
444         optional<boost::filesystem::path> certificate;
445         optional<string> screen;
446         vector<shared_ptr<Screen>> screens;
447         optional<dcp::EncryptedKDM> dkdm;
448         optional<boost::posix_time::ptime> valid_from;
449         optional<boost::posix_time::ptime> valid_to;
450         bool zip = false;
451         bool list_cinemas = false;
452         bool list_dkdm_cpls = false;
453         optional<string> duration_string;
454         bool verbose = false;
455         dcp::Formulation formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
456         bool disable_forensic_marking_picture = false;
457         optional<int> disable_forensic_marking_audio;
458         bool email = false;
459         optional<boost::filesystem::path> cinemas_file;
460
461         program_name = argv[0];
462
463         /* Reset getopt() so we can call this method several times in one test process */
464         optind = 1;
465
466         int option_index = 0;
467         while (true) {
468                 static struct option long_options[] = {
469                         { "help", no_argument, 0, 'h'},
470                         { "output", required_argument, 0, 'o'},
471                         { "filename-format", required_argument, 0, 'K'},
472                         { "container-name-format", required_argument, 0, 'Z'},
473                         { "valid-from", required_argument, 0, 'f'},
474                         { "valid-to", required_argument, 0, 't'},
475                         { "valid-duration", required_argument, 0, 'd'},
476                         { "formulation", required_argument, 0, 'F' },
477                         { "disable-forensic-marking-picture", no_argument, 0, 'p' },
478                         { "disable-forensic-marking-audio", optional_argument, 0, 'a' },
479                         { "email", no_argument, 0, 'e' },
480                         { "zip", no_argument, 0, 'z' },
481                         { "verbose", no_argument, 0, 'v' },
482                         { "cinema", required_argument, 0, 'c' },
483                         { "screen", required_argument, 0, 'S' },
484                         { "certificate", required_argument, 0, 'C' },
485                         { "trusted-device", required_argument, 0, 'T' },
486                         { "list-cinemas", no_argument, 0, 'B' },
487                         { "list-dkdm-cpls", no_argument, 0, 'D' },
488                         { "cinemas-file", required_argument, 0, 'E' },
489                         { 0, 0, 0, 0 }
490                 };
491
492                 int c = getopt_long (argc, argv, "ho:K:Z:f:t:d:F:pae::zvc:S:C:T:BDE:", long_options, &option_index);
493
494                 if (c == -1) {
495                         break;
496                 }
497
498                 switch (c) {
499                 case 'h':
500                         help (out);
501                         return {};
502                 case 'o':
503                         output = optarg;
504                         break;
505                 case 'K':
506                         filename_format = dcp::NameFormat (optarg);
507                         break;
508                 case 'Z':
509                         container_name_format = dcp::NameFormat (optarg);
510                         break;
511                 case 'f':
512                         valid_from = time_from_string (optarg);
513                         break;
514                 case 't':
515                         valid_to = time_from_string (optarg);
516                         break;
517                 case 'd':
518                         duration_string = optarg;
519                         break;
520                 case 'F':
521                         if (string(optarg) == "modified-transitional-1") {
522                                 formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
523                         } else if (string(optarg) == "multiple-modified-transitional-1") {
524                                 formulation = dcp::Formulation::MULTIPLE_MODIFIED_TRANSITIONAL_1;
525                         } else if (string(optarg) == "dci-any") {
526                                 formulation = dcp::Formulation::DCI_ANY;
527                         } else if (string(optarg) == "dci-specific") {
528                                 formulation = dcp::Formulation::DCI_SPECIFIC;
529                         } else {
530                                 throw KDMCLIError ("unrecognised KDM formulation " + string (optarg));
531                         }
532                         break;
533                 case 'p':
534                         disable_forensic_marking_picture = true;
535                         break;
536                 case 'a':
537                         disable_forensic_marking_audio = 0;
538                         if (optarg == 0 && argv[optind] != 0 && argv[optind][0] != '-') {
539                                 disable_forensic_marking_audio = atoi (argv[optind++]);
540                         } else if (optarg) {
541                                 disable_forensic_marking_audio = atoi (optarg);
542                         }
543                         break;
544                 case 'e':
545                         email = true;
546                         break;
547                 case 'z':
548                         zip = true;
549                         break;
550                 case 'v':
551                         verbose = true;
552                         break;
553                 case 'c':
554                         /* This could be a cinema to search for in the configured list or the name of a cinema being
555                            built up on-the-fly in the option.  Cater for both possilibities here by storing the name
556                            (for lookup) and by creating a Cinema which the next Screen will be added to.
557                         */
558                         cinema_name = optarg;
559                         cinema = make_shared<Cinema>(optarg, vector<string>(), "", 0, 0);
560                         break;
561                 case 'S':
562                         /* Similarly, this could be the name of a new (temporary) screen or the name of a screen
563                          * to search for.
564                          */
565                         screen = optarg;
566                         break;
567                 case 'C':
568                         certificate = optarg;
569                         break;
570                 case 'T':
571                         /* A trusted device ends up in the last screen we made */
572                         if (!screens.empty ()) {
573                                 screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
574                         }
575                         break;
576                 case 'B':
577                         list_cinemas = true;
578                         break;
579                 case 'D':
580                         list_dkdm_cpls = true;
581                         break;
582                 case 'E':
583                         cinemas_file = optarg;
584                         break;
585                 }
586         }
587
588         if (cinemas_file) {
589                 Config::instance()->set_cinemas_file(*cinemas_file);
590         }
591
592         if (certificate) {
593                 /* Make a new screen and add it to the current cinema */
594                 dcp::CertificateChain chain(dcp::file_to_string(*certificate));
595                 auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>());
596                 if (cinema) {
597                         cinema->add_screen(screen_to_add);
598                 }
599                 screens.push_back(screen_to_add);
600         }
601
602         if (list_cinemas) {
603                 auto cinemas = Config::instance()->cinemas ();
604                 for (auto i: cinemas) {
605                         out (String::compose("%1 (%2)", i->name, Emailer::address_list (i->emails)));
606                 }
607                 return {};
608         }
609
610         if (list_dkdm_cpls) {
611                 dump_dkdm_group (Config::instance()->dkdms(), 0, out);
612                 return {};
613         }
614
615         if (!duration_string && !valid_to) {
616                 throw KDMCLIError ("you must specify a --valid-duration or --valid-to");
617         }
618
619         if (!valid_from) {
620                 throw KDMCLIError ("you must specify --valid-from");
621         }
622
623         if (optind >= argc) {
624                 throw KDMCLIError ("no film, CPL ID or DKDM specified");
625         }
626
627         if (screens.empty()) {
628                 if (!cinema_name) {
629                         throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files");
630                 }
631
632                 screens = find_cinema (*cinema_name)->screens ();
633                 if (screen) {
634                         screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end());
635                 }
636         }
637
638         if (duration_string) {
639                 valid_to = valid_from.get() + duration_from_string (*duration_string);
640         }
641
642         if (verbose) {
643                 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())));
644         }
645
646         string const thing = argv[optind];
647         if (dcp::filesystem::is_directory(thing) && dcp::filesystem::is_regular_file(boost::filesystem::path(thing) / "metadata.xml")) {
648                 from_film (
649                         screens,
650                         thing,
651                         verbose,
652                         output,
653                         container_name_format,
654                         filename_format,
655                         *valid_from,
656                         *valid_to,
657                         formulation,
658                         disable_forensic_marking_picture,
659                         disable_forensic_marking_audio,
660                         email,
661                         zip,
662                         out
663                         );
664         } else {
665                 if (dcp::filesystem::is_regular_file(thing)) {
666                         dkdm = dcp::EncryptedKDM (dcp::file_to_string (thing));
667                 } else {
668                         dkdm = find_dkdm (thing);
669                 }
670
671                 if (!dkdm) {
672                         throw KDMCLIError ("could not find film or CPL ID corresponding to " + thing);
673                 }
674
675                 from_dkdm (
676                         screens,
677                         dcp::DecryptedKDM (*dkdm, Config::instance()->decryption_chain()->key().get()),
678                         verbose,
679                         output,
680                         container_name_format,
681                         filename_format,
682                         *valid_from,
683                         *valid_to,
684                         formulation,
685                         disable_forensic_marking_picture,
686                         disable_forensic_marking_audio,
687                         email,
688                         zip,
689                         out
690                         );
691         }
692
693         return {};
694 } catch (std::exception& e) {
695         return string(e.what());
696 }
697