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