Allow the missing full "valid-duration" argument.
[dcpomatic.git] / src / tools / dcpomatic_kdm_cli.cc
1 /*
2     Copyright (C) 2013-2017 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 /** @file  src/tools/dcpomatic_kdm_cli.cc
22  *  @brief Command-line program to generate KDMs.
23  */
24
25 #include "lib/film.h"
26 #include "lib/cinema.h"
27 #include "lib/screen_kdm.h"
28 #include "lib/cinema_kdms.h"
29 #include "lib/config.h"
30 #include "lib/exceptions.h"
31 #include "lib/emailer.h"
32 #include "lib/dkdm_wrapper.h"
33 #include "lib/screen.h"
34 #include <dcp/certificate.h>
35 #include <dcp/decrypted_kdm.h>
36 #include <dcp/encrypted_kdm.h>
37 #include <getopt.h>
38 #include <iostream>
39
40 using std::string;
41 using std::cout;
42 using std::cerr;
43 using std::list;
44 using std::vector;
45 using boost::shared_ptr;
46 using boost::optional;
47 using boost::bind;
48 using boost::dynamic_pointer_cast;
49
50 static void
51 help ()
52 {
53         cerr << "Syntax: " << program_name << " [OPTION] <FILM|CPL-ID>\n"
54                 "  -h, --help             show this help\n"
55                 "  -o, --output           output file or directory\n"
56                 "  -f, --valid-from       valid from time (in local time zone of the cinema) (e.g. \"2013-09-28 01:41:51\") or \"now\"\n"
57                 "  -t, --valid-to         valid to time (in local time zone of the cinema) (e.g. \"2014-09-28 01:41:51\")\n"
58                 "  -d, --valid-duration   valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")\n"
59                 "      --formulation      modified-transitional-1, multiple-modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]\n"
60                 "  -z, --zip              ZIP each cinema's KDMs into its own file\n"
61                 "  -v, --verbose          be verbose\n"
62                 "  -c, --cinema           specify a cinema, either by name or email address\n"
63                 "      --certificate      file containing projector certificate\n"
64                 "      --cinemas          list known cinemas from the DCP-o-matic settings\n"
65                 "      --dkdm-cpls        list CPLs for which DCP-o-matic has DKDMs\n\n"
66                 "CPL-ID must be the ID of a CPL that is mentioned in DCP-o-matic's DKDM list.\n\n"
67                 "For example:\n\n"
68                 "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.\n"
69                 "(Fred's Cinema must have been set up in DCP-o-matic's KDM window)\n\n"
70                 "\tdcpomatic_kdm -c \"Fred's Cinema\" -f now -d \"2 weeks\" -z my_great_movie\n\n";
71 }
72
73 static void
74 error (string m)
75 {
76         cerr << program_name << ": " << m << "\n";
77         exit (EXIT_FAILURE);
78 }
79
80 static boost::posix_time::ptime
81 time_from_string (string t)
82 {
83         if (t == "now") {
84                 return boost::posix_time::second_clock::local_time ();
85         }
86
87         return boost::posix_time::time_from_string (t);
88 }
89
90 static boost::posix_time::time_duration
91 duration_from_string (string d)
92 {
93         int N;
94         char unit_buf[64] = "\0";
95         sscanf (d.c_str(), "%d %63s", &N, unit_buf);
96         string const unit (unit_buf);
97
98         if (N == 0) {
99                 cerr << "Could not understand duration \"" << d << "\"\n";
100                 exit (EXIT_FAILURE);
101         }
102
103         if (unit == "year" || unit == "years") {
104                 return boost::posix_time::time_duration (N * 24 * 365, 0, 0, 0);
105         } else if (unit == "week" || unit == "weeks") {
106                 return boost::posix_time::time_duration (N * 24 * 7, 0, 0, 0);
107         } else if (unit == "day" || unit == "days") {
108                 return boost::posix_time::time_duration (N * 24, 0, 0, 0);
109         } else if (unit == "hour" || unit == "hours") {
110                 return boost::posix_time::time_duration (N, 0, 0, 0);
111         }
112
113         cerr << "Could not understand duration \"" << d << "\"\n";
114         exit (EXIT_FAILURE);
115 }
116
117 static bool
118 always_overwrite ()
119 {
120         return true;
121 }
122
123 void
124 write_files (list<ScreenKDM> screen_kdms, bool zip, boost::filesystem::path output, dcp::NameFormat::Map values, bool verbose)
125 {
126         if (zip) {
127                 int const N = CinemaKDMs::write_zip_files (
128                         CinemaKDMs::collect (screen_kdms),
129                         output,
130                         Config::instance()->kdm_container_name_format(),
131                         Config::instance()->kdm_filename_format(),
132                         values,
133                         bind (&always_overwrite)
134                         );
135
136                 if (verbose) {
137                         cout << "Wrote " << N << " ZIP files to " << output << "\n";
138                 }
139         } else {
140                 int const N = ScreenKDM::write_files (
141                         screen_kdms, output, Config::instance()->kdm_filename_format(), values,
142                         bind (&always_overwrite)
143                         );
144
145                 if (verbose) {
146                         cout << "Wrote " << N << " KDM files to " << output << "\n";
147                 }
148         }
149 }
150
151 shared_ptr<Cinema>
152 find_cinema (string cinema_name)
153 {
154         list<shared_ptr<Cinema> > cinemas = Config::instance()->cinemas ();
155         list<shared_ptr<Cinema> >::const_iterator i = cinemas.begin();
156         while (
157                 i != cinemas.end() &&
158                 (*i)->name != cinema_name &&
159                 find ((*i)->emails.begin(), (*i)->emails.end(), cinema_name) == (*i)->emails.end()) {
160
161                 ++i;
162         }
163
164         if (i == cinemas.end ()) {
165                 cerr << program_name << ": could not find cinema \"" << cinema_name << "\"\n";
166                 exit (EXIT_FAILURE);
167         }
168
169         return *i;
170 }
171
172 void
173 from_film (
174         boost::filesystem::path film_dir,
175         bool verbose,
176         optional<string> cinema_name,
177         optional<boost::filesystem::path> output,
178         optional<boost::filesystem::path> certificate_file,
179         boost::posix_time::ptime valid_from,
180         boost::posix_time::ptime valid_to,
181         dcp::Formulation formulation,
182         bool zip
183         )
184 {
185         shared_ptr<Film> film;
186         try {
187                 film.reset (new Film (film_dir));
188                 film->read_metadata ();
189                 if (verbose) {
190                         cout << "Read film " << film->name () << "\n";
191                 }
192         } catch (std::exception& e) {
193                 cerr << program_name << ": error reading film `" << film_dir.string() << "' (" << e.what() << ")\n";
194                 exit (EXIT_FAILURE);
195         }
196
197         /* XXX: allow specification of this */
198         vector<CPLSummary> cpls = film->cpls ();
199         if (cpls.empty ()) {
200                 error ("no CPLs found in film");
201         } else if (cpls.size() > 1) {
202                 error ("more than one CPL found in film");
203         }
204
205         boost::filesystem::path cpl = cpls.front().cpl_file;
206
207         if (!cinema_name) {
208
209                 if (!output) {
210                         error ("you must specify --output");
211                 }
212
213                 dcp::Certificate certificate (dcp::file_to_string (*certificate_file));
214                 dcp::EncryptedKDM kdm = film->make_kdm (
215                         certificate, vector<dcp::Certificate>(), cpl, dcp::LocalTime (valid_from), dcp::LocalTime (valid_to), formulation
216                         );
217                 kdm.as_xml (*output);
218                 if (verbose) {
219                         cout << "Generated KDM " << *output << " for certificate.\n";
220                 }
221         } else {
222
223                 if (!output) {
224                         output = ".";
225                 }
226
227                 dcp::NameFormat::Map values;
228                 values['f'] = film->name();
229                 values['b'] = dcp::LocalTime(valid_from).date() + " " + dcp::LocalTime(valid_from).time_of_day(true, false);
230                 values['e'] = dcp::LocalTime(valid_to).date() + " " + dcp::LocalTime(valid_to).time_of_day(true, false);
231
232                 try {
233                         list<ScreenKDM> screen_kdms = film->make_kdms (
234                                 find_cinema(*cinema_name)->screens(), cpl, valid_from, valid_to, formulation
235                                 );
236
237                         write_files (screen_kdms, zip, *output, values, verbose);
238                 } catch (FileError& e) {
239                         cerr << program_name << ": " << e.what() << " (" << e.file().string() << ")\n";
240                         exit (EXIT_FAILURE);
241                 } catch (KDMError& e) {
242                         cerr << program_name << ": " << e.what() << "\n";
243                         exit (EXIT_FAILURE);
244                 }
245         }
246 }
247
248 optional<dcp::EncryptedKDM>
249 sub_find_dkdm (shared_ptr<DKDMGroup> group, string cpl_id)
250 {
251         BOOST_FOREACH (shared_ptr<DKDMBase> i, group->children()) {
252                 shared_ptr<DKDMGroup> g = dynamic_pointer_cast<DKDMGroup>(i);
253                 if (g) {
254                         optional<dcp::EncryptedKDM> dkdm = sub_find_dkdm (g, cpl_id);
255                         if (dkdm) {
256                                 return dkdm;
257                         }
258                 } else {
259                         shared_ptr<DKDM> d = dynamic_pointer_cast<DKDM>(i);
260                         assert (d);
261                         if (d->dkdm().cpl_id() == cpl_id) {
262                                 return d->dkdm();
263                         }
264                 }
265         }
266
267         return optional<dcp::EncryptedKDM>();
268 }
269
270 optional<dcp::EncryptedKDM>
271 find_dkdm (string cpl_id)
272 {
273         return sub_find_dkdm (Config::instance()->dkdms(), cpl_id);
274 }
275
276 dcp::EncryptedKDM
277 kdm_from_dkdm (
278         dcp::EncryptedKDM dkdm,
279         dcp::Certificate target,
280         vector<dcp::Certificate> trusted_devices,
281         dcp::LocalTime valid_from,
282         dcp::LocalTime valid_to,
283         dcp::Formulation formulation
284         )
285 {
286         /* Decrypted DKDM */
287         dcp::DecryptedKDM decrypted_dkdm (dkdm, Config::instance()->decryption_chain()->key().get());
288
289         /* Signer for new KDM */
290         shared_ptr<const dcp::CertificateChain> signer = Config::instance()->signer_chain ();
291         if (!signer->valid ()) {
292                 error ("signing certificate chain is invalid.");
293         }
294
295         /* Make a new empty KDM and add the keys from the DKDM to it */
296         dcp::DecryptedKDM kdm (
297                 valid_from,
298                 valid_to,
299                 dkdm.annotation_text().get_value_or(""),
300                 dkdm.content_title_text(),
301                 dcp::LocalTime().as_string()
302                 );
303
304         BOOST_FOREACH (dcp::DecryptedKDMKey const & j, decrypted_dkdm.keys()) {
305                 kdm.add_key(j);
306         }
307
308         return kdm.encrypt (signer, target, trusted_devices, formulation);
309 }
310
311 void
312 from_dkdm (
313         dcp::EncryptedKDM dkdm,
314         bool verbose,
315         optional<string> cinema_name,
316         optional<boost::filesystem::path> output,
317         optional<boost::filesystem::path> certificate_file,
318         boost::posix_time::ptime valid_from,
319         boost::posix_time::ptime valid_to,
320         dcp::Formulation formulation,
321         bool zip
322         )
323 {
324         if (!cinema_name) {
325                 if (!output) {
326                         error ("you must specify --output");
327                 }
328
329                 dcp::EncryptedKDM kdm = kdm_from_dkdm (
330                         dkdm,
331                         dcp::Certificate (dcp::file_to_string (*certificate_file)),
332                         vector<dcp::Certificate>(),
333                         dcp::LocalTime(valid_from), dcp::LocalTime(valid_to),
334                         formulation
335                         );
336
337                 kdm.as_xml (*output);
338                 if (verbose) {
339                         cout << "Generated KDM " << *output << " for certificate.\n";
340                 }
341         } else {
342
343                 if (!output) {
344                         output = ".";
345                 }
346
347                 dcp::NameFormat::Map values;
348                 values['f'] = dkdm.annotation_text().get_value_or("");
349                 values['b'] = dcp::LocalTime(valid_from).date() + " " + dcp::LocalTime(valid_from).time_of_day(true, false);
350                 values['e'] = dcp::LocalTime(valid_to).date() + " " + dcp::LocalTime(valid_to).time_of_day(true, false);
351
352                 try {
353                         list<ScreenKDM> screen_kdms;
354                         BOOST_FOREACH (shared_ptr<Screen> i, find_cinema(*cinema_name)->screens()) {
355                                 if (!i->recipient) {
356                                         continue;
357                                 }
358                                 screen_kdms.push_back (
359                                         ScreenKDM (
360                                                 i,
361                                                 kdm_from_dkdm (
362                                                         dkdm,
363                                                         i->recipient.get(),
364                                                         i->trusted_devices,
365                                                         dcp::LocalTime(valid_from, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
366                                                         dcp::LocalTime(valid_to, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
367                                                         formulation
368                                                         )
369                                                 )
370                                         );
371                         }
372                         write_files (screen_kdms, zip, *output, values, verbose);
373                 } catch (FileError& e) {
374                         cerr << program_name << ": " << e.what() << " (" << e.file().string() << ")\n";
375                         exit (EXIT_FAILURE);
376                 } catch (KDMError& e) {
377                         cerr << program_name << ": " << e.what() << "\n";
378                         exit (EXIT_FAILURE);
379                 }
380         }
381 }
382
383 void
384 dump_dkdm_group (shared_ptr<DKDMGroup> group, int indent)
385 {
386         if (indent > 0) {
387                 for (int i = 0; i < indent; ++i) {
388                         cout << " ";
389                 }
390                 cout << group->name() << "\n";
391         }
392         BOOST_FOREACH (shared_ptr<DKDMBase> i, group->children()) {
393                 shared_ptr<DKDMGroup> g = dynamic_pointer_cast<DKDMGroup>(i);
394                 if (g) {
395                         dump_dkdm_group (g, indent + 2);
396                 } else {
397                         for (int j = 0; j < indent; ++j) {
398                                 cout << " ";
399                         }
400                         shared_ptr<DKDM> d = dynamic_pointer_cast<DKDM>(i);
401                         assert(d);
402                         cout << d->dkdm().cpl_id() << "\n";
403                 }
404         }
405 }
406
407 int main (int argc, char* argv[])
408 {
409         optional<boost::filesystem::path> output;
410         optional<boost::posix_time::ptime> valid_from;
411         optional<boost::posix_time::ptime> valid_to;
412         optional<boost::filesystem::path> certificate_file;
413         bool zip = false;
414         optional<string> cinema_name;
415         bool cinemas = false;
416         bool dkdm_cpls = false;
417         optional<string> duration_string;
418         bool verbose = false;
419         dcp::Formulation formulation = dcp::MODIFIED_TRANSITIONAL_1;
420
421         program_name = argv[0];
422
423         int option_index = 0;
424         while (true) {
425                 static struct option long_options[] = {
426                         { "help", no_argument, 0, 'h'},
427                         { "output", required_argument, 0, 'o'},
428                         { "valid-from", required_argument, 0, 'f'},
429                         { "valid-to", required_argument, 0, 't'},
430                         { "valid-duration", required_argument, 0, 'd'},
431                         { "certificate", required_argument, 0, 'A' },
432                         { "cinema", required_argument, 0, 'c' },
433                         { "cinemas", no_argument, 0, 'B' },
434                         { "dkdm-cpls", no_argument, 0, 'D' },
435                         { "zip", no_argument, 0, 'z' },
436                         { "duration", required_argument, 0, 'd' },
437                         { "verbose", no_argument, 0, 'v' },
438                         { "formulation", required_argument, 0, 'C' },
439                         { 0, 0, 0, 0 }
440                 };
441
442                 int c = getopt_long (argc, argv, "ho:f:t:c:A:Bzd:vC:D", long_options, &option_index);
443
444                 if (c == -1) {
445                         break;
446                 }
447
448                 switch (c) {
449                 case 'h':
450                         help ();
451                         exit (EXIT_SUCCESS);
452                 case 'o':
453                         output = optarg;
454                         break;
455                 case 'f':
456                         valid_from = time_from_string (optarg);
457                         break;
458                 case 't':
459                         valid_to = time_from_string (optarg);
460                         break;
461                 case 'A':
462                         certificate_file = optarg;
463                         break;
464                 case 'c':
465                         cinema_name = optarg;
466                         break;
467                 case 'B':
468                         cinemas = true;
469                         break;
470                 case 'D':
471                         dkdm_cpls = true;
472                         break;
473                 case 'z':
474                         zip = true;
475                         break;
476                 case 'd':
477                         duration_string = optarg;
478                         break;
479                 case 'v':
480                         verbose = true;
481                         break;
482                 case 'C':
483                         if (string (optarg) == "modified-transitional-1") {
484                                 formulation = dcp::MODIFIED_TRANSITIONAL_1;
485                         } else if (string (optarg) == "multiple-modified-transitional-1") {
486                                 formulation = dcp::MULTIPLE_MODIFIED_TRANSITIONAL_1;
487                         } else if (string (optarg) == "dci-any") {
488                                 formulation = dcp::DCI_ANY;
489                         } else if (string (optarg) == "dci-specific") {
490                                 formulation = dcp::DCI_SPECIFIC;
491                         } else {
492                                 error ("unrecognised KDM formulation " + string (optarg));
493                         }
494                 }
495         }
496
497         if (cinemas) {
498                 list<boost::shared_ptr<Cinema> > cinemas = Config::instance()->cinemas ();
499                 for (list<boost::shared_ptr<Cinema> >::const_iterator i = cinemas.begin(); i != cinemas.end(); ++i) {
500                         cout << (*i)->name << " (" << Emailer::address_list ((*i)->emails) << ")\n";
501                 }
502                 exit (EXIT_SUCCESS);
503         }
504
505         if (dkdm_cpls) {
506                 dump_dkdm_group (Config::instance()->dkdms(), 0);
507                 exit (EXIT_SUCCESS);
508         }
509
510         if (!duration_string && !valid_to) {
511                 error ("you must specify a --valid-duration or --valid-to");
512         }
513
514         if (!valid_from) {
515                 error ("you must specify --valid-from");
516                 exit (EXIT_FAILURE);
517         }
518
519         if (optind >= argc) {
520                 help ();
521                 exit (EXIT_FAILURE);
522         }
523
524         if (!cinema_name && !certificate_file) {
525                 error ("you must specify either a cinema, a screen or a certificate file");
526         }
527
528         if (duration_string) {
529                 valid_to = valid_from.get() + duration_from_string (*duration_string);
530         }
531
532         dcpomatic_setup_path_encoding ();
533         dcpomatic_setup ();
534
535         if (verbose) {
536                 cout << "Making KDMs valid from " << valid_from.get() << " to " << valid_to.get() << "\n";
537         }
538
539         string const thing = argv[optind];
540         if (boost::filesystem::is_directory(thing) && boost::filesystem::is_regular_file(boost::filesystem::path(thing) / "metadata.xml")) {
541                 from_film (thing, verbose, cinema_name, output, certificate_file, *valid_from, *valid_to, formulation, zip);
542         } else {
543                 optional<dcp::EncryptedKDM> dkdm = find_dkdm (thing);
544                 if (!dkdm) {
545                         error ("could not find film or CPL ID corresponding to " + thing);
546                 }
547                 from_dkdm (*dkdm, verbose, cinema_name, output, certificate_file, *valid_from, *valid_to, formulation, zip);
548         }
549
550         return 0;
551 }