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