Be more careful about allowing possibly-trouble-causing characters in DCP filenames.
[dcpomatic.git] / src / lib / film.cc
index edb9112177ef7362fdc5da2585a7565552c1772a..dd31388b6fd7adf58210cd49fa72e3432822830e 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2017 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -57,6 +57,8 @@
 #include <dcp/local_time.h>
 #include <dcp/decrypted_kdm.h>
 #include <dcp/raw_convert.h>
+#include <dcp/reel_mxf.h>
+#include <dcp/reel_asset.h>
 #include <libxml++/libxml++.h>
 #include <boost/filesystem.hpp>
 #include <boost/algorithm/string.hpp>
@@ -83,6 +85,9 @@ using std::cout;
 using std::list;
 using std::set;
 using std::runtime_error;
+using std::copy;
+using std::back_inserter;
+using std::map;
 using boost::shared_ptr;
 using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
@@ -124,7 +129,7 @@ int const Film::current_state_version = 36;
  *  @param dir Film directory.
  */
 
-Film::Film (boost::filesystem::path dir, bool log)
+Film::Film (optional<boost::filesystem::path> dir)
        : _playlist (new Playlist)
        , _use_isdcf_name (true)
        , _dcp_content_type (Config::instance()->default_dcp_content_type ())
@@ -132,6 +137,7 @@ Film::Film (boost::filesystem::path dir, bool log)
        , _resolution (RESOLUTION_2K)
        , _signed (true)
        , _encrypted (false)
+       , _context_id (dcp::make_uuid ())
        , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ())
        , _isdcf_metadata (Config::instance()->default_isdcf_metadata ())
        , _video_frame_rate (24)
@@ -152,27 +158,30 @@ Film::Film (boost::filesystem::path dir, bool log)
        _playlist_order_changed_connection = _playlist->OrderChanged.connect (bind (&Film::playlist_order_changed, this));
        _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2, _3));
 
-       /* Make state.directory a complete path without ..s (where possible)
-          (Code swiped from Adam Bowen on stackoverflow)
-          XXX: couldn't/shouldn't this just be boost::filesystem::canonical?
-       */
+       if (dir) {
+               /* Make state.directory a complete path without ..s (where possible)
+                  (Code swiped from Adam Bowen on stackoverflow)
+                  XXX: couldn't/shouldn't this just be boost::filesystem::canonical?
+               */
 
-       boost::filesystem::path p (boost::filesystem::system_complete (dir));
-       boost::filesystem::path result;
-       for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
-               if (*i == "..") {
-                       if (boost::filesystem::is_symlink (result) || result.filename() == "..") {
+               boost::filesystem::path p (boost::filesystem::system_complete (dir.get()));
+               boost::filesystem::path result;
+               for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
+                       if (*i == "..") {
+                               if (boost::filesystem::is_symlink (result) || result.filename() == "..") {
+                                       result /= *i;
+                               } else {
+                                       result = result.parent_path ();
+                               }
+                       } else if (*i != ".") {
                                result /= *i;
-                       } else {
-                               result = result.parent_path ();
                        }
-               } else if (*i != ".") {
-                       result /= *i;
                }
+
+               set_directory (result.make_preferred ());
        }
 
-       set_directory (result.make_preferred ());
-       if (log) {
+       if (_directory) {
                _log.reset (new FileLog (file ("log")));
        } else {
                _log.reset (new NullLog);
@@ -284,6 +293,35 @@ Film::make_dcp ()
                throw BadSettingError (_("name"), _("cannot contain slashes"));
        }
 
+       if (container() == 0) {
+               throw MissingSettingError (_("container"));
+       }
+
+       if (content().empty()) {
+               throw runtime_error (_("you must add some content to the DCP before creating it"));
+       }
+
+       if (dcp_content_type() == 0) {
+               throw MissingSettingError (_("content type"));
+       }
+
+       if (name().empty()) {
+               throw MissingSettingError (_("name"));
+       }
+
+       BOOST_FOREACH (shared_ptr<const Content> i, content ()) {
+               if (!i->paths_valid()) {
+                       throw runtime_error (_("some of your content is missing"));
+               }
+               shared_ptr<const DCPContent> dcp = dynamic_pointer_cast<const DCPContent> (i);
+               if (dcp && dcp->needs_kdm()) {
+                       throw runtime_error (_("some of your content needs a KDM"));
+               }
+               if (dcp && dcp->needs_assets()) {
+                       throw runtime_error (_("some of your content needs an OV"));
+               }
+       }
+
        set_isdcf_date_today ();
 
        BOOST_FOREACH (string i, environment_info ()) {
@@ -301,22 +339,6 @@ Film::make_dcp ()
        }
        LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth());
 
-       if (container() == 0) {
-               throw MissingSettingError (_("container"));
-       }
-
-       if (content().empty()) {
-               throw runtime_error (_("You must add some content to the DCP before creating it"));
-       }
-
-       if (dcp_content_type() == 0) {
-               throw MissingSettingError (_("content type"));
-       }
-
-       if (name().empty()) {
-               throw MissingSettingError (_("name"));
-       }
-
        JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this())));
 }
 
