Try to improve path handling a bit, and add a few tests.
[dcpomatic.git] / src / lib / film.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 #include <stdexcept>
21 #include <iostream>
22 #include <fstream>
23 #include <cstdlib>
24 #include <sstream>
25 #include <iomanip>
26 #include <unistd.h>
27 #include <boost/filesystem.hpp>
28 #include <boost/algorithm/string.hpp>
29 #ifdef DVDOMATIC_WINDOWS
30 #include <winsock2.h>
31 #endif
32 #include "film.h"
33 #include "format.h"
34 #include "tiff_encoder.h"
35 #include "job.h"
36 #include "filter.h"
37 #include "transcoder.h"
38 #include "util.h"
39 #include "job_manager.h"
40 #include "ab_transcode_job.h"
41 #include "transcode_job.h"
42 #include "scp_dcp_job.h"
43 #include "copy_from_dvd_job.h"
44 #include "make_dcp_job.h"
45 #include "film_state.h"
46 #include "log.h"
47 #include "options.h"
48 #include "exceptions.h"
49 #include "examine_content_job.h"
50 #include "scaler.h"
51 #include "decoder_factory.h"
52 #include "config.h"
53
54 using namespace std;
55 using namespace boost;
56
57 /** Construct a Film object in a given directory, reading any metadata
58  *  file that exists in that directory.  An exception will be thrown if
59  *  must_exist is true, and the specified directory does not exist.
60  *
61  *  @param d Film directory.
62  *  @param must_exist true to throw an exception if does not exist.
63  */
64
65 Film::Film (string d, bool must_exist)
66         : _dirty (false)
67 {
68         /* Make _state.directory a complete path without ..s (where possible)
69            (Code swiped from Adam Bowen on stackoverflow)
70         */
71         
72         filesystem::path p (filesystem::system_complete (d));
73         filesystem::path result;
74         for(filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
75                 if (*i == "..") {
76                         if (filesystem::is_symlink (result) || result.filename() == "..") {
77                                 result /= *i;
78                         } else {
79                                 result = result.parent_path ();
80                         }
81                 } else if (*i != ".") {
82                         result /= *i;
83                 }
84         }
85
86         _state.directory = result.string ();
87         
88         if (must_exist && !filesystem::exists (_state.directory)) {
89                 throw OpenFileError (_state.directory);
90         }
91
92         read_metadata ();
93
94         _log = new Log (_state.file ("log"));
95 }
96
97 /** Copy constructor */
98 Film::Film (Film const & other)
99         : _state (other._state)
100         , _dirty (other._dirty)
101 {
102
103 }
104
105 Film::~Film ()
106 {
107         delete _log;
108 }
109           
110 /** Read the `metadata' file inside this Film's directory, and fill the
111  *  object's data with its content.
112  */
113
114 void
115 Film::read_metadata ()
116 {
117         ifstream f (metadata_file().c_str ());
118         string line;
119         while (getline (f, line)) {
120                 if (line.empty ()) {
121                         continue;
122                 }
123                 
124                 if (line[0] == '#') {
125                         continue;
126                 }
127
128                 size_t const s = line.find (' ');
129                 if (s == string::npos) {
130                         continue;
131                 }
132
133                 _state.read_metadata (line.substr (0, s), line.substr (s + 1));
134         }
135
136         _dirty = false;
137 }
138
139 /** Write our state to a file `metadata' inside the Film's directory */
140 void
141 Film::write_metadata () const
142 {
143         filesystem::create_directories (_state.directory);
144         
145         ofstream f (metadata_file().c_str ());
146         if (!f.good ()) {
147                 throw CreateFileError (metadata_file ());
148         }
149
150         _state.write_metadata (f);
151
152         _dirty = false;
153 }
154
155 /** Set the name by which DVD-o-matic refers to this Film */
156 void
157 Film::set_name (string n)
158 {
159         _state.name = n;
160         signal_changed (NAME);
161 }
162
163 /** Set the content file for this film.
164  *  @param c New content file; if specified as an absolute path, the content should
165  *  be within the film's _state.directory; if specified as a relative path, the content
166  *  will be assumed to be within the film's _state.directory.
167  */
168 void
169 Film::set_content (string c)
170 {
171         if (filesystem::path(c).has_root_directory () && starts_with (c, _state.directory)) {
172                 c = c.substr (_state.directory.length() + 1);
173         }
174
175         if (c == _state.content) {
176                 return;
177         }
178         
179         /* Create a temporary decoder so that we can get information
180            about the content.
181         */
182         shared_ptr<FilmState> s = state_copy ();
183         s->content = c;
184         shared_ptr<Options> o (new Options ("", "", ""));
185         o->out_size = Size (1024, 1024);
186
187         shared_ptr<Decoder> d = decoder_factory (s, o, 0, _log);
188         
189         _state.size = d->native_size ();
190         _state.length = d->length_in_frames ();
191         _state.frames_per_second = d->frames_per_second ();
192         _state.audio_channels = d->audio_channels ();
193         _state.audio_sample_rate = d->audio_sample_rate ();
194         _state.audio_sample_format = d->audio_sample_format ();
195
196         _state.content_digest = md5_digest (c);
197         _state.content = c;
198         
199         signal_changed (SIZE);
200         signal_changed (LENGTH);
201         signal_changed (FRAMES_PER_SECOND);
202         signal_changed (AUDIO_CHANNELS);
203         signal_changed (AUDIO_SAMPLE_RATE);
204         signal_changed (CONTENT);
205 }
206
207 /** Set the format that this Film should be shown in */
208 void
209 Film::set_format (Format const * f)
210 {
211         _state.format = f;
212         signal_changed (FORMAT);
213 }
214
215 /** Set the type to specify the DCP as having
216  *  (feature, trailer etc.)
217  */
218 void
219 Film::set_dcp_content_type (DCPContentType const * t)
220 {
221         _state.dcp_content_type = t;
222         signal_changed (DCP_CONTENT_TYPE);
223 }
224
225 /** Set the number of pixels by which to crop the left of the source video */
226 void
227 Film::set_left_crop (int c)
228 {
229         if (c == _state.left_crop) {
230                 return;
231         }
232         
233         _state.left_crop = c;
234         signal_changed (LEFT_CROP);
235 }
236
237 /** Set the number of pixels by which to crop the right of the source video */
238 void
239 Film::set_right_crop (int c)
240 {
241         if (c == _state.right_crop) {
242                 return;
243         }
244
245         _state.right_crop = c;
246         signal_changed (RIGHT_CROP);
247 }
248
249 /** Set the number of pixels by which to crop the top of the source video */
250 void
251 Film::set_top_crop (int c)
252 {
253         if (c == _state.top_crop) {
254                 return;
255         }
256         
257         _state.top_crop = c;
258         signal_changed (TOP_CROP);
259 }
260
261 /** Set the number of pixels by which to crop the bottom of the source video */
262 void
263 Film::set_bottom_crop (int c)
264 {
265         if (c == _state.bottom_crop) {
266                 return;
267         }
268         
269         _state.bottom_crop = c;
270         signal_changed (BOTTOM_CROP);
271 }
272
273 /** Set the filters to apply to the image when generating thumbnails
274  *  or a DCP.
275  */
276 void
277 Film::set_filters (vector<Filter const *> const & f)
278 {
279         _state.filters = f;
280         signal_changed (FILTERS);
281 }
282
283 /** Set the number of frames to put in any generated DCP (from
284  *  the start of the film).  0 indicates that all frames should
285  *  be used.
286  */
287 void
288 Film::set_dcp_frames (int n)
289 {
290         _state.dcp_frames = n;
291         signal_changed (DCP_FRAMES);
292 }
293
294 void
295 Film::set_dcp_trim_action (TrimAction a)
296 {
297         _state.dcp_trim_action = a;
298         signal_changed (DCP_TRIM_ACTION);
299 }
300
301 /** Set whether or not to generate a A/B comparison DCP.
302  *  Such a DCP has the left half of its frame as the Film
303  *  content without any filtering or post-processing; the
304  *  right half is rendered with filters and post-processing.
305  */
306 void
307 Film::set_dcp_ab (bool a)
308 {
309         _state.dcp_ab = a;
310         signal_changed (DCP_AB);
311 }
312
313 void
314 Film::set_audio_gain (float g)
315 {
316         _state.audio_gain = g;
317         signal_changed (AUDIO_GAIN);
318 }
319
320 void
321 Film::set_audio_delay (int d)
322 {
323         _state.audio_delay = d;
324         signal_changed (AUDIO_DELAY);
325 }
326
327 /** @return path of metadata file */
328 string
329 Film::metadata_file () const
330 {
331         return _state.file ("metadata");
332 }
333
334 /** @return full path of the content (actual video) file
335  *  of this Film.
336  */
337 string
338 Film::content () const
339 {
340         return _state.content_path ();
341 }
342
343 /** The pre-processing GUI part of a thumbs update.
344  *  Must be called from the GUI thread.
345  */
346 void
347 Film::update_thumbs_pre_gui ()
348 {
349         _state.thumbs.clear ();
350         filesystem::remove_all (_state.dir ("thumbs"));
351
352         /* This call will recreate the directory */
353         _state.dir ("thumbs");
354 }
355
356 /** The post-processing GUI part of a thumbs update.
357  *  Must be called from the GUI thread.
358  */
359 void
360 Film::update_thumbs_post_gui ()
361 {
362         string const tdir = _state.dir ("thumbs");
363         
364         for (filesystem::directory_iterator i = filesystem::directory_iterator (tdir); i != filesystem::directory_iterator(); ++i) {
365
366                 /* Aah, the sweet smell of progress */
367 #if BOOST_FILESYSTEM_VERSION == 3               
368                 string const l = filesystem::path(*i).leaf().generic_string();
369 #else
370                 string const l = i->leaf ();
371 #endif
372                 
373                 size_t const d = l.find (".tiff");
374                 if (d != string::npos) {
375                         _state.thumbs.push_back (atoi (l.substr (0, d).c_str()));
376                 }
377         }
378
379         sort (_state.thumbs.begin(), _state.thumbs.end());
380         
381         write_metadata ();
382         signal_changed (THUMBS);
383 }
384
385 /** @return the number of thumbnail images that we have */
386 int
387 Film::num_thumbs () const
388 {
389         return _state.thumbs.size ();
390 }
391
392 /** @param n A thumb index.
393  *  @return The frame within the Film that it is for.
394  */
395 int
396 Film::thumb_frame (int n) const
397 {
398         return _state.thumb_frame (n);
399 }
400
401 /** @param n A thumb index.
402  *  @return The path to the thumb's image file.
403  */
404 string
405 Film::thumb_file (int n) const
406 {
407         return _state.thumb_file (n);
408 }
409
410 /** @return The path to the directory to write JPEG2000 files to */
411 string
412 Film::j2k_dir () const
413 {
414         assert (format());
415
416         filesystem::path p;
417
418
419         /* Start with j2c */
420         p /= "j2c";
421
422         pair<string, string> f = Filter::ffmpeg_strings (filters ());
423
424         /* Write stuff to specify the filter / post-processing settings that are in use,
425            so that we don't get confused about J2K files generated using different
426            settings.
427         */
428         stringstream s;
429         s << _state.format->nickname()
430           << "_" << _state.content_digest
431           << "_" << left_crop() << "_" << right_crop() << "_" << top_crop() << "_" << bottom_crop()
432           << "_" << f.first << "_" << f.second
433           << "_" << _state.scaler->id();
434
435         p /= s.str ();
436
437         /* Similarly for the A/B case */
438         if (dcp_ab()) {
439                 stringstream s;
440                 pair<string, string> fa = Filter::ffmpeg_strings (Config::instance()->reference_filters());
441                 s << "ab_" << Config::instance()->reference_scaler()->id() << "_" << fa.first << "_" << fa.second;
442                 p /= s.str ();
443         }
444         
445         return _state.dir (p.string ());
446 }
447
448 /** Handle a change to the Film's metadata */
449 void
450 Film::signal_changed (Property p)
451 {
452         _dirty = true;
453         Changed (p);
454 }
455
456 /** Add suitable Jobs to the JobManager to create a DCP for this Film.
457  *  @param true to transcode, false to use the WAV and J2K files that are already there.
458  */
459 void
460 Film::make_dcp (bool transcode, int freq)
461 {
462         string const t = name ();
463         if (t.find ("/") != string::npos) {
464                 throw BadSettingError ("name", "cannot contain slashes");
465         }
466         
467         {
468                 stringstream s;
469                 s << "DVD-o-matic " << DVDOMATIC_VERSION << " using " << dependency_version_summary ();
470                 log()->log (s.str ());
471         }
472
473         {
474                 char buffer[128];
475                 gethostname (buffer, sizeof (buffer));
476                 stringstream s;
477                 s << "Starting to make a DCP on " << buffer;
478                 log()->log (s.str ());
479         }
480                 
481         if (format() == 0) {
482                 throw MissingSettingError ("format");
483         }
484
485         if (content().empty ()) {
486                 throw MissingSettingError ("content");
487         }
488
489         if (dcp_content_type() == 0) {
490                 throw MissingSettingError ("content type");
491         }
492
493         if (name().empty()) {
494                 throw MissingSettingError ("name");
495         }
496
497         shared_ptr<const FilmState> fs = state_copy ();
498         shared_ptr<Options> o (new Options (j2k_dir(), ".j2c", _state.dir ("wavs")));
499         o->out_size = format()->dcp_size ();
500         if (dcp_frames() == 0) {
501                 /* Decode the whole film, no blacking */
502                 o->num_frames = 0;
503                 o->black_after = 0;
504         } else {
505                 switch (dcp_trim_action()) {
506                 case CUT:
507                         /* Decode only part of the film, no blacking */
508                         o->num_frames = dcp_frames ();
509                         o->black_after = 0;
510                         break;
511                 case BLACK_OUT:
512                         /* Decode the whole film, but black some frames out */
513                         o->num_frames = 0;
514                         o->black_after = dcp_frames ();
515                 }
516         }
517         
518         o->decode_video_frequency = freq;
519         o->padding = format()->dcp_padding ();
520         o->ratio = format()->ratio_as_float ();
521
522         if (transcode) {
523                 if (_state.dcp_ab) {
524                         JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (fs, o, log ())));
525                 } else {
526                         JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (fs, o, log ())));
527                 }
528         }
529         
530         JobManager::instance()->add (shared_ptr<Job> (new MakeDCPJob (fs, o, log ())));
531 }
532
533 shared_ptr<FilmState>
534 Film::state_copy () const
535 {
536         return shared_ptr<FilmState> (new FilmState (_state));
537 }
538
539 void
540 Film::copy_from_dvd_post_gui ()
541 {
542         const string dvd_dir = _state.dir ("dvd");
543
544         string largest_file;
545         uintmax_t largest_size = 0;
546         for (filesystem::directory_iterator i = filesystem::directory_iterator (dvd_dir); i != filesystem::directory_iterator(); ++i) {
547                 uintmax_t const s = filesystem::file_size (*i);
548                 if (s > largest_size) {
549
550 #if BOOST_FILESYSTEM_VERSION == 3               
551                         largest_file = filesystem::path(*i).generic_string();
552 #else
553                         largest_file = i->string ();
554 #endif
555                         largest_size = s;
556                 }
557         }
558
559         set_content (largest_file);
560 }
561
562 void
563 Film::examine_content ()
564 {
565         if (_examine_content_job) {
566                 return;
567         }
568         
569         _examine_content_job.reset (new ExamineContentJob (state_copy (), log ()));
570         _examine_content_job->Finished.connect (sigc::mem_fun (*this, &Film::examine_content_post_gui));
571         JobManager::instance()->add (_examine_content_job);
572 }
573
574 void
575 Film::examine_content_post_gui ()
576 {
577         _state.length = _examine_content_job->last_video_frame ();
578         signal_changed (LENGTH);
579         
580         _examine_content_job.reset ();
581 }
582
583 void
584 Film::set_scaler (Scaler const * s)
585 {
586         _state.scaler = s;
587         signal_changed (SCALER);
588 }
589
590 void
591 Film::set_frames_per_second (float f)
592 {
593         _state.frames_per_second = f;
594         signal_changed (FRAMES_PER_SECOND);
595 }
596
597 /** @return full paths to any audio files that this Film has */
598 vector<string>
599 Film::audio_files () const
600 {
601         vector<string> f;
602         for (filesystem::directory_iterator i = filesystem::directory_iterator (_state.dir("wavs")); i != filesystem::directory_iterator(); ++i) {
603                 f.push_back (i->path().string ());
604         }
605
606         return f;
607 }
608
609 ContentType
610 Film::content_type () const
611 {
612         return _state.content_type ();
613 }
614
615 void
616 Film::set_still_duration (int d)
617 {
618         _state.still_duration = d;
619         signal_changed (STILL_DURATION);
620 }
621
622 void
623 Film::send_dcp_to_tms ()
624 {
625         shared_ptr<Job> j (new SCPDCPJob (state_copy (), log ()));
626         JobManager::instance()->add (j);
627 }
628
629 void
630 Film::copy_from_dvd ()
631 {
632         shared_ptr<Job> j (new CopyFromDVDJob (state_copy (), log ()));
633         j->Finished.connect (sigc::mem_fun (*this, &Film::copy_from_dvd_post_gui));
634         JobManager::instance()->add (j);
635 }
636