Try to be recursive when examining DCP directories.
[libdcp.git] / src / dcp.cc
1 /*
2     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 /** @file  src/dcp.cc
21  *  @brief A class to create a DCP.
22  */
23
24 #include <sstream>
25 #include <fstream>
26 #include <iomanip>
27 #include <cassert>
28 #include <iostream>
29 #include <boost/filesystem.hpp>
30 #include <libxml++/libxml++.h>
31 #include "dcp.h"
32 #include "asset.h"
33 #include "sound_asset.h"
34 #include "picture_asset.h"
35 #include "util.h"
36 #include "metadata.h"
37 #include "exceptions.h"
38 #include "cpl.h"
39 #include "pkl.h"
40 #include "asset_map.h"
41
42 using namespace std;
43 using namespace boost;
44 using namespace libdcp;
45
46 DCP::DCP (string directory, string name, ContentKind content_kind, int fps, int length)
47         : _directory (directory)
48         , _name (name)
49         , _content_kind (content_kind)
50         , _fps (fps)
51         , _length (length)
52 {
53         
54 }
55
56 void
57 DCP::add_sound_asset (vector<string> const & files)
58 {
59         _assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (files, _directory, "audio.mxf", &Progress, _fps, _length)));
60 }
61
62 void
63 DCP::add_sound_asset (sigc::slot<string, Channel> get_path, int channels)
64 {
65         _assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (get_path, _directory, "audio.mxf", &Progress, _fps, _length, channels)));
66 }
67
68 void
69 DCP::add_picture_asset (vector<string> const & files, int width, int height)
70 {
71         _assets.push_back (shared_ptr<PictureAsset> (new PictureAsset (files, _directory, "video.mxf", &Progress, _fps, _length, width, height)));
72 }
73
74 void
75 DCP::add_picture_asset (sigc::slot<string, int> get_path, int width, int height)
76 {
77         _assets.push_back (shared_ptr<PictureAsset> (new PictureAsset (get_path, _directory, "video.mxf", &Progress, _fps, _length, width, height)));
78 }
79
80 void
81 DCP::write_xml () const
82 {
83         string cpl_uuid = make_uuid ();
84         string cpl_path = write_cpl (cpl_uuid);
85         int cpl_length = filesystem::file_size (cpl_path);
86         string cpl_digest = make_digest (cpl_path, 0);
87
88         string pkl_uuid = make_uuid ();
89         string pkl_path = write_pkl (pkl_uuid, cpl_uuid, cpl_digest, cpl_length);
90         
91         write_volindex ();
92         write_assetmap (cpl_uuid, cpl_length, pkl_uuid, filesystem::file_size (pkl_path));
93 }
94
95 string
96 DCP::write_cpl (string cpl_uuid) const
97 {
98         filesystem::path p;
99         p /= _directory;
100         stringstream s;
101         s << cpl_uuid << "_cpl.xml";
102         p /= s.str();
103         ofstream cpl (p.string().c_str());
104         
105         cpl << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
106             << "<CompositionPlaylist xmlns=\"http://www.smpte-ra.org/schemas/429-7/2006/CPL\">\n"
107             << "  <Id>urn:uuid:" << cpl_uuid << "</Id>\n"
108             << "  <AnnotationText>" << _name << "</AnnotationText>\n"
109             << "  <IssueDate>" << Metadata::instance()->issue_date << "</IssueDate>\n"
110             << "  <Creator>" << Metadata::instance()->creator << "</Creator>\n"
111             << "  <ContentTitleText>" << _name << "</ContentTitleText>\n"
112             << "  <ContentKind>" << content_kind_to_string (_content_kind) << "</ContentKind>\n"
113             << "  <ContentVersion>\n"
114             << "    <Id>urn:uri:" << cpl_uuid << "_" << Metadata::instance()->issue_date << "</Id>\n"
115             << "    <LabelText>" << cpl_uuid << "_" << Metadata::instance()->issue_date << "</LabelText>\n"
116             << "  </ContentVersion>\n"
117             << "  <RatingList/>\n"
118             << "  <ReelList>\n";
119
120         cpl << "    <Reel>\n"
121             << "      <Id>urn:uuid:" << make_uuid() << "</Id>\n"
122             << "      <AssetList>\n";
123
124         for (list<shared_ptr<Asset> >::const_iterator i = _assets.begin(); i != _assets.end(); ++i) {
125                 (*i)->write_to_cpl (cpl);
126         }
127
128         cpl << "      </AssetList>\n"
129             << "    </Reel>\n"
130             << "  </ReelList>\n"
131             << "</CompositionPlaylist>\n";
132
133         return p.string ();
134 }
135
136 std::string
137 DCP::write_pkl (string pkl_uuid, string cpl_uuid, string cpl_digest, int cpl_length) const
138 {
139         filesystem::path p;
140         p /= _directory;
141         stringstream s;
142         s << pkl_uuid << "_pkl.xml";
143         p /= s.str();
144         ofstream pkl (p.string().c_str());
145
146         pkl << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
147             << "<PackingList xmlns=\"http://www.smpte-ra.org/schemas/429-8/2007/PKL\">\n"
148             << "  <Id>urn:uuid:" << pkl_uuid << "</Id>\n"
149             << "  <AnnotationText>" << _name << "</AnnotationText>\n"
150             << "  <IssueDate>" << Metadata::instance()->issue_date << "</IssueDate>\n"
151             << "  <Issuer>" << Metadata::instance()->issuer << "</Issuer>\n"
152             << "  <Creator>" << Metadata::instance()->creator << "</Creator>\n"
153             << "  <AssetList>\n";
154
155         for (list<shared_ptr<Asset> >::const_iterator i = _assets.begin(); i != _assets.end(); ++i) {
156                 (*i)->write_to_pkl (pkl);
157         }
158
159         pkl << "    <Asset>\n"
160             << "      <Id>urn:uuid:" << cpl_uuid << "</Id>\n"
161             << "      <Hash>" << cpl_digest << "</Hash>\n"
162             << "      <Size>" << cpl_length << "</Size>\n"
163             << "      <Type>text/xml</Type>\n"
164             << "    </Asset>\n";
165
166         pkl << "  </AssetList>\n"
167             << "</PackingList>\n";
168
169         return p.string ();
170 }
171
172 void
173 DCP::write_volindex () const
174 {
175         filesystem::path p;
176         p /= _directory;
177         p /= "VOLINDEX.xml";
178         ofstream vi (p.string().c_str());
179
180         vi << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
181            << "<VolumeIndex xmlns=\"http://www.smpte-ra.org/schemas/429-9/2007/AM\">\n"
182            << "  <Index>1</Index>\n"
183            << "</VolumeIndex>\n";
184 }
185
186 void
187 DCP::write_assetmap (string cpl_uuid, int cpl_length, string pkl_uuid, int pkl_length) const
188 {
189         filesystem::path p;
190         p /= _directory;
191         p /= "ASSETMAP.xml";
192         ofstream am (p.string().c_str());
193
194         am << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
195            << "<AssetMap xmlns=\"http://www.smpte-ra.org/schemas/429-9/2007/AM\">\n"
196            << "  <Id>urn:uuid:" << make_uuid() << "</Id>\n"
197            << "  <Creator>" << Metadata::instance()->creator << "</Creator>\n"
198            << "  <VolumeCount>1</VolumeCount>\n"
199            << "  <IssueDate>" << Metadata::instance()->issue_date << "</IssueDate>\n"
200            << "  <Issuer>" << Metadata::instance()->issuer << "</Issuer>\n"
201            << "  <AssetList>\n";
202
203         am << "    <Asset>\n"
204            << "      <Id>urn:uuid:" << pkl_uuid << "</Id>\n"
205            << "      <PackingList>true</PackingList>\n"
206            << "      <ChunkList>\n"
207            << "        <Chunk>\n"
208            << "          <Path>" << pkl_uuid << "_pkl.xml</Path>\n"
209            << "          <VolumeIndex>1</VolumeIndex>\n"
210            << "          <Offset>0</Offset>\n"
211            << "          <Length>" << pkl_length << "</Length>\n"
212            << "        </Chunk>\n"
213            << "      </ChunkList>\n"
214            << "    </Asset>\n";
215
216         am << "    <Asset>\n"
217            << "      <Id>urn:uuid:" << cpl_uuid << "</Id>\n"
218            << "      <ChunkList>\n"
219            << "        <Chunk>\n"
220            << "          <Path>" << cpl_uuid << "_cpl.xml</Path>\n"
221            << "          <VolumeIndex>1</VolumeIndex>\n"
222            << "          <Offset>0</Offset>\n"
223            << "          <Length>" << cpl_length << "</Length>\n"
224            << "        </Chunk>\n"
225            << "      </ChunkList>\n"
226            << "    </Asset>\n";
227         
228         for (list<shared_ptr<Asset> >::const_iterator i = _assets.begin(); i != _assets.end(); ++i) {
229                 (*i)->write_to_assetmap (am);
230         }
231
232         am << "  </AssetList>\n"
233            << "</AssetMap>\n";
234 }
235
236
237 DCP::DCP (string directory)
238         : _directory (directory)
239 {
240         Files files;
241         scan (files, directory);
242
243         if (files.cpl.empty ()) {
244                 throw FileError ("no CPL file found", "");
245         }
246
247         if (files.pkl.empty ()) {
248                 throw FileError ("no PKL file found", "");
249         }
250
251         if (files.asset_map.empty ()) {
252                 throw FileError ("no AssetMap file found", "");
253         }
254
255         /* Read the XML */
256         shared_ptr<CPL> cpl;
257         try {
258                 cpl.reset (new CPL (files.cpl));
259         } catch (FileError& e) {
260                 throw FileError ("could not load CPL file", files.cpl);
261         }
262
263         shared_ptr<PKL> pkl;
264         try {
265                 pkl.reset (new PKL (files.pkl));
266         } catch (FileError& e) {
267                 throw FileError ("could not load PKL file", files.pkl);
268         }
269
270         shared_ptr<AssetMap> asset_map;
271         try {
272                 asset_map.reset (new AssetMap (files.asset_map));
273         } catch (FileError& e) {
274                 throw FileError ("could not load AssetMap file", files.asset_map);
275         }
276
277         /* Cross-check */
278         /* XXX */
279
280         /* Now cherry-pick the required bits into our own data structure */
281         
282         _name = cpl->annotation_text;
283         _content_kind = cpl->content_kind;
284
285         shared_ptr<CPLAssetList> cpl_assets = cpl->reels.front()->asset_list;
286
287         /* XXX */
288         _fps = cpl_assets->main_picture->frame_rate.numerator;
289         _length = cpl_assets->main_picture->duration;
290
291         string n = cpl_assets->main_picture->annotation_text;
292         if (n.empty ()) {
293                 n = pkl->asset_from_id(cpl_assets->main_picture->id)->original_file_name;
294         }
295
296         _assets.push_back (shared_ptr<PictureAsset> (
297                                    new PictureAsset (
298                                            _directory,
299                                            n,
300                                            _fps,
301                                            _length
302                                            )
303                                    ));
304
305         if (cpl_assets->main_sound) {
306
307                 n = cpl_assets->main_sound->annotation_text;
308                 if (n.empty ()) {
309                         n = pkl->asset_from_id(cpl_assets->main_sound->id)->original_file_name;
310                 }
311         
312                 _assets.push_back (shared_ptr<SoundAsset> (
313                                            new SoundAsset (
314                                                    _directory,
315                                                    n,
316                                                    _fps,
317                                                    _length
318                                                    )
319                                            ));
320         }
321 }
322
323
324 void
325 DCP::scan (Files& files, string directory) const
326 {
327         for (filesystem::directory_iterator i = filesystem::directory_iterator(directory); i != filesystem::directory_iterator(); ++i) {
328                 
329                 string const t = i->path().string ();
330
331                 if (filesystem::is_directory (*i)) {
332                         scan (files, t);
333                         continue;
334                 }
335
336                 if (ends_with (t, ".mxf")) {
337                         continue;
338                 }
339
340                 xmlpp::DomParser* p = new xmlpp::DomParser;
341
342                 try {
343                         p->parse_file (t);
344                 } catch (std::exception& e) {
345                         delete p;
346                         continue;
347                 }
348                 
349                 if (!p) {
350                         delete p;
351                         continue;
352                 }
353
354                 string const root = p->get_document()->get_root_node()->get_name ();
355                 delete p;
356                 
357                 if (root == "CompositionPlaylist") {
358                         if (files.cpl.empty ()) {
359                                 files.cpl = t;
360                         } else {
361                                 throw DCPReadError ("duplicate CPLs found");
362                         }
363                 } else if (root == "PackingList") {
364                         if (files.pkl.empty ()) {
365                                 files.pkl = t;
366                         } else {
367                                 throw DCPReadError ("duplicate PKLs found");
368                         }
369                 } else if (root == "AssetMap") {
370                         if (files.asset_map.empty ()) {
371                                 files.asset_map = t;
372                         } else {
373                                 throw DCPReadError ("duplicate AssetMaps found");
374                         }
375                         files.asset_map = t;
376                 }
377         }
378 }
379
380
381 list<string>
382 DCP::equals (DCP const & other, EqualityOptions opt) const
383 {
384         list<string> notes;
385         
386         if (opt.flags & LIBDCP_METADATA) {
387                 if (_name != other._name) {
388                         notes.push_back ("names differ");
389                 }
390                 if (_content_kind != other._content_kind) {
391                         notes.push_back ("content kinds differ");
392                 }
393                 if (_fps != other._fps) {
394                         notes.push_back ("frames per second differ");
395                 }
396                 if (_length != other._length) {
397                         notes.push_back ("lengths differ");
398                 }
399         }
400
401         if (_assets.size() != other._assets.size()) {
402                 notes.push_back ("asset counts differ");
403         }
404         
405         list<shared_ptr<Asset> >::const_iterator a = _assets.begin ();
406         list<shared_ptr<Asset> >::const_iterator b = other._assets.begin ();
407         
408         while (a != _assets.end ()) {
409                 list<string> n = (*a)->equals (*b, opt);
410                 notes.merge (n);
411                 ++a;
412                 ++b;
413         }
414
415         return notes;
416 }
417
418 shared_ptr<const PictureAsset>
419 DCP::picture_asset () const
420 {
421         for (list<shared_ptr<Asset> >::const_iterator i = _assets.begin(); i != _assets.end(); ++i) {
422                 shared_ptr<PictureAsset> p = dynamic_pointer_cast<PictureAsset> (*i);
423                 if (p) {
424                         return p;
425                 }
426         }
427
428         return shared_ptr<const PictureAsset> ();
429 }
430
431 shared_ptr<const SoundAsset>
432 DCP::sound_asset () const
433 {
434         for (list<shared_ptr<Asset> >::const_iterator i = _assets.begin(); i != _assets.end(); ++i) {
435                 shared_ptr<SoundAsset> s = dynamic_pointer_cast<SoundAsset> (*i);
436                 if (s) {
437                         return s;
438                 }
439         }
440
441         return shared_ptr<const SoundAsset> ();
442 }