Move UTC offset for KDMs from the cinema to the point of KDM creation (#2300).
[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 optional<string>
429 kdm_cli (int argc, char* argv[], std::function<void (string)> out)
430 try
431 {
432         boost::filesystem::path output = dcp::filesystem::current_path();
433         auto container_name_format = Config::instance()->kdm_container_name_format();
434         auto filename_format = Config::instance()->kdm_filename_format();
435         optional<string> cinema_name;
436         shared_ptr<Cinema> cinema;
437         optional<boost::filesystem::path> certificate;
438         optional<string> screen;
439         vector<shared_ptr<Screen>> screens;
440         optional<dcp::EncryptedKDM> dkdm;
441         optional<dcp::LocalTime> valid_from;
442         optional<dcp::LocalTime> valid_to;
443         bool zip = false;
444         bool list_cinemas = false;
445         bool list_dkdm_cpls = false;
446         optional<string> duration_string;
447         bool verbose = false;
448         dcp::Formulation formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
449         bool disable_forensic_marking_picture = false;
450         optional<int> disable_forensic_marking_audio;
451         bool email = false;
452         optional<boost::filesystem::path> cinemas_file;
453
454         program_name = argv[0];
455
456         /* Reset getopt() so we can call this method several times in one test process */
457         optind = 1;
458
459         int option_index = 0;
460         while (true) {
461                 static struct option long_options[] = {
462                         { "help", no_argument, 0, 'h'},
463                         { "output", required_argument, 0, 'o'},
464                         { "filename-format", required_argument, 0, 'K'},
465                         { "container-name-format", required_argument, 0, 'Z'},
466                         { "valid-from", required_argument, 0, 'f'},
467                         { "valid-to", required_argument, 0, 't'},
468                         { "valid-duration", required_argument, 0, 'd'},
469                         { "formulation", required_argument, 0, 'F' },
470                         { "disable-forensic-marking-picture", no_argument, 0, 'p' },
471                         { "disable-forensic-marking-audio", optional_argument, 0, 'a' },
472                         { "email", no_argument, 0, 'e' },
473                         { "zip", no_argument, 0, 'z' },
474                         { "verbose", no_argument, 0, 'v' },
475                         { "cinema", required_argument, 0, 'c' },
476                         { "screen", required_argument, 0, 'S' },
477                         { "certificate", required_argument, 0, 'C' },
478                         { "trusted-device", required_argument, 0, 'T' },
479                         { "list-cinemas", no_argument, 0, 'B' },
480                         { "list-dkdm-cpls", no_argument, 0, 'D' },
481                         { "cinemas-file", required_argument, 0, 'E' },
482                         { 0, 0, 0, 0 }
483                 };
484
485                 int c = getopt_long (argc, argv, "ho:K:Z:f:t:d:F:pae::zvc:S:C:T:BDE:", long_options, &option_index);
486
487                 if (c == -1) {
488                         break;
489                 }
490
491                 switch (c) {
492                 case 'h':
493                         help (out);
494                         return {};
495                 case 'o':
496                         output = optarg;
497                         break;
498                 case 'K':
499                         filename_format = dcp::NameFormat (optarg);
500                         break;
501                 case 'Z':
502                         container_name_format = dcp::NameFormat (optarg);
503                         break;
504                 case 'f':
505                         if (string(optarg) == "now") {
506                                 valid_from = dcp::LocalTime();
507                         } else {
508                                 valid_from = dcp::LocalTime(optarg);
509                         }
510                         break;
511                 case 't':
512                         valid_to = dcp::LocalTime(optarg);
513                         break;
514                 case 'd':
515                         duration_string = optarg;
516                         break;
517                 case 'F':
518                         if (string(optarg) == "modified-transitional-1") {
519                                 formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
520                         } else if (string(optarg) == "multiple-modified-transitional-1") {
521                                 formulation = dcp::Formulation::MULTIPLE_MODIFIED_TRANSITIONAL_1;
522                         } else if (string(optarg) == "dci-any") {
523                                 formulation = dcp::Formulation::DCI_ANY;
524                         } else if (string(optarg) == "dci-specific") {
525                                 formulation = dcp::Formulation::DCI_SPECIFIC;
526                         } else {
527                                 throw KDMCLIError ("unrecognised KDM formulation " + string (optarg));
528                         }
529                         break;
530                 case 'p':
531                         disable_forensic_marking_picture = true;
532                         break;
533                 case 'a':
534                         disable_forensic_marking_audio = 0;
535                         if (optarg == 0 && argv[optind] != 0 && argv[optind][0] != '-') {
536                                 disable_forensic_marking_audio = atoi (argv[optind++]);
537                         } else if (optarg) {
538                                 disable_forensic_marking_audio = atoi (optarg);
539                         }
540                         break;
541                 case 'e':
542                         email = true;
543                         break;
544                 case 'z':
545                         zip = true;
546                         break;
547                 case 'v':
548                         verbose = true;
549                         break;
550                 case 'c':
551                         /* This could be a cinema to search for in the configured list or the name of a cinema being
552                            built up on-the-fly in the option.  Cater for both possilibities here by storing the name
553                            (for lookup) and by creating a Cinema which the next Screen will be added to.
554                         */
555                         cinema_name = optarg;
556                         cinema = make_shared<Cinema>(optarg, vector<string>(), "");
557                         break;
558                 case 'S':
559                         /* Similarly, this could be the name of a new (temporary) screen or the name of a screen
560                          * to search for.
561                          */
562                         screen = optarg;
563                         break;
564                 case 'C':
565                         certificate = optarg;
566                         break;
567                 case 'T':
568                         /* A trusted device ends up in the last screen we made */
569                         if (!screens.empty ()) {
570                                 screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
571                         }
572                         break;
573                 case 'B':
574                         list_cinemas = true;
575                         break;
576                 case 'D':
577                         list_dkdm_cpls = true;
578                         break;
579                 case 'E':
580                         cinemas_file = optarg;
581                         break;
582                 }
583         }
584
585         if (cinemas_file) {
586                 Config::instance()->set_cinemas_file(*cinemas_file);
587         }
588
589         if (certificate) {
590                 /* Make a new screen and add it to the current cinema */
591                 dcp::CertificateChain chain(dcp::file_to_string(*certificate));
592                 auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>());
593                 if (cinema) {
594                         cinema->add_screen(screen_to_add);
595                 }
596                 screens.push_back(screen_to_add);
597         }
598
599         if (list_cinemas) {
600                 auto cinemas = Config::instance()->cinemas ();
601                 for (auto i: cinemas) {
602                         out (String::compose("%1 (%2)", i->name, Emailer::address_list (i->emails)));
603                 }
604                 return {};
605         }
606
607         if (list_dkdm_cpls) {
608                 dump_dkdm_group (Config::instance()->dkdms(), 0, out);
609                 return {};
610         }
611
612         if (!duration_string && !valid_to) {
613                 throw KDMCLIError ("you must specify a --valid-duration or --valid-to");
614         }
615
616         if (!valid_from) {
617                 throw KDMCLIError ("you must specify --valid-from");
618         }
619
620         if (optind >= argc) {
621                 throw KDMCLIError ("no film, CPL ID or DKDM specified");
622         }
623
624         if (screens.empty()) {
625                 if (!cinema_name) {
626                         throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files");
627                 }
628
629                 screens = find_cinema (*cinema_name)->screens ();
630                 if (screen) {
631                         screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end());
632                 }
633         }
634
635         if (duration_string) {
636                 valid_to = valid_from.get();
637                 valid_to->add(duration_from_string(*duration_string));
638         }
639
640         if (verbose) {
641                 out(String::compose("Making KDMs valid from %1 to %2", valid_from->as_string(), valid_to->as_string()));
642         }
643
644         string const thing = argv[optind];
645         if (dcp::filesystem::is_directory(thing) && dcp::filesystem::is_regular_file(boost::filesystem::path(thing) / "metadata.xml")) {
646                 from_film (
647                         screens,
648                         thing,
649                         verbose,
650                         output,
651                         container_name_format,
652                         filename_format,
653                         *valid_from,
654                         *valid_to,
655                         formulation,
656                         disable_forensic_marking_picture,
657                         disable_forensic_marking_audio,
658                         email,
659                         zip,
660                         out
661                         );
662         } else {
663                 if (dcp::filesystem::is_regular_file(thing)) {
664                         dkdm = dcp::EncryptedKDM (dcp::file_to_string (thing));
665                 } else {
666                         dkdm = find_dkdm (thing);
667                 }
668
669                 if (!dkdm) {
670                         throw KDMCLIError ("could not find film or CPL ID corresponding to " + thing);
671                 }
672
673                 from_dkdm (
674                         screens,
675                         dcp::DecryptedKDM (*dkdm, Config::instance()->decryption_chain()->key().get()),
676                         verbose,
677                         output,
678                         container_name_format,
679                         filename_format,
680                         *valid_from,
681                         *valid_to,
682                         formulation,
683                         disable_forensic_marking_picture,
684                         disable_forensic_marking_audio,
685                         email,
686                         zip,
687                         out
688                         );
689         }
690
691         return {};
692 } catch (std::exception& e) {
693         return string(e.what());
694 }
695