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