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