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