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