Be more defensive when calling boost::filesystem::last_write_time.
[dcpomatic.git] / src / lib / content.cc
1 /*
2     Copyright (C) 2013-2021 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/lib/content.cc
22  *  @brief Content class.
23  */
24
25 #include "content.h"
26 #include "change_signaller.h"
27 #include "util.h"
28 #include "content_factory.h"
29 #include "video_content.h"
30 #include "audio_content.h"
31 #include "text_content.h"
32 #include "exceptions.h"
33 #include "film.h"
34 #include "job.h"
35 #include "compose.hpp"
36 #include <dcp/locale_convert.h>
37 #include <dcp/raw_convert.h>
38 #include <libcxml/cxml.h>
39 #include <libxml++/libxml++.h>
40 #include <boost/thread/mutex.hpp>
41 #include <iostream>
42
43 #include "i18n.h"
44
45 using std::string;
46 using std::list;
47 using std::cout;
48 using std::vector;
49 using std::max;
50 using std::pair;
51 using std::shared_ptr;
52 using boost::optional;
53 using dcp::raw_convert;
54 using dcp::locale_convert;
55 using namespace dcpomatic;
56
57 int const ContentProperty::PATH = 400;
58 int const ContentProperty::POSITION = 401;
59 int const ContentProperty::LENGTH = 402;
60 int const ContentProperty::TRIM_START = 403;
61 int const ContentProperty::TRIM_END = 404;
62 int const ContentProperty::VIDEO_FRAME_RATE = 405;
63
64 Content::Content ()
65         : _position (0)
66         , _trim_start (0)
67         , _trim_end (0)
68         , _change_signals_frequent (false)
69 {
70
71 }
72
73 Content::Content (DCPTime p)
74         : _position (p)
75         , _trim_start (0)
76         , _trim_end (0)
77         , _change_signals_frequent (false)
78 {
79
80 }
81
82 Content::Content (boost::filesystem::path p)
83         : _position (0)
84         , _trim_start (0)
85         , _trim_end (0)
86         , _change_signals_frequent (false)
87 {
88         add_path (p);
89 }
90
91 Content::Content (cxml::ConstNodePtr node)
92         : _change_signals_frequent (false)
93 {
94         for (auto i: node->node_children("Path")) {
95                 _paths.push_back (i->content());
96                 auto const mod = i->optional_number_attribute<time_t>("mtime");
97                 if (mod) {
98                         _last_write_times.push_back (*mod);
99                 } else {
100                         boost::system::error_code ec;
101                         auto last_write = boost::filesystem::last_write_time(i->content(), ec);
102                         _last_write_times.push_back (ec ? 0 : last_write);
103                 }
104         }
105         _digest = node->optional_string_child ("Digest").get_value_or ("X");
106         _position = DCPTime (node->number_child<DCPTime::Type> ("Position"));
107         _trim_start = ContentTime (node->number_child<ContentTime::Type> ("TrimStart"));
108         _trim_end = ContentTime (node->number_child<ContentTime::Type> ("TrimEnd"));
109         _video_frame_rate = node->optional_number_child<double> ("VideoFrameRate");
110 }
111
112 Content::Content (vector<shared_ptr<Content> > c)
113         : _position (c.front()->position ())
114         , _trim_start (c.front()->trim_start ())
115         , _trim_end (c.back()->trim_end ())
116         , _video_frame_rate (c.front()->video_frame_rate())
117         , _change_signals_frequent (false)
118 {
119         for (size_t i = 0; i < c.size(); ++i) {
120                 if (i > 0 && c[i]->trim_start() > ContentTime ()) {
121                         throw JoinError (_("Only the first piece of content to be joined can have a start trim."));
122                 }
123
124                 if (i < (c.size() - 1) && c[i]->trim_end () > ContentTime ()) {
125                         throw JoinError (_("Only the last piece of content to be joined can have an end trim."));
126                 }
127
128                 if (
129                         (_video_frame_rate && !c[i]->_video_frame_rate) ||
130                         (!_video_frame_rate && c[i]->_video_frame_rate)
131                         ) {
132                         throw JoinError (_("Content to be joined must have the same video frame rate"));
133                 }
134
135                 if (_video_frame_rate && fabs (_video_frame_rate.get() - c[i]->_video_frame_rate.get()) > VIDEO_FRAME_RATE_EPSILON) {
136                         throw JoinError (_("Content to be joined must have the same video frame rate"));
137                 }
138
139                 for (size_t j = 0; j < c[i]->number_of_paths(); ++j) {
140                         _paths.push_back (c[i]->path(j));
141                         _last_write_times.push_back (c[i]->_last_write_times[j]);
142                 }
143         }
144 }
145
146 void
147 Content::as_xml (xmlpp::Node* node, bool with_paths) const
148 {
149         boost::mutex::scoped_lock lm (_mutex);
150
151         if (with_paths) {
152                 for (size_t i = 0; i < _paths.size(); ++i) {
153                         xmlpp::Element* p = node->add_child("Path");
154                         p->add_child_text (_paths[i].string());
155                         p->set_attribute ("mtime", raw_convert<string>(_last_write_times[i]));
156                 }
157         }
158         node->add_child("Digest")->add_child_text (_digest);
159         node->add_child("Position")->add_child_text (raw_convert<string> (_position.get ()));
160         node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start.get ()));
161         node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end.get ()));
162         if (_video_frame_rate) {
163                 node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate.get()));
164         }
165 }
166
167 string
168 Content::calculate_digest () const
169 {
170         boost::mutex::scoped_lock lm (_mutex);
171         auto p = _paths;
172         lm.unlock ();
173
174         /* Some content files are very big, so we use a poor man's
175            digest here: a digest of the first and last 1e6 bytes with the
176            size of the first file tacked on the end as a string.
177         */
178         return digest_head_tail(p, 1000000) + raw_convert<string>(boost::filesystem::file_size(p.front()));
179 }
180
181 void
182 Content::examine (shared_ptr<const Film>, shared_ptr<Job> job)
183 {
184         if (job) {
185                 job->sub (_("Computing digest"));
186         }
187
188         auto const d = calculate_digest ();
189
190         boost::mutex::scoped_lock lm (_mutex);
191         _digest = d;
192
193         _last_write_times.clear ();
194         for (auto i: _paths) {
195                 boost::system::error_code ec;
196                 auto last_write = boost::filesystem::last_write_time(i, ec);
197                 _last_write_times.push_back (ec ? 0 : last_write);
198         }
199 }
200
201 void
202 Content::signal_change (ChangeType c, int p)
203 {
204         try {
205                 if (c == ChangeType::PENDING || c == ChangeType::CANCELLED) {
206                         Change (c, shared_from_this(), p, _change_signals_frequent);
207                 } else {
208                         emit (boost::bind (boost::ref(Change), c, shared_from_this(), p, _change_signals_frequent));
209                 }
210         } catch (std::bad_weak_ptr &) {
211                 /* This must be during construction; never mind */
212         }
213 }
214
215 void
216 Content::set_position (shared_ptr<const Film> film, DCPTime p, bool force_emit)
217 {
218         /* video and audio content can modify its position */
219
220         if (video) {
221                 video->modify_position (film, p);
222         }
223
224         /* Only allow the audio to modify if we have no video;
225            sometimes p can't be on an integer video AND audio frame,
226            and in these cases we want the video constraint to be
227            satisfied since (I think) the audio code is better able to
228            cope.
229         */
230         if (!video && audio) {
231                 audio->modify_position (film, p);
232         }
233
234         ContentChangeSignaller cc (this, ContentProperty::POSITION);
235
236         {
237                 boost::mutex::scoped_lock lm (_mutex);
238                 if (p == _position && !force_emit) {
239                         cc.abort ();
240                         return;
241                 }
242
243                 _position = p;
244         }
245 }
246
247 void
248 Content::set_trim_start (ContentTime t)
249 {
250         /* video and audio content can modify its start trim */
251
252         if (video) {
253                 video->modify_trim_start (t);
254         }
255
256         /* See note in ::set_position */
257         if (!video && audio) {
258                 audio->modify_trim_start (t);
259         }
260
261         ContentChangeSignaller cc (this, ContentProperty::TRIM_START);
262
263         {
264                 boost::mutex::scoped_lock lm (_mutex);
265                 _trim_start = t;
266         }
267 }
268
269 void
270 Content::set_trim_end (ContentTime t)
271 {
272         ContentChangeSignaller cc (this, ContentProperty::TRIM_END);
273
274         {
275                 boost::mutex::scoped_lock lm (_mutex);
276                 _trim_end = t;
277         }
278 }
279
280
281 shared_ptr<Content>
282 Content::clone () const
283 {
284         /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */
285         xmlpp::Document doc;
286         auto node = doc.create_root_node ("Content");
287         as_xml (node, true);
288
289         /* notes is unused here (we assume) */
290         list<string> notes;
291         return content_factory (cxml::NodePtr(new cxml::Node(node)), Film::current_state_version, notes);
292 }
293
294 string
295 Content::technical_summary () const
296 {
297         auto s = String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds());
298         if (_video_frame_rate) {
299                 s += String::compose(" %1", *_video_frame_rate);
300         }
301         return s;
302 }
303
304 DCPTime
305 Content::length_after_trim (shared_ptr<const Film> film) const
306 {
307         auto length = max(DCPTime(), full_length(film) - DCPTime(trim_start() + trim_end(), film->active_frame_rate_change(position())));
308         if (video) {
309                 length = length.round(film->video_frame_rate());
310         }
311         return length;
312 }
313
314 /** @return string which changes when something about this content changes which affects
315  *  the appearance of its video.
316  */
317 string
318 Content::identifier () const
319 {
320         char buffer[256];
321         snprintf (
322                 buffer, sizeof(buffer), "%s_%" PRId64 "_%" PRId64 "_%" PRId64,
323                 Content::digest().c_str(), position().get(), trim_start().get(), trim_end().get()
324                 );
325         return buffer;
326 }
327
328 bool
329 Content::paths_valid () const
330 {
331         for (auto i: _paths) {
332                 if (!boost::filesystem::exists (i)) {
333                         return false;
334                 }
335         }
336
337         return true;
338 }
339
340 void
341 Content::set_paths (vector<boost::filesystem::path> paths)
342 {
343         ContentChangeSignaller cc (this, ContentProperty::PATH);
344
345         {
346                 boost::mutex::scoped_lock lm (_mutex);
347                 _paths = paths;
348                 _last_write_times.clear ();
349                 for (auto i: _paths) {
350                         boost::system::error_code ec;
351                         auto last_write = boost::filesystem::last_write_time(i, ec);
352                         _last_write_times.push_back (ec ? 0 : last_write);
353                 }
354         }
355 }
356
357 string
358 Content::path_summary () const
359 {
360         /* XXX: should handle multiple paths more gracefully */
361
362         DCPOMATIC_ASSERT (number_of_paths ());
363
364         auto s = path(0).filename().string();
365         if (number_of_paths() > 1) {
366                 s += " ...";
367         }
368
369         return s;
370 }
371
372 /** @return a list of properties that might be interesting to the user */
373 list<UserProperty>
374 Content::user_properties (shared_ptr<const Film> film) const
375 {
376         list<UserProperty> p;
377         add_properties (film, p);
378         return p;
379 }
380
381 /** @return DCP times of points within this content where a reel split could occur */
382 list<DCPTime>
383 Content::reel_split_points (shared_ptr<const Film>) const
384 {
385         list<DCPTime> t;
386         /* This is only called for video content and such content has its position forced
387            to start on a frame boundary.
388         */
389         t.push_back (position());
390         return t;
391 }
392
393 void
394 Content::set_video_frame_rate (double r)
395 {
396         ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
397
398         {
399                 boost::mutex::scoped_lock lm (_mutex);
400                 if (_video_frame_rate && fabs(r - *_video_frame_rate) < VIDEO_FRAME_RATE_EPSILON) {
401                         cc.abort();
402                 }
403                 _video_frame_rate = r;
404         }
405
406         /* Make sure trim is still on a frame boundary */
407         if (video) {
408                 set_trim_start (trim_start());
409         }
410 }
411
412 void
413 Content::unset_video_frame_rate ()
414 {
415         ContentChangeSignaller cc (this, ContentProperty::VIDEO_FRAME_RATE);
416
417         {
418                 boost::mutex::scoped_lock lm (_mutex);
419                 _video_frame_rate = optional<double>();
420         }
421 }
422
423 double
424 Content::active_video_frame_rate (shared_ptr<const Film> film) const
425 {
426         {
427                 boost::mutex::scoped_lock lm (_mutex);
428                 if (_video_frame_rate) {
429                         return _video_frame_rate.get ();
430                 }
431         }
432
433         /* No frame rate specified, so assume this content has been
434            prepared for any concurrent video content or perhaps
435            just the DCP rate.
436         */
437         return film->active_frame_rate_change(position()).source;
438 }
439
440 void
441 Content::add_properties (shared_ptr<const Film>, list<UserProperty>& p) const
442 {
443         p.push_back (UserProperty (UserProperty::GENERAL, _("Filename"), path(0).string ()));
444
445         if (_video_frame_rate) {
446                 if (video) {
447                         p.push_back (
448                                 UserProperty (
449                                         UserProperty::VIDEO,
450                                         _("Frame rate"),
451                                         locale_convert<string> (_video_frame_rate.get(), 5),
452                                         _("frames per second")
453                                         )
454                                 );
455                 } else {
456                         p.push_back (
457                                 UserProperty (
458                                         UserProperty::GENERAL,
459                                         _("Prepared for video frame rate"),
460                                         locale_convert<string> (_video_frame_rate.get(), 5),
461                                         _("frames per second")
462                                         )
463                                 );
464                 }
465         }
466 }
467
468 /** Take settings from the given content if it is of the correct type */
469 void
470 Content::take_settings_from (shared_ptr<const Content> c)
471 {
472         if (video && c->video) {
473                 video->take_settings_from (c->video);
474         }
475         if (audio && c->audio) {
476                 audio->take_settings_from (c->audio);
477         }
478
479         auto i = text.begin ();
480         auto j = c->text.begin ();
481         while (i != text.end() && j != c->text.end()) {
482                 (*i)->take_settings_from (*j);
483                 ++i;
484                 ++j;
485         }
486 }
487
488 shared_ptr<TextContent>
489 Content::only_text () const
490 {
491         DCPOMATIC_ASSERT (text.size() < 2);
492         if (text.empty ()) {
493                 return shared_ptr<TextContent> ();
494         }
495         return text.front ();
496 }
497
498 shared_ptr<TextContent>
499 Content::text_of_original_type (TextType type) const
500 {
501         for (auto i: text) {
502                 if (i->original_type() == type) {
503                         return i;
504                 }
505         }
506
507         return shared_ptr<TextContent> ();
508 }
509
510 void
511 Content::add_path (boost::filesystem::path p)
512 {
513         boost::mutex::scoped_lock lm (_mutex);
514         _paths.push_back (p);
515         boost::system::error_code ec;
516         auto last_write = boost::filesystem::last_write_time(p, ec);
517         _last_write_times.push_back (ec ? 0 : last_write);
518 }