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