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