Supporters update.
[dcpomatic.git] / src / lib / map_cli.cc
1 /*
2     Copyright (C) 2023 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 #include "compose.hpp"
23 #include "config.h"
24 #include "util.h"
25 #include <dcp/cpl.h>
26 #include <dcp/dcp.h>
27 #include <dcp/interop_subtitle_asset.h>
28 #include <dcp/filesystem.h>
29 #include <dcp/font_asset.h>
30 #include <dcp/mono_picture_asset.h>
31 #include <dcp/reel.h>
32 #include <dcp/reel_atmos_asset.h>
33 #include <dcp/reel_closed_caption_asset.h>
34 #include <dcp/reel_file_asset.h>
35 #include <dcp/reel_picture_asset.h>
36 #include <dcp/reel_sound_asset.h>
37 #include <dcp/reel_subtitle_asset.h>
38 #include <dcp/smpte_subtitle_asset.h>
39 #include <dcp/sound_asset.h>
40 #include <dcp/stereo_picture_asset.h>
41 #include <boost/optional.hpp>
42 #include <getopt.h>
43 #include <algorithm>
44 #include <memory>
45 #include <string>
46
47
48 using std::dynamic_pointer_cast;
49 using std::shared_ptr;
50 using std::string;
51 using std::make_shared;
52 using std::vector;
53 using boost::optional;
54
55
56 static void
57 help(std::function<void (string)> out)
58 {
59         out(String::compose("Syntax: %1 [OPTION} <cpl-file|ID> [<cpl-file|ID> ... ]", program_name));
60         out("  -V, --version    show libdcp version");
61         out("  -h, --help       show this help");
62         out("  -o, --output     output directory");
63         out("  -l, --hard-link  using hard links instead of copying");
64         out("  -s, --soft-link  using soft links instead of copying");
65         out("  -d, --assets-dir look in this directory for assets (can be given more than once)");
66         out("  -r, --rename     rename all files to <uuid>.<mxf|xml>");
67         out("  --config <dir>   directory containing config.xml and cinemas.xml");
68 }
69
70
71 optional<string>
72 map_cli(int argc, char* argv[], std::function<void (string)> out)
73 {
74         optional<boost::filesystem::path> output_dir;
75         bool hard_link = false;
76         bool soft_link = false;
77         bool rename = false;
78         vector<boost::filesystem::path> assets_dir;
79         optional<boost::filesystem::path> config_dir;
80
81         /* This makes it possible to call getopt several times in the same executable, for tests */
82         optind = 0;
83
84         int option_index = 0;
85         while (true) {
86                 static struct option long_options[] = {
87                         { "help", no_argument, 0, 'h' },
88                         { "output", required_argument, 0, 'o' },
89                         { "hard-link", no_argument, 0, 'l' },
90                         { "soft-link", no_argument, 0, 's' },
91                         { "assets-dir", required_argument, 0, 'd' },
92                         { "rename", no_argument, 0, 'r' },
93                         { "config", required_argument, 0, 'c' },
94                         { 0, 0, 0, 0 }
95                 };
96
97                 int c = getopt_long(argc, argv, "ho:lsd:rc:", long_options, &option_index);
98
99                 if (c == -1) {
100                         break;
101                 } else if (c == '?' || c == ':') {
102                         exit(EXIT_FAILURE);
103                 }
104
105                 switch (c) {
106                 case 'h':
107                         help(out);
108                         exit(EXIT_SUCCESS);
109                 case 'o':
110                         output_dir = optarg;
111                         break;
112                 case 'l':
113                         hard_link = true;
114                         break;
115                 case 's':
116                         soft_link = true;
117                         break;
118                 case 'd':
119                         assets_dir.push_back(optarg);
120                         break;
121                 case 'r':
122                         rename = true;
123                         break;
124                 case 'c':
125                         config_dir = optarg;
126                         break;
127                 }
128         }
129
130         program_name = argv[0];
131
132         if (argc <= optind) {
133                 help(out);
134                 exit(EXIT_FAILURE);
135         }
136
137         if (config_dir) {
138                 State::override_path = *config_dir;
139         }
140
141         vector<string> cpl_filenames_or_ids;
142         for (int i = optind; i < argc; ++i) {
143                 cpl_filenames_or_ids.push_back(argv[i]);
144         }
145
146         if (cpl_filenames_or_ids.empty()) {
147                 return string{"No CPL specified."};
148         }
149
150         if (!output_dir) {
151                 return string{"Missing -o or --output"};
152         }
153
154         if (dcp::filesystem::exists(*output_dir)) {
155                 return String::compose("Output directory %1 already exists.", *output_dir);
156         }
157
158         if (hard_link && soft_link) {
159                 return string{"Specify either -s,--soft-link or -l,--hard-link, not both."};
160         }
161
162         boost::system::error_code ec;
163         dcp::filesystem::create_directory(*output_dir, ec);
164         if (ec) {
165                 return String::compose("Could not create output directory %1: %2", *output_dir, ec.message());
166         }
167
168         /* Find all the assets in the asset directories.  This assumes that the asset directories are in fact
169          * DCPs (with AssetMaps and so on).  We could search for assets ourselves here but interop fonts are
170          * a little tricky because they don't contain their own UUID within the DCP.
171          */
172         vector<shared_ptr<dcp::Asset>> assets;
173         for (auto dir: assets_dir) {
174                 dcp::DCP dcp(dir);
175                 dcp.read();
176                 auto dcp_assets = dcp.assets(true);
177                 std::copy(dcp_assets.begin(), dcp_assets.end(), back_inserter(assets));
178         }
179
180         dcp::DCP dcp(*output_dir);
181
182         /* Find all the CPLs */
183         vector<shared_ptr<dcp::CPL>> cpls;
184         for (auto filename_or_id: cpl_filenames_or_ids) {
185                 if (boost::filesystem::exists(filename_or_id)) {
186                         try {
187                                 auto cpl = make_shared<dcp::CPL>(filename_or_id);
188                                 cpl->resolve_refs(assets);
189                                 cpls.push_back(cpl);
190                         } catch (std::exception& e) {
191                                 return String::compose("Could not read CPL %1: %2", filename_or_id, e.what());
192                         }
193                 } else {
194                         auto cpl_iter = std::find_if(assets.begin(), assets.end(), [filename_or_id](shared_ptr<dcp::Asset> asset) {
195                                 return asset->id() == filename_or_id;
196                         });
197                         if (cpl_iter == assets.end()) {
198                                 return String::compose("Could not find CPL with ID %1", filename_or_id);
199                         }
200                         if (auto cpl = dynamic_pointer_cast<dcp::CPL>(*cpl_iter)) {
201                                 cpl->resolve_refs(assets);
202                                 cpls.push_back(cpl);
203                         } else {
204                                 return String::compose("Could not find CPL with ID %1", filename_or_id);
205                         }
206                 }
207         }
208
209         class CopyError : public std::runtime_error
210         {
211         public:
212                 CopyError(std::string message) : std::runtime_error(message) {}
213         };
214
215         vector<string> already_copied;
216
217         auto copy = [](
218                 boost::filesystem::path input_path,
219                 boost::filesystem::path output_path,
220                 bool hard_link,
221                 bool soft_link
222                 ) {
223                 dcp::filesystem::create_directories(output_path.parent_path());
224
225                 boost::system::error_code ec;
226                 if (hard_link) {
227                         dcp::filesystem::create_hard_link(input_path, output_path, ec);
228                         if (ec) {
229                                 throw CopyError(String::compose("Could not hard-link asset %1: %2", input_path.string(), ec.message()));
230                         }
231                 } else if (soft_link) {
232                         dcp::filesystem::create_symlink(input_path, output_path, ec);
233                         if (ec) {
234                                 throw CopyError(String::compose("Could not soft-link asset %1: %2", input_path.string(), ec.message()));
235                         }
236                 } else {
237                         dcp::filesystem::copy_file(input_path, output_path, ec);
238                         if (ec) {
239                                 throw CopyError(String::compose("Could not copy asset %1: %2", input_path.string(), ec.message()));
240                         }
241                 }
242         };
243
244         auto maybe_copy = [&assets, &already_copied, output_dir, copy](
245                 string asset_id,
246                 bool rename,
247                 bool hard_link,
248                 bool soft_link,
249                 boost::optional<boost::filesystem::path> extra = boost::none
250                 ) {
251
252                 if (std::find(already_copied.begin(), already_copied.end(), asset_id) != already_copied.end()) {
253                         return;
254                 }
255
256                 auto iter = std::find_if(assets.begin(), assets.end(), [asset_id](shared_ptr<const dcp::Asset> a) { return a->id() == asset_id; });
257                 if (iter != assets.end()) {
258                         DCP_ASSERT((*iter)->file());
259
260                         auto const input_path = (*iter)->file().get();
261                         boost::filesystem::path output_path = *output_dir;
262                         if (extra) {
263                                 output_path /= *extra;
264                         }
265
266                         if (rename) {
267                                 output_path /= String::compose("%1%2", (*iter)->id(), dcp::filesystem::extension((*iter)->file().get()));
268                                 (*iter)->rename_file(output_path);
269                         } else {
270                                 output_path /= (*iter)->file()->filename();
271                         }
272
273                         copy(input_path, output_path, hard_link, soft_link);
274                         (*iter)->set_file_preserving_hash(output_path);
275                         already_copied.push_back(asset_id);
276                 } else {
277                         boost::system::error_code ec;
278                         dcp::filesystem::remove_all(*output_dir, ec);
279                         throw CopyError(String::compose("Could not find required asset %1", asset_id));
280                 }
281         };
282
283         auto maybe_copy_from_reel = [output_dir, &maybe_copy](
284                 shared_ptr<dcp::ReelFileAsset> asset,
285                 bool rename,
286                 bool hard_link,
287                 bool soft_link,
288                 boost::optional<boost::filesystem::path> extra = boost::none
289                 ) {
290                 if (asset && asset->asset_ref().resolved()) {
291                         maybe_copy(asset->asset_ref().id(), rename, hard_link, soft_link, extra);
292                 }
293         };
294
295         auto maybe_copy_font_and_images = [&maybe_copy, output_dir, copy](shared_ptr<const dcp::SubtitleAsset> asset, bool rename, bool hard_link, bool soft_link) {
296                 auto interop = dynamic_pointer_cast<const dcp::InteropSubtitleAsset>(asset);
297                 boost::optional<boost::filesystem::path> extra;
298                 if (interop) {
299                         extra = interop->id();
300                         for (auto font_asset: interop->font_assets()) {
301                                 maybe_copy(font_asset->id(), rename, hard_link, soft_link, extra);
302                         }
303                         for (auto subtitle: interop->subtitles()) {
304                                 if (auto image = dynamic_pointer_cast<const dcp::SubtitleImage>(subtitle)) {
305                                         auto const output_path = *output_dir / asset->id() / image->file()->filename();
306                                         copy(*image->file(), output_path, hard_link, soft_link);
307                                 }
308                         }
309                 }
310                 return extra;
311         };
312
313         /* Copy assets that the CPLs need */
314         try {
315                 for (auto cpl: cpls) {
316                         for (auto reel: cpl->reels()) {
317                                 maybe_copy_from_reel(reel->main_picture(), rename, hard_link, soft_link);
318                                 maybe_copy_from_reel(reel->main_sound(), rename, hard_link, soft_link);
319                                 if (reel->main_subtitle()) {
320                                         auto extra = maybe_copy_font_and_images(reel->main_subtitle()->asset(), rename, hard_link, soft_link);
321                                         maybe_copy_from_reel(reel->main_subtitle(), rename, hard_link, soft_link, extra);
322                                 }
323                                 for (auto ccap: reel->closed_captions()) {
324                                         auto extra = maybe_copy_font_and_images(ccap->asset(), rename, hard_link, soft_link);
325                                         maybe_copy_from_reel(ccap, rename, hard_link, soft_link, extra);
326                                 }
327                                 maybe_copy_from_reel(reel->atmos(), rename, hard_link, soft_link);
328                         }
329
330                         dcp.add(cpl);
331                 }
332         } catch (CopyError& e) {
333                 return string{e.what()};
334         }
335
336         dcp.resolve_refs(assets);
337         dcp.set_annotation_text(cpls[0]->annotation_text().get_value_or(""));
338         try {
339                 dcp.set_creator(Config::instance()->dcp_creator());
340                 dcp.set_issuer(Config::instance()->dcp_issuer());
341                 dcp.write_xml(Config::instance()->signer_chain());
342         } catch (dcp::UnresolvedRefError& e) {
343                 return String::compose("%1\nPerhaps you need to give a -d parameter to say where this asset is located.", e.what());
344         }
345
346         return {};
347 }
348