@@ -329,7 +351,7 @@ Film::send_dcp_to_tms ()
 }
 
 shared_ptr<xmlpp::Document>
-Film::metadata () const
+Film::metadata (bool with_content_paths) const
 {
        shared_ptr<xmlpp::Document> doc (new xmlpp::Document);
        xmlpp::Element* root = doc->create_root_node ("Metadata");
@@ -358,13 +380,14 @@ Film::metadata () const
        root->add_child("Signed")->add_child_text (_signed ? "1" : "0");
        root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
        root->add_child("Key")->add_child_text (_key.hex ());
+       root->add_child("ContextID")->add_child_text (_context_id);
        if (_audio_processor) {
                root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ());
        }
        root->add_child("ReelType")->add_child_text (raw_convert<string> (static_cast<int> (_reel_type)));
        root->add_child("ReelLength")->add_child_text (raw_convert<string> (_reel_length));
        root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0");
-       _playlist->as_xml (root->add_child ("Playlist"));
+       _playlist->as_xml (root->add_child ("Playlist"), with_content_paths);
 
        return doc;
 }
@@ -373,24 +396,38 @@ Film::metadata () const
 void
 Film::write_metadata () const
 {
-       boost::filesystem::create_directories (directory ());
+       DCPOMATIC_ASSERT (directory());
+       boost::filesystem::create_directories (directory().get());
        shared_ptr<xmlpp::Document> doc = metadata ();
        doc->write_to_file_formatted (file("metadata.xml").string ());
        _dirty = false;
 }
 
+/** Write a template from this film */
+void
+Film::write_template (boost::filesystem::path path) const
+{
+       boost::filesystem::create_directories (path.parent_path());
+       shared_ptr<xmlpp::Document> doc = metadata (false);
+       doc->write_to_file_formatted (path.string ());
+}
+
 /** Read state from our metadata file.
  *  @return Notes about things that the user should know about, or empty.
  */
 list<string>
-Film::read_metadata ()
+Film::read_metadata (optional<boost::filesystem::path> path)
 {
-       if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) {
-               throw runtime_error (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version.  You will need to create a new Film, re-add your content and set it up again.  Sorry!"));
+       if (!path) {
+               if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) {
+                       throw runtime_error (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version.  You will need to create a new Film, re-add your content and set it up again.  Sorry!"));
+               }
+
+               path = file ("metadata.xml");
        }
 
        cxml::Document f ("Metadata");
-       f.read_file (file ("metadata.xml"));
+       f.read_file (path.get ());
 
        _state_version = f.number_child<int> ("Version");
        if (_state_version > current_state_version) {
@@ -446,6 +483,7 @@ Film::read_metadata ()
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
        _key = dcp::Key (f.string_child ("Key"));
+       _context_id = f.optional_string_child("ContextID").get_value_or (dcp::make_uuid ());
 
        if (f.optional_string_child ("AudioProcessor")) {
                _audio_processor = AudioProcessor::from_id (f.string_child ("AudioProcessor"));
@@ -462,7 +500,9 @@ Film::read_metadata ()
        _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version, notes);
 
        /* Write backtraces to this film's directory, until another film is loaded */
-       set_backtrace_file (file ("backtrace.txt"));
+       if (_directory) {
+               set_backtrace_file (file ("backtrace.txt"));
+       }
 
        _dirty = false;
        return notes;
@@ -474,8 +514,10 @@ Film::read_metadata ()
 boost::filesystem::path
 Film::dir (boost::filesystem::path d) const
 {
+       DCPOMATIC_ASSERT (_directory);
+
        boost::filesystem::path p;
-       p /= _directory;
+       p /= _directory.get();
        p /= d;
 
        boost::filesystem::create_directories (p);
@@ -489,8 +531,10 @@ Film::dir (boost::filesystem::path d) const
 boost::filesystem::path
 Film::file (boost::filesystem::path f) const
 {
+       DCPOMATIC_ASSERT (_directory);
+
        boost::filesystem::path p;
-       p /= _directory;
+       p /= _directory.get();
        p /= f;
 
        boost::filesystem::create_directories (p.parent_path ());
@@ -747,24 +791,10 @@ Film::dcp_name (bool if_created_now) const
 {
        string unfiltered;
        if (use_isdcf_name()) {
-               unfiltered = isdcf_name (if_created_now);
-       } else {
-               unfiltered = name ();
+               return careful_string_filter (isdcf_name (if_created_now));
        }
 
-       /* Filter out `bad' characters which cause problems with some systems.
-          There's no apparent list of what really is allowed, so this is a guess.
-       */
-
-       string filtered;
-       string const allowed = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
-       for (size_t i = 0; i < unfiltered.size(); ++i) {
-               if (allowed.find (unfiltered[i]) != string::npos) {
-                       filtered += unfiltered[i];
-               }
-       }
-
-       return filtered;
+       return careful_string_filter (name ());
 }
 
 void
@@ -943,9 +973,13 @@ Film::j2c_path (int reel, Frame frame, Eyes eyes, bool tmp) const
 vector<CPLSummary>
 Film::cpls () const
 {
+       if (!directory ()) {
+               return vector<CPLSummary> ();
+       }
+
        vector<CPLSummary> out;
 
-       boost::filesystem::path const dir = directory ();
+       boost::filesystem::path const dir = directory().get();
        for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator(dir); i != boost::filesystem::directory_iterator(); ++i) {
                if (
                        boost::filesystem::is_directory (*i) &&
@@ -1000,17 +1034,10 @@ Film::content () const
        return _playlist->content ();
 }
 
-void
-Film::examine_content (shared_ptr<Content> c)
-{
-       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
-       JobManager::instance()->add (j);
-}
-
 void
 Film::examine_and_add_content (shared_ptr<Content> c)
 {
-       if (dynamic_pointer_cast<FFmpegContent> (c) && !_directory.empty ()) {
+       if (dynamic_pointer_cast<FFmpegContent> (c) && _directory) {
                run_ffprobe (c->path(0), file ("ffprobe.log"), _log);
        }
 
@@ -1037,6 +1064,7 @@ Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c)
        }
 
        add_content (content);
+
        if (Config::instance()->automatic_audio_analysis() && content->audio) {
                shared_ptr<Playlist> playlist (new Playlist);
                playlist->add (content);
@@ -1058,6 +1086,15 @@ Film::add_content (shared_ptr<Content> c)
                c->set_position (_playlist->subtitle_end ());
        }
 
+       if (_template_film) {
+               /* Take settings from the first piece of content of c's type in _template */
+               BOOST_FOREACH (shared_ptr<Content> i, _template_film->content()) {
+                       if (typeid(i.get()) == typeid(c.get())) {
+                               c->use_template (i);
+                       }
+               }
+       }
+
        _playlist->add (c);
 }
 
@@ -1186,8 +1223,44 @@ Film::make_kdm (
                throw InvalidSignerError ();
        }
 
+       /* Find keys that have been added to imported, encrypted DCP content */
+       list<dcp::DecryptedKDMKey> imported_keys;
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent> (i);
+               if (d && d->kdm()) {
+                       dcp::DecryptedKDM kdm (d->kdm().get(), Config::instance()->decryption_chain()->key().get());
+                       list<dcp::DecryptedKDMKey> keys = kdm.keys ();
+                       copy (keys.begin(), keys.end(), back_inserter (imported_keys));
+               }
+       }
+
+       map<shared_ptr<const dcp::ReelMXF>, dcp::Key> keys;
+
+       BOOST_FOREACH(shared_ptr<const dcp::ReelAsset> i, cpl->reel_assets ()) {
+               shared_ptr<const dcp::ReelMXF> mxf = boost::dynamic_pointer_cast<const dcp::ReelMXF> (i);
+               if (!mxf || !mxf->key_id()) {
+                       continue;
+               }
+
+               /* Get any imported key for this ID */
+               bool done = false;
+               BOOST_FOREACH (dcp::DecryptedKDMKey j, imported_keys) {
+                       if (j.id() == mxf->key_id().get()) {
+                               LOG_GENERAL ("Using imported key for %1", mxf->key_id().get());
+                               keys[mxf] = j.key();
+                               done = true;
+                       }
+               }
+
+               if (!done) {
+                       /* No imported key; it must be an asset that we encrypted */
+                       LOG_GENERAL ("Using our own key for %1", mxf->key_id().get());
+                       keys[mxf] = key();
+               }
+       }
+
        return dcp::DecryptedKDM (
-               cpl, key(), from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string()
+               cpl->id(), keys, from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string()
                ).encrypt (signer, recipient, trusted_devices, formulation);
 }
 
@@ -1353,24 +1426,12 @@ Film::audio_output_names () const
        DCPOMATIC_ASSERT (MAX_DCP_AUDIO_CHANNELS == 16);
 
        vector<string> n;
-       n.push_back (_("L"));
-       n.push_back (_("R"));
-       n.push_back (_("C"));
-       n.push_back (_("Lfe"));
-       n.push_back (_("Ls"));
-       n.push_back (_("Rs"));
-       n.push_back (_("HI"));
-       n.push_back (_("VI"));
-       n.push_back (_("Lc"));
-       n.push_back (_("Rc"));
-       n.push_back (_("BsL"));
-       n.push_back (_("BsR"));
-       n.push_back (_("DBP"));
-       n.push_back (_("DBS"));
-       n.push_back ("");
-       n.push_back ("");
-
-       return vector<string> (n.begin(), n.begin() + audio_channels ());
+
+       for (int i = 0; i < audio_channels(); ++i) {
+               n.push_back (short_audio_channel_name (i));
+       }
+
+       return n;
 }
 
 void
@@ -1395,7 +1456,7 @@ list<DCPTimePeriod>
 Film::reels () const
 {
        list<DCPTimePeriod> p;
-       DCPTime const len = length().round_up (video_frame_rate ());
+       DCPTime const len = length().ceil (video_frame_rate ());
 
        switch (reel_type ()) {
        case REELTYPE_SINGLE:
@@ -1487,3 +1548,26 @@ Film::fix_conflicting_settings ()
 
        return notes;
 }
+
+void
+Film::use_template (string name)
+{
+       _template_film.reset (new Film (optional<boost::filesystem::path>()));
+       _template_film->read_metadata (Config::instance()->template_path (name));
+       _use_isdcf_name = _template_film->_use_isdcf_name;
+       _dcp_content_type = _template_film->_dcp_content_type;
+       _container = _template_film->_container;
+       _resolution = _template_film->_resolution;
+       _j2k_bandwidth = _template_film->_j2k_bandwidth;
+       _video_frame_rate = _template_film->_video_frame_rate;
+       _signed = _template_film->_signed;
+       _encrypted = _template_film->_encrypted;
+       _audio_channels = _template_film->_audio_channels;
+       _sequence = _template_film->_sequence;
+       _three_d = _template_film->_three_d;
+       _interop = _template_film->_interop;
+       _audio_processor = _template_film->_audio_processor;
+       _reel_type = _template_film->_reel_type;
+       _reel_length = _template_film->_reel_length;
+       _upload_after_make_dcp = _template_film->_upload_after_make_dcp;
+}