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