Coalesce short reels.
[dcpomatic.git] / src / lib / film.cc
index 79eb313ce44de451e37184094e92454e2b492085..ed2c5a3725946e23dee1c03f5628de8e1cec6cf1 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2012-2017 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
 
     This file is part of DCP-o-matic.
 
@@ -19,7 +19,7 @@
 */
 
 /** @file  src/film.cc
- *  @brief A representation of some audio and video content, and details of
+ *  @brief A representation of some audio, video and subtitle content, and details of
  *  how they should be presented in a DCP.
  */
 
 #include "job.h"
 #include "util.h"
 #include "job_manager.h"
+#include "dcp_encoder.h"
 #include "transcode_job.h"
 #include "upload_job.h"
 #include "null_log.h"
 #include "file_log.h"
+#include "dcpomatic_log.h"
 #include "exceptions.h"
 #include "examine_content_job.h"
 #include "config.h"
 #include "screen.h"
 #include "audio_content.h"
 #include "video_content.h"
-#include "subtitle_content.h"
+#include "text_content.h"
 #include "ffmpeg_content.h"
 #include "dcp_content.h"
 #include "screen_kdm.h"
 #include "cinema.h"
+#include "change_signaller.h"
+#include "check_content_change_job.h"
 #include <libcxml/cxml.h>
 #include <dcp/cpl.h>
 #include <dcp/certificate_chain.h>
@@ -88,15 +92,17 @@ using std::runtime_error;
 using std::copy;
 using std::back_inserter;
 using std::map;
+using std::exception;
+using std::find;
 using boost::shared_ptr;
 using boost::weak_ptr;
 using boost::dynamic_pointer_cast;
 using boost::optional;
 using boost::is_any_of;
 using dcp::raw_convert;
+using namespace dcpomatic;
 
-#define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), LogEntry::TYPE_GENERAL);
-#define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, LogEntry::TYPE_GENERAL);
+string const Film::metadata_file = "metadata.xml";
 
 /* 5 -> 6
  * AudioMapping XML changed.
@@ -121,8 +127,11 @@ using dcp::raw_convert;
  * VideoFrameType in VideoContent is a string rather than an integer.
  * 35 -> 36
  * EffectColour rather than OutlineColour in Subtitle.
+ * 36 -> 37
+ * TextContent can be in a Caption tag, and some of the tag names
+ * have had Subtitle prefixes or suffixes removed.
  */
-int const Film::current_state_version = 36;
+int const Film::current_state_version = 37;
 
 /** Construct a Film object in a given directory.
  *
@@ -148,15 +157,19 @@ Film::Film (optional<boost::filesystem::path> dir)
        , _audio_processor (0)
        , _reel_type (REELTYPE_SINGLE)
        , _reel_length (2000000000)
-       , _upload_after_make_dcp (false)
+       , _upload_after_make_dcp (Config::instance()->default_upload_after_make_dcp())
+       , _reencode_j2k (false)
+       , _user_explicit_video_frame_rate (false)
        , _state_version (current_state_version)
        , _dirty (false)
+       , _tolerant (false)
 {
        set_isdcf_date_today ();
 
-       _playlist_changed_connection = _playlist->Changed.connect (bind (&Film::playlist_changed, this));
-       _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));
+       _playlist_change_connection = _playlist->Change.connect (bind (&Film::playlist_change, this, _1));
+       _playlist_order_changed_connection = _playlist->OrderChange.connect (bind (&Film::playlist_order_changed, this));
+       _playlist_content_change_connection = _playlist->ContentChange.connect (bind (&Film::playlist_content_change, this, _1, _2, _3, _4));
+       _playlist_length_change_connection = _playlist->LengthChange.connect (bind(&Film::playlist_length_change, this));
 
        if (dir) {
                /* Make state.directory a complete path without ..s (where possible)
@@ -168,7 +181,8 @@ Film::Film (optional<boost::filesystem::path> 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::system::error_code ec;
+                               if (boost::filesystem::is_symlink(result, ec) || result.filename() == "..") {
                                        result /= *i;
                                } else {
                                        result = result.parent_path ();
@@ -213,7 +227,8 @@ Film::video_identifier () const
                + "_" + raw_convert<string>(j2k_bandwidth());
 
        if (encrypted ()) {
-               s += "_E";
+               /* This is insecure but hey, the key is in plaintext in metadata.xml */
+               s += "_E" + _key.hex();
        } else {
                s += "_P";
        }
@@ -281,16 +296,21 @@ Film::audio_analysis_path (shared_ptr<const Playlist> playlist) const
                digester.add (audio_processor()->id ());
        }
 
+       digester.add (audio_channels());
+
        p /= digester.get ();
        return p;
 }
 
-/** Add suitable Jobs to the JobManager to create a DCP for this Film */
+/** Add suitable Jobs to the JobManager to create a DCP for this Film.
+ *  @param gui true if this is being called from a GUI tool.
+ *  @param check true to check the content in the project for changes before making the DCP.
+ */
 void
-Film::make_dcp ()
+Film::make_dcp (bool gui, bool check)
 {
        if (dcp_name().find ("/") != string::npos) {
-               throw BadSettingError (_("name"), _("cannot contain slashes"));
+               throw BadSettingError (_("name"), _("Cannot contain slashes"));
        }
 
        if (container() == 0) {
@@ -298,7 +318,11 @@ Film::make_dcp ()
        }
 
        if (content().empty()) {
-               throw runtime_error (_("you must add some content to the DCP before creating it"));
+               throw runtime_error (_("You must add some content to the DCP before creating it"));
+       }
+
+       if (length() == DCPTime()) {
+               throw runtime_error (_("The DCP is empty, perhaps because all the content has zero length."));
        }
 
        if (dcp_content_type() == 0) {
@@ -306,7 +330,7 @@ Film::make_dcp ()
        }
 
        if (name().empty()) {
-               throw MissingSettingError (_("name"));
+               set_name ("DCP");
        }
 
        BOOST_FOREACH (shared_ptr<const Content> i, content ()) {
@@ -315,10 +339,10 @@ Film::make_dcp ()
                }
                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"));
+                       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"));
+                       throw runtime_error (_("Some of your content needs an OV"));
                }
        }
 
@@ -335,11 +359,18 @@ Film::make_dcp ()
        if (Config::instance()->only_servers_encode ()) {
                LOG_GENERAL_NC ("0 threads: ONLY SERVERS SET TO ENCODE");
        } else {
-               LOG_GENERAL ("%1 threads", Config::instance()->num_local_encoding_threads());
+               LOG_GENERAL ("%1 threads", Config::instance()->master_encoding_threads());
        }
        LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth());
 
-       JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this())));
+       shared_ptr<TranscodeJob> tj (new TranscodeJob (shared_from_this()));
+       tj->set_encoder (shared_ptr<Encoder> (new DCPEncoder (shared_from_this(), tj)));
+       if (check) {
+               shared_ptr<CheckContentChangeJob> cc (new CheckContentChangeJob(shared_from_this(), tj, gui));
+               JobManager::instance()->add (cc);
+       } else {
+               JobManager::instance()->add (tj);
+       }
 }
 
 /** Start a job to send our DCP to the configured TMS */
@@ -387,11 +418,29 @@ Film::metadata (bool with_content_paths) const
        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");
+       root->add_child("ReencodeJ2K")->add_child_text (_reencode_j2k ? "1" : "0");
+       root->add_child("UserExplicitVideoFrameRate")->add_child_text(_user_explicit_video_frame_rate ? "1" : "0");
+       for (map<dcp::Marker, DCPTime>::const_iterator i = _markers.begin(); i != _markers.end(); ++i) {
+               xmlpp::Element* m = root->add_child("Marker");
+               m->set_attribute("Type", dcp::marker_to_string(i->first));
+               m->add_child_text(raw_convert<string>(i->second.get()));
+       }
+       BOOST_FOREACH (dcp::Rating i, _ratings) {
+               i.as_xml (root->add_child("Rating"));
+       }
+       root->add_child("ContentVersion")->add_child_text(_content_version);
        _playlist->as_xml (root->add_child ("Playlist"), with_content_paths);
 
        return doc;
 }
 
+void
+Film::write_metadata (boost::filesystem::path path) const
+{
+       shared_ptr<xmlpp::Document> doc = metadata ();
+       doc->write_to_file_formatted (path.string());
+}
+
 /** Write state to our `metadata' file */
 void
 Film::write_metadata () const
@@ -399,7 +448,7 @@ Film::write_metadata () const
        DCPOMATIC_ASSERT (directory());
        boost::filesystem::create_directories (directory().get());
        shared_ptr<xmlpp::Document> doc = metadata ();
-       doc->write_to_file_formatted (file("metadata.xml").string ());
+       doc->write_to_file_formatted (file(metadata_file).string ());
        _dirty = false;
 }
 
@@ -419,11 +468,15 @@ list<string>
 Film::read_metadata (optional<boost::filesystem::path> path)
 {
        if (!path) {
-               if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) {
+               if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file (metadata_file))) {
                        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");
+               path = file (metadata_file);
+       }
+
+       if (!boost::filesystem::exists(*path)) {
+               throw FileNotFoundError(*path);
        }
 
        cxml::Document f ("Metadata");
@@ -432,6 +485,16 @@ Film::read_metadata (optional<boost::filesystem::path> path)
        _state_version = f.number_child<int> ("Version");
        if (_state_version > current_state_version) {
                throw runtime_error (_("This film was created with a newer version of DCP-o-matic, and it cannot be loaded into this version.  Sorry!"));
+       } else if (_state_version < current_state_version) {
+               /* This is an older version; save a copy (if we haven't already) */
+               boost::filesystem::path const older = path->parent_path() / String::compose("metadata.%1.xml", _state_version);
+               if (!boost::filesystem::is_regular_file(older)) {
+                       try {
+                               boost::filesystem::copy_file(*path, older);
+                       } catch (...) {
+                               /* Never mind; at least we tried */
+                       }
+               }
        }
 
        _name = f.string_child ("Name");
@@ -445,6 +508,7 @@ Film::read_metadata (optional<boost::filesystem::path> path)
                _isdcf_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate"));
        }
 
+
        {
                optional<string> c = f.optional_string_child ("DCPContentType");
                if (c) {
@@ -491,12 +555,30 @@ Film::read_metadata (optional<boost::filesystem::path> path)
                _audio_processor = 0;
        }
 
+       if (_audio_processor && !Config::instance()->show_experimental_audio_processors()) {
+               list<AudioProcessor const *> ap = AudioProcessor::visible();
+               if (find(ap.begin(), ap.end(), _audio_processor) == ap.end()) {
+                       Config::instance()->set_show_experimental_audio_processors(true);
+               }
+       }
+
        _reel_type = static_cast<ReelType> (f.optional_number_child<int>("ReelType").get_value_or (static_cast<int>(REELTYPE_SINGLE)));
        _reel_length = f.optional_number_child<int64_t>("ReelLength").get_value_or (2000000000);
        _upload_after_make_dcp = f.optional_bool_child("UploadAfterMakeDCP").get_value_or (false);
+       _reencode_j2k = f.optional_bool_child("ReencodeJ2K").get_value_or(false);
+       _user_explicit_video_frame_rate = f.optional_bool_child("UserExplicitVideoFrameRate").get_value_or(false);
+
+       BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("Marker")) {
+               _markers[dcp::marker_from_string(i->string_attribute("Type"))] = DCPTime(dcp::raw_convert<DCPTime::Type>(i->content()));
+       }
+
+       BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children("Rating")) {
+               _ratings.push_back (dcp::Rating(i));
+       }
+
+       _content_version = f.optional_string_child("ContentVersion").get_value_or("");
 
        list<string> notes;
-       /* This method is the only one that can return notes (so far) */
        _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 */
@@ -509,10 +591,11 @@ Film::read_metadata (optional<boost::filesystem::path> path)
 }
 
 /** Given a directory name, return its full path within the Film's directory.
- *  The directory (and its parents) will be created if they do not exist.
+ *  @param d directory name within the Film's directory.
+ *  @param create true to create the directory (and its parents) if they do not exist.
  */
 boost::filesystem::path
-Film::dir (boost::filesystem::path d) const
+Film::dir (boost::filesystem::path d, bool create) const
 {
        DCPOMATIC_ASSERT (_directory);
 
@@ -520,7 +603,9 @@ Film::dir (boost::filesystem::path d) const
        p /= _directory.get();
        p /= d;
 
-       boost::filesystem::create_directories (p);
+       if (create) {
+               boost::filesystem::create_directories (p);
+       }
 
        return p;
 }
@@ -661,7 +746,7 @@ Film::isdcf_name (bool if_created_now) const
 
        /* XXX: this uses the first bit of content only */
 
-       /* The standard says we don't do this for trailers, for some strange reason */
+       /* Interior aspect ratio.  The standard says we don't do this for trailers, for some strange reason */
        if (dcp_content_type() && dcp_content_type()->libdcp_kind() != dcp::TRAILER) {
                Ratio const * content_ratio = 0;
                BOOST_FOREACH (shared_ptr<Content> i, content ()) {
@@ -677,34 +762,56 @@ Film::isdcf_name (bool if_created_now) const
                }
 
                if (content_ratio && content_ratio != container()) {
-                       d += "-" + content_ratio->isdcf_name();
+                       /* This needs to be the numeric version of the ratio, and ::id() is close enough */
+                       d += "-" + content_ratio->id();
                }
        }
 
        if (!dm.audio_language.empty ()) {
                d += "_" + dm.audio_language;
-               if (!dm.subtitle_language.empty()) {
 
-                       bool burnt_in = true;
-                       BOOST_FOREACH (shared_ptr<Content> i, content ()) {
-                               if (!i->subtitle) {
-                                       continue;
-                               }
+               /* I'm not clear on the precise details of the convention for CCAP labelling;
+                  for now I'm just appending -CCAP if we have any closed captions.
+               */
 
-                               if (i->subtitle->use() && !i->subtitle->burn()) {
-                                       burnt_in = false;
+               optional<string> subtitle_language;
+               bool burnt_in = true;
+               bool ccap = false;
+               BOOST_FOREACH (shared_ptr<Content> i, content()) {
+                       BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
+                               if (j->type() == TEXT_OPEN_SUBTITLE && j->use()) {
+                                       subtitle_language = j->language ();
+                                       if (!j->burn()) {
+                                               burnt_in = false;
+                                       }
+                               } else if (j->type() == TEXT_CLOSED_CAPTION && j->use()) {
+                                       ccap = true;
                                }
                        }
+               }
 
-                       string language = dm.subtitle_language;
-                       if (burnt_in && language != "XX") {
-                               transform (language.begin(), language.end(), language.begin(), ::tolower);
+               if (dm.subtitle_language) {
+                       /* Subtitle language is overridden in ISDCF metadata, primarily to handle
+                          content with pre-burnt subtitles.
+                       */
+                       d += "-" + *dm.subtitle_language;
+                       if (ccap) {
+                               d += "-CCAP";
+                       }
+               } else if (subtitle_language) {
+                       /* Language is worked out from the content */
+                       if (burnt_in && *subtitle_language != "XX") {
+                               transform (subtitle_language->begin(), subtitle_language->end(), subtitle_language->begin(), ::tolower);
                        } else {
-                               transform (language.begin(), language.end(), language.begin(), ::toupper);
+                               transform (subtitle_language->begin(), subtitle_language->end(), subtitle_language->begin(), ::toupper);
                        }
 
-                       d += "-" + language;
+                       d += "-" + *subtitle_language;
+                       if (ccap) {
+                               d += "-CCAP";
+                       }
                } else {
+                       /* No subtitles */
                        d += "-XX";
                }
        }
@@ -720,27 +827,21 @@ Film::isdcf_name (bool if_created_now) const
 
        /* Count mapped audio channels */
 
-       int non_lfe = 0;
-       int lfe = 0;
+       list<int> mapped = mapped_audio_channels ();
 
-       BOOST_FOREACH (int i, mapped_audio_channels ()) {
-               if (i >= audio_channels()) {
-                       /* This channel is mapped but is not included in the DCP */
-                       continue;
-               }
-
-               if (static_cast<dcp::Channel> (i) == dcp::LFE) {
-                       ++lfe;
-               } else {
-                       ++non_lfe;
-               }
+       pair<int, int> ch = audio_channel_types (mapped, audio_channels());
+       if (!ch.first && !ch.second) {
+               d += "_MOS";
+       } else if (ch.first) {
+               d += String::compose("_%1%2", ch.first, ch.second);
        }
 
-       if (non_lfe) {
-               d += String::compose("_%1%2", non_lfe, lfe);
+       if (audio_channels() > static_cast<int>(dcp::HI) && find(mapped.begin(), mapped.end(), dcp::HI) != mapped.end()) {
+               d += "-HI";
+       }
+       if (audio_channels() > static_cast<int>(dcp::VI) && find(mapped.begin(), mapped.end(), dcp::VI) != mapped.end()) {
+               d += "-VI";
        }
-
-       /* XXX: HI/VI */
 
        d += "_" + resolution_to_string (_resolution);
 
@@ -771,7 +872,17 @@ Film::isdcf_name (bool if_created_now) const
        bool vf = false;
        BOOST_FOREACH (shared_ptr<Content> i, content ()) {
                shared_ptr<const DCPContent> dc = dynamic_pointer_cast<const DCPContent> (i);
-               if (dc && (dc->reference_video() || dc->reference_audio() || dc->reference_subtitle())) {
+               if (!dc) {
+                       continue;
+               }
+
+               bool any_text = false;
+               for (int i = 0; i < TEXT_COUNT; ++i) {
+                       if (dc->reference_text(static_cast<TextType>(i))) {
+                               any_text = true;
+                       }
+               }
+               if (dc->reference_video() || dc->reference_audio() || any_text) {
                        vf = true;
                }
        }
@@ -791,24 +902,10 @@ Film::dcp_name (bool if_created_now) const
 {
        string unfiltered;
        if (use_isdcf_name()) {
-               unfiltered = isdcf_name (if_created_now);
-       } else {
-               unfiltered = name ();
-       }
-
-       /* 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 careful_string_filter (isdcf_name (if_created_now));
        }
 
-       return filtered;
+       return careful_string_filter (name ());
 }
 
 void
@@ -821,133 +918,158 @@ Film::set_directory (boost::filesystem::path d)
 void
 Film::set_name (string n)
 {
+       ChangeSignaller<Film> ch (this, NAME);
        _name = n;
-       signal_changed (NAME);
 }
 
 void
 Film::set_use_isdcf_name (bool u)
 {
+       ChangeSignaller<Film> ch (this, USE_ISDCF_NAME);
        _use_isdcf_name = u;
-       signal_changed (USE_ISDCF_NAME);
 }
 
 void
 Film::set_dcp_content_type (DCPContentType const * t)
 {
+       ChangeSignaller<Film> ch (this, DCP_CONTENT_TYPE);
        _dcp_content_type = t;
-       signal_changed (DCP_CONTENT_TYPE);
 }
 
 void
 Film::set_container (Ratio const * c)
 {
+       ChangeSignaller<Film> ch (this, CONTAINER);
        _container = c;
-       signal_changed (CONTAINER);
 }
 
 void
 Film::set_resolution (Resolution r)
 {
+       ChangeSignaller<Film> ch (this, RESOLUTION);
        _resolution = r;
-       signal_changed (RESOLUTION);
 }
 
 void
 Film::set_j2k_bandwidth (int b)
 {
+       ChangeSignaller<Film> ch (this, J2K_BANDWIDTH);
        _j2k_bandwidth = b;
-       signal_changed (J2K_BANDWIDTH);
 }
 
 void
 Film::set_isdcf_metadata (ISDCFMetadata m)
 {
+       ChangeSignaller<Film> ch (this, ISDCF_METADATA);
        _isdcf_metadata = m;
-       signal_changed (ISDCF_METADATA);
 }
 
+/** @param f New frame rate.
+ *  @param user_explicit true if this comes from a direct user instruction, false if it is from
+ *  DCP-o-matic being helpful.
+ */
 void
-Film::set_video_frame_rate (int f)
+Film::set_video_frame_rate (int f, bool user_explicit)
 {
+       ChangeSignaller<Film> ch (this, VIDEO_FRAME_RATE);
        _video_frame_rate = f;
-       signal_changed (VIDEO_FRAME_RATE);
+       if (user_explicit) {
+               _user_explicit_video_frame_rate = true;
+       }
 }
 
 void
 Film::set_audio_channels (int c)
 {
+       ChangeSignaller<Film> ch (this, AUDIO_CHANNELS);
        _audio_channels = c;
-       signal_changed (AUDIO_CHANNELS);
 }
 
 void
 Film::set_three_d (bool t)
 {
+       ChangeSignaller<Film> ch (this, THREE_D);
        _three_d = t;
-       signal_changed (THREE_D);
 
        if (_three_d && _isdcf_metadata.two_d_version_of_three_d) {
+               ChangeSignaller<Film> ch (this, ISDCF_METADATA);
                _isdcf_metadata.two_d_version_of_three_d = false;
-               signal_changed (ISDCF_METADATA);
        }
 }
 
 void
 Film::set_interop (bool i)
 {
+       ChangeSignaller<Film> ch (this, INTEROP);
        _interop = i;
-       signal_changed (INTEROP);
 }
 
 void
 Film::set_audio_processor (AudioProcessor const * processor)
 {
+       ChangeSignaller<Film> ch1 (this, AUDIO_PROCESSOR);
+       ChangeSignaller<Film> ch2 (this, AUDIO_CHANNELS);
        _audio_processor = processor;
-       signal_changed (AUDIO_PROCESSOR);
-       signal_changed (AUDIO_CHANNELS);
 }
 
 void
 Film::set_reel_type (ReelType t)
 {
+       ChangeSignaller<Film> ch (this, REEL_TYPE);
        _reel_type = t;
-       signal_changed (REEL_TYPE);
 }
 
 /** @param r Desired reel length in bytes */
 void
 Film::set_reel_length (int64_t r)
 {
+       ChangeSignaller<Film> ch (this, REEL_LENGTH);
        _reel_length = r;
-       signal_changed (REEL_LENGTH);
 }
 
 void
 Film::set_upload_after_make_dcp (bool u)
 {
+       ChangeSignaller<Film> ch (this, UPLOAD_AFTER_MAKE_DCP);
        _upload_after_make_dcp = u;
-       signal_changed (UPLOAD_AFTER_MAKE_DCP);
 }
 
 void
-Film::signal_changed (Property p)
+Film::set_reencode_j2k (bool r)
 {
-       _dirty = true;
+       ChangeSignaller<Film> ch (this, REENCODE_J2K);
+       _reencode_j2k = r;
+}
 
-       switch (p) {
-       case Film::CONTENT:
-               set_video_frame_rate (_playlist->best_video_frame_rate ());
-               break;
-       case Film::VIDEO_FRAME_RATE:
-       case Film::SEQUENCE:
-               _playlist->maybe_sequence ();
-               break;
-       default:
-               break;
-       }
+void
+Film::signal_change (ChangeType type, int p)
+{
+       signal_change (type, static_cast<Property>(p));
+}
 
-       emit (boost::bind (boost::ref (Changed), p));
+void
+Film::signal_change (ChangeType type, Property p)
+{
+       if (type == CHANGE_TYPE_DONE) {
+               _dirty = true;
+
+               if (p == Film::CONTENT) {
+                       if (!_user_explicit_video_frame_rate) {
+                               set_video_frame_rate (best_video_frame_rate());
+                       }
+               }
+
+               emit (boost::bind (boost::ref (Change), type, p));
+
+               if (p == Film::VIDEO_FRAME_RATE || p == Film::SEQUENCE) {
+                       /* We want to call Playlist::maybe_sequence but this must happen after the
+                          main signal emission (since the butler will see that emission and un-suspend itself).
+                       */
+                       emit (boost::bind(&Playlist::maybe_sequence, _playlist.get(), shared_from_this()));
+               }
+       } else {
+               Change (type, p);
+       }
 }
 
 void
@@ -983,7 +1105,16 @@ Film::j2c_path (int reel, Frame frame, Eyes eyes, bool tmp) const
        return file (p);
 }
 
-/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs */
+static
+bool
+cpl_summary_compare (CPLSummary const & a, CPLSummary const & b)
+{
+       return a.last_write_time > b.last_write_time;
+}
+
+/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs.
+ *  The list will be returned in reverse order of timestamp (i.e. most recent first).
+ */
 vector<CPLSummary>
 Film::cpls () const
 {
@@ -1001,45 +1132,37 @@ Film::cpls () const
                        ) {
 
                        try {
-                               dcp::DCP dcp (*i);
-                               dcp.read ();
-                               DCPOMATIC_ASSERT (dcp.cpls().front()->file());
-                               out.push_back (
-                                       CPLSummary (
-                                               i->path().leaf().string(),
-                                               dcp.cpls().front()->id(),
-                                               dcp.cpls().front()->annotation_text(),
-                                               dcp.cpls().front()->file().get()
-                                               )
-                                       );
+                               out.push_back (CPLSummary(*i));
                        } catch (...) {
 
                        }
                }
        }
 
+       sort (out.begin(), out.end(), cpl_summary_compare);
+
        return out;
 }
 
 void
 Film::set_signed (bool s)
 {
+       ChangeSignaller<Film> ch (this, SIGNED);
        _signed = s;
-       signal_changed (SIGNED);
 }
 
 void
 Film::set_encrypted (bool e)
 {
+       ChangeSignaller<Film> ch (this, ENCRYPTED);
        _encrypted = e;
-       signal_changed (ENCRYPTED);
 }
 
 void
 Film::set_key (dcp::Key key)
 {
+       ChangeSignaller<Film> ch (this, KEY);
        _key = key;
-       signal_changed (KEY);
 }
 
 ContentList
@@ -1048,24 +1171,27 @@ Film::content () const
        return _playlist->content ();
 }
 
+/** @param content Content to add.
+ *  @param disable_audio_analysis true to never do automatic audio analysis, even if it is enabled in configuration.
+ */
 void
-Film::examine_and_add_content (shared_ptr<Content> c)
+Film::examine_and_add_content (shared_ptr<Content> content, bool disable_audio_analysis)
 {
-       if (dynamic_pointer_cast<FFmpegContent> (c) && _directory) {
-               run_ffprobe (c->path(0), file ("ffprobe.log"), _log);
+       if (dynamic_pointer_cast<FFmpegContent> (content) && _directory) {
+               run_ffprobe (content->path(0), file("ffprobe.log"));
        }
 
-       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
+       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), content));
 
        _job_connections.push_back (
-               j->Finished.connect (bind (&Film::maybe_add_content, this, weak_ptr<Job> (j), weak_ptr<Content> (c)))
+               j->Finished.connect (bind (&Film::maybe_add_content, this, weak_ptr<Job>(j), weak_ptr<Content>(content), disable_audio_analysis))
                );
 
        JobManager::instance()->add (j);
 }
 
 void
-Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c)
+Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c, bool disable_audio_analysis)
 {
        shared_ptr<Job> job = j.lock ();
        if (!job || !job->finished_ok ()) {
@@ -1079,12 +1205,12 @@ Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c)
 
        add_content (content);
 
-       if (Config::instance()->automatic_audio_analysis() && content->audio) {
+       if (Config::instance()->automatic_audio_analysis() && content->audio && !disable_audio_analysis) {
                shared_ptr<Playlist> playlist (new Playlist);
-               playlist->add (content);
+               playlist->add (shared_from_this(), content);
                boost::signals2::connection c;
                JobManager::instance()->analyse_audio (
-                       shared_from_this (), playlist, c, bind (&Film::audio_analysis_finished, this)
+                       shared_from_this(), playlist, false, c, bind (&Film::audio_analysis_finished, this)
                        );
                _audio_analysis_connections.push_back (c);
        }
@@ -1095,21 +1221,19 @@ Film::add_content (shared_ptr<Content> c)
 {
        /* Add {video,subtitle} content after any existing {video,subtitle} content */
        if (c->video) {
-               c->set_position (_playlist->video_end ());
-       } else if (c->subtitle) {
-               c->set_position (_playlist->subtitle_end ());
+               c->set_position (shared_from_this(), _playlist->video_end(shared_from_this()));
+       } else if (!c->text.empty()) {
+               c->set_position (shared_from_this(), _playlist->text_end(shared_from_this()));
        }
 
        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);
-                       }
+                       c->take_settings_from (i);
                }
        }
 
-       _playlist->add (c);
+       _playlist->add (shared_from_this(), c);
 }
 
 void
@@ -1121,26 +1245,33 @@ Film::remove_content (shared_ptr<Content> c)
 void
 Film::move_content_earlier (shared_ptr<Content> c)
 {
-       _playlist->move_earlier (c);
+       _playlist->move_earlier (shared_from_this(), c);
 }
 
 void
 Film::move_content_later (shared_ptr<Content> c)
 {
-       _playlist->move_later (c);
+       _playlist->move_later (shared_from_this(), c);
 }
 
-/** @return length of the film from time 0 to the last thing on the playlist */
+/** @return length of the film from time 0 to the last thing on the playlist,
+ *  with a minimum length of 1 second.
+ */
 DCPTime
 Film::length () const
 {
-       return _playlist->length ();
+       return max(DCPTime::from_seconds(1), _playlist->length(shared_from_this()).ceil(video_frame_rate()));
 }
 
 int
 Film::best_video_frame_rate () const
 {
-       return _playlist->best_video_frame_rate ();
+       /* Don't default to anything above 30fps (make the user select that explicitly) */
+       int best = _playlist->best_video_frame_rate ();
+       if (best > 30) {
+               best /= 2;
+       }
+       return best;
 }
 
 FrameRateChange
@@ -1150,50 +1281,96 @@ Film::active_frame_rate_change (DCPTime t) const
 }
 
 void
-Film::playlist_content_changed (weak_ptr<Content> c, int p, bool frequent)
+Film::playlist_content_change (ChangeType type, weak_ptr<Content> c, int p, bool frequent)
 {
-       _dirty = true;
-
        if (p == ContentProperty::VIDEO_FRAME_RATE) {
-               set_video_frame_rate (_playlist->best_video_frame_rate ());
+               signal_change (type, Film::CONTENT);
        } else if (p == AudioContentProperty::STREAMS) {
-               signal_changed (NAME);
+               signal_change (type, Film::NAME);
        }
 
-       emit (boost::bind (boost::ref (ContentChanged), c, p, frequent));
+       if (type == CHANGE_TYPE_DONE) {
+               emit (boost::bind (boost::ref (ContentChange), type, c, p, frequent));
+               if (!frequent) {
+                       check_settings_consistency ();
+               }
+       } else {
+               ContentChange (type, c, p, frequent);
+       }
 }
 
 void
-Film::playlist_changed ()
+Film::playlist_length_change ()
 {
-       signal_changed (CONTENT);
-       signal_changed (NAME);
+       LengthChange ();
 }
 
 void
-Film::playlist_order_changed ()
+Film::playlist_change (ChangeType type)
 {
-       signal_changed (CONTENT_ORDER);
+       signal_change (type, CONTENT);
+       signal_change (type, NAME);
+
+       if (type == CHANGE_TYPE_DONE) {
+               check_settings_consistency ();
+       }
 }
 
-int
-Film::audio_frame_rate () const
+/** Check for (and if necessary fix) impossible settings combinations, like
+ *  video set to being referenced when it can't be.
+ */
+void
+Film::check_settings_consistency ()
 {
-       BOOST_FOREACH (shared_ptr<Content> i, content ()) {
-               if (i->audio && i->audio->has_rate_above_48k ()) {
-                       return 96000;
+       bool change_made = false;
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
+               if (!d) {
+                       continue;
+               }
+
+               string why_not;
+               if (d->reference_video() && !d->can_reference_video(shared_from_this(), why_not)) {
+                       d->set_reference_video(false);
+                       change_made = true;
+               }
+               if (d->reference_audio() && !d->can_reference_audio(shared_from_this(), why_not)) {
+                       d->set_reference_audio(false);
+                       change_made = true;
                }
        }
 
+       if (change_made) {
+               Message (_("DCP-o-matic had to change your settings for referring to DCPs as OV.  Please review those settings to make sure they are what you want."));
+       }
+}
+
+void
+Film::playlist_order_changed ()
+{
+       /* XXX: missing PENDING */
+       signal_change (CHANGE_TYPE_DONE, CONTENT_ORDER);
+}
+
+int
+Film::audio_frame_rate () const
+{
+       /* It seems that nobody makes 96kHz DCPs at the moment, so let's avoid them.
+          See #1436.
+       */
        return 48000;
 }
 
 void
 Film::set_sequence (bool s)
 {
+       if (s == _sequence) {
+               return;
+       }
+
+       ChangeSignaller<Film> cc (this, SEQUENCE);
        _sequence = s;
        _playlist->set_sequence (s);
-       signal_changed (SEQUENCE);
 }
 
 /** @return Size of the largest possible image in whatever resolution we are using */
@@ -1218,19 +1395,32 @@ Film::frame_size () const
        return fit_ratio_within (container()->ratio(), full_frame ());
 }
 
-/** @param from KDM from time expressed as a local time with an offset from UTC
- *  @param to KDM to time expressed as a local time with an offset from UTC
+/** @param recipient KDM recipient certificate.
+ *  @param trusted_devices Certificate thumbprints of other trusted devices (can be empty).
+ *  @param cpl_file CPL filename.
+ *  @param from KDM from time expressed as a local time with an offset from UTC.
+ *  @param until KDM to time expressed as a local time with an offset from UTC.
+ *  @param formulation KDM formulation to use.
+ *  @param disable_forensic_marking_picture true to disable forensic marking of picture.
+ *  @param disable_forensic_marking_audio if not set, don't disable forensic marking of audio.  If set to 0,
+ *  disable all forensic marking; if set above 0, disable forensic marking above that channel.
  */
 dcp::EncryptedKDM
 Film::make_kdm (
        dcp::Certificate recipient,
-       vector<dcp::Certificate> trusted_devices,
+       vector<string> trusted_devices,
        boost::filesystem::path cpl_file,
        dcp::LocalTime from,
        dcp::LocalTime until,
-       dcp::Formulation formulation
+       dcp::Formulation formulation,
+       bool disable_forensic_marking_picture,
+       optional<int> disable_forensic_marking_audio
        ) const
 {
+       if (!_encrypted) {
+               throw runtime_error (_("Cannot make a KDM as this project is not encrypted."));
+       }
+
        shared_ptr<const dcp::CPL> cpl (new dcp::CPL (cpl_file));
        shared_ptr<const dcp::CertificateChain> signer = Config::instance()->signer_chain ();
        if (!signer->valid ()) {
@@ -1250,58 +1440,69 @@ Film::make_kdm (
 
        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()) {
+       BOOST_FOREACH(shared_ptr<const dcp::ReelMXF> i, cpl->reel_mxfs()) {
+               if (!i->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()) {
-                               keys[mxf] = j.key();
+                       if (j.id() == i->key_id().get()) {
+                               LOG_GENERAL ("Using imported key for %1", i->key_id().get());
+                               keys[i] = j.key();
                                done = true;
                        }
                }
 
                if (!done) {
                        /* No imported key; it must be an asset that we encrypted */
-                       keys[mxf] = key();
+                       LOG_GENERAL ("Using our own key for %1", i->key_id().get());
+                       keys[i] = key();
                }
        }
 
        return dcp::DecryptedKDM (
                cpl->id(), keys, from, until, cpl->content_title_text(), cpl->content_title_text(), dcp::LocalTime().as_string()
-               ).encrypt (signer, recipient, trusted_devices, formulation);
+               ).encrypt (signer, recipient, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio);
 }
 
-/** @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema.
- *  @param to KDM to time expressed as a local time in the time zone of the Screen's Cinema.
+/** @param screens Screens to make KDMs for.
+ *  @param cpl_file Path to CPL to make KDMs for.
+ *  @param from KDM from time expressed as a local time in the time zone of the Screen's Cinema.
+ *  @param until KDM to time expressed as a local time in the time zone of the Screen's Cinema.
+ *  @param formulation KDM formulation to use.
+ *  @param disable_forensic_marking_picture true to disable forensic marking of picture.
+ *  @param disable_forensic_marking_audio if not set, don't disable forensic marking of audio.  If set to 0,
+ *  disable all forensic marking; if set above 0, disable forensic marking above that channel.
  */
-list<ScreenKDM>
+list<shared_ptr<ScreenKDM> >
 Film::make_kdms (
        list<shared_ptr<Screen> > screens,
-       boost::filesystem::path dcp,
+       boost::filesystem::path cpl_file,
        boost::posix_time::ptime from,
        boost::posix_time::ptime until,
-       dcp::Formulation formulation
+       dcp::Formulation formulation,
+       bool disable_forensic_marking_picture,
+       optional<int> disable_forensic_marking_audio
        ) const
 {
-       list<ScreenKDM> kdms;
+       list<shared_ptr<ScreenKDM> > kdms;
 
        BOOST_FOREACH (shared_ptr<Screen> i, screens) {
                if (i->recipient) {
                        dcp::EncryptedKDM const kdm = make_kdm (
                                i->recipient.get(),
-                               i->trusted_devices,
-                               dcp,
-                               dcp::LocalTime (from, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
-                               dcp::LocalTime (until, i->cinema->utc_offset_hour(), i->cinema->utc_offset_minute()),
-                               formulation
+                               i->trusted_device_thumbprints(),
+                               cpl_file,
+                               dcp::LocalTime (from,  i->cinema ? i->cinema->utc_offset_hour() : 0, i->cinema ? i->cinema->utc_offset_minute() : 0),
+                               dcp::LocalTime (until, i->cinema ? i->cinema->utc_offset_hour() : 0, i->cinema ? i->cinema->utc_offset_minute() : 0),
+                               formulation,
+                               disable_forensic_marking_picture,
+                               disable_forensic_marking_audio
                                );
 
-                       kdms.push_back (ScreenKDM (i, kdm));
+                       kdms.push_back (shared_ptr<ScreenKDM>(new DCPScreenKDM(i, kdm)));
                }
        }
 
@@ -1314,13 +1515,13 @@ Film::make_kdms (
 uint64_t
 Film::required_disk_space () const
 {
-       return _playlist->required_disk_space (j2k_bandwidth(), audio_channels(), audio_frame_rate());
+       return _playlist->required_disk_space (shared_from_this(), j2k_bandwidth(), audio_channels(), audio_frame_rate());
 }
 
 /** This method checks the disk that the Film is on and tries to decide whether or not
  *  there will be enough space to make a DCP for it.  If so, true is returned; if not,
  *  false is returned and required and available are filled in with the amount of disk space
- *  required and available respectively (in Gb).
+ *  required and available respectively (in GB).
  *
  *  Note: the decision made by this method isn't, of course, 100% reliable.
  */
@@ -1357,10 +1558,9 @@ Film::subtitle_language () const
 {
        set<string> languages;
 
-       ContentList cl = content ();
-       BOOST_FOREACH (shared_ptr<Content>& c, cl) {
-               if (c->subtitle) {
-                       languages.insert (c->subtitle->language ());
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+               BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
+                       languages.insert (j->language ());
                }
        }
 
@@ -1376,55 +1576,6 @@ Film::subtitle_language () const
        return all;
 }
 
-/** Change the gains of the supplied AudioMapping to make it a default
- *  for this film.  The defaults are guessed based on what processor (if any)
- *  is in use, the number of input channels and any filename supplied.
- */
-void
-Film::make_audio_mapping_default (AudioMapping& mapping, optional<boost::filesystem::path> filename) const
-{
-       static string const regex[] = {
-               ".*[\\._-]L[\\._-].*",
-               ".*[\\._-]R[\\._-].*",
-               ".*[\\._-]C[\\._-].*",
-               ".*[\\._-]Lfe[\\._-].*",
-               ".*[\\._-]Ls[\\._-].*",
-               ".*[\\._-]Rs[\\._-].*"
-       };
-
-       static int const regexes = sizeof(regex) / sizeof(*regex);
-
-       if (audio_processor ()) {
-               audio_processor()->make_audio_mapping_default (mapping);
-       } else {
-               mapping.make_zero ();
-               if (mapping.input_channels() == 1) {
-                       bool guessed = false;
-
-                       /* See if we can guess where this stream should go */
-                       if (filename) {
-                               for (int i = 0; i < regexes; ++i) {
-                                       boost::regex e (regex[i], boost::regex::icase);
-                                       if (boost::regex_match (filename->string(), e) && i < mapping.output_channels()) {
-                                               mapping.set (0, i, 1);
-                                               guessed = true;
-                                       }
-                               }
-                       }
-
-                       if (!guessed) {
-                               /* If we have no idea, just put it on centre */
-                               mapping.set (0, static_cast<int> (dcp::CENTRE), 1);
-                       }
-               } else {
-                       /* 1:1 mapping */
-                       for (int i = 0; i < min (mapping.input_channels(), mapping.output_channels()); ++i) {
-                               mapping.set (i, i, 1);
-                       }
-               }
-       }
-}
-
 /** @return The names of the channels that audio contents' outputs are passed into;
  *  this is either the DCP or a AudioProcessor.
  */
@@ -1449,7 +1600,7 @@ Film::audio_output_names () const
 void
 Film::repeat_content (ContentList c, int n)
 {
-       _playlist->repeat (c, n);
+       _playlist->repeat (shared_from_this(), c, n);
 }
 
 void
@@ -1468,7 +1619,7 @@ list<DCPTimePeriod>
 Film::reels () const
 {
        list<DCPTimePeriod> p;
-       DCPTime const len = length().ceil (video_frame_rate ());
+       DCPTime const len = length();
 
        switch (reel_type ()) {
        case REELTYPE_SINGLE:
@@ -1476,37 +1627,43 @@ Film::reels () const
                break;
        case REELTYPE_BY_VIDEO_CONTENT:
        {
-               optional<DCPTime> last_split;
-               shared_ptr<Content> last_video;
-               BOOST_FOREACH (shared_ptr<Content> c, content ()) {
+               /* Collect all reel boundaries */
+               list<DCPTime> split_points;
+               split_points.push_back (DCPTime());
+               split_points.push_back (len);
+               BOOST_FOREACH (shared_ptr<Content> c, content()) {
                        if (c->video) {
-                               BOOST_FOREACH (DCPTime t, c->reel_split_points()) {
-                                       if (last_split) {
-                                               p.push_back (DCPTimePeriod (last_split.get(), t));
-                                       }
-                                       last_split = t;
+                               BOOST_FOREACH (DCPTime t, c->reel_split_points(shared_from_this())) {
+                                       split_points.push_back (t);
                                }
-                               last_video = c;
+                               split_points.push_back (c->end(shared_from_this()));
                        }
                }
 
-               DCPTime video_end = last_video ? last_video->end() : DCPTime(0);
-               if (last_split) {
-                       /* Definitely go from the last split to the end of the video content */
-                       p.push_back (DCPTimePeriod (last_split.get(), video_end));
-               }
-
-               if (video_end < len) {
-                       /* And maybe go after that as well if there is any non-video hanging over the end */
-                       p.push_back (DCPTimePeriod (video_end, len));
+               split_points.sort ();
+               split_points.unique ();
+
+               /* Make them into periods, coalescing any that are less than 1 second long */
+               optional<DCPTime> last;
+               BOOST_FOREACH (DCPTime t, split_points) {
+                       if (last && (t - *last) >= DCPTime::from_seconds(1)) {
+                               /* Period from *last to t is long enough; use it and start a new one */
+                               p.push_back (DCPTimePeriod(*last, t));
+                               last = t;
+                       } else if (!last) {
+                               /* That was the first time, so start a new period */
+                               last = t;
+                       }
                }
                break;
        }
        case REELTYPE_BY_LENGTH:
        {
                DCPTime current;
-               /* Integer-divide reel length by the size of one frame to give the number of frames per reel */
-               Frame const reel_in_frames = _reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8);
+               /* Integer-divide reel length by the size of one frame to give the number of frames per reel,
+                * making sure we don't go less than 1s long.
+                */
+               Frame const reel_in_frames = max(_reel_length / ((j2k_bandwidth() / video_frame_rate()) / 8), static_cast<Frame>(video_frame_rate()));
                while (current < len) {
                        DCPTime end = min (len, current + DCPTime::from_frames (reel_in_frames, video_frame_rate ()));
                        p.push_back (DCPTimePeriod (current, end));
@@ -1519,46 +1676,13 @@ Film::reels () const
        return p;
 }
 
+/** @param period A period within the DCP
+ *  @return Name of the content which most contributes to the given period.
+ */
 string
 Film::content_summary (DCPTimePeriod period) const
 {
-       return _playlist->content_summary (period);
-}
-
-list<string>
-Film::fix_conflicting_settings ()
-{
-       list<string> notes;
-
-       list<boost::filesystem::path> was_referencing;
-       BOOST_FOREACH (shared_ptr<Content> i, content()) {
-               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent> (i);
-               if (d) {
-                       list<string> reasons;
-                       bool was = false;
-                       if (!d->can_reference_video(reasons) && d->reference_video()) {
-                               d->set_reference_video (false);
-                               was = true;
-                       }
-                       if (!d->can_reference_audio(reasons) && d->reference_audio()) {
-                               d->set_reference_audio (false);
-                               was = true;
-                       }
-                       if (!d->can_reference_subtitle(reasons) && d->reference_subtitle()) {
-                               d->set_reference_subtitle (false);
-                               was = true;
-                       }
-                       if (was) {
-                               was_referencing.push_back (d->path(0).parent_path().filename());
-                       }
-               }
-       }
-
-       BOOST_FOREACH (boost::filesystem::path d, was_referencing) {
-               notes.push_back (String::compose (_("The DCP %1 was being referred to by this film.  This not now possible because the reel sizes in the film no longer agree with those in the imported DCP.\n\nSetting the 'Reel type' to 'split by video content' will probably help.\n\nAfter doing that you would need to re-tick the appropriate 'refer to existing DCP' checkboxes."), d.string()));
-       }
-
-       return notes;
+       return _playlist->content_summary (shared_from_this(), period);
 }
 
 void
@@ -1582,4 +1706,132 @@ Film::use_template (string name)
        _reel_type = _template_film->_reel_type;
        _reel_length = _template_film->_reel_length;
        _upload_after_make_dcp = _template_film->_upload_after_make_dcp;
+       _isdcf_metadata = _template_film->_isdcf_metadata;
+}
+
+pair<double, double>
+Film::speed_up_range (int dcp_frame_rate) const
+{
+       return _playlist->speed_up_range (dcp_frame_rate);
+}
+
+void
+Film::copy_from (shared_ptr<const Film> film)
+{
+       read_metadata (film->file (metadata_file));
+}
+
+bool
+Film::references_dcp_video () const
+{
+       BOOST_FOREACH (shared_ptr<Content> i, _playlist->content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
+               if (d && d->reference_video()) {
+                       return true;
+               }
+       }
+
+       return false;
+}
+
+bool
+Film::references_dcp_audio () const
+{
+       BOOST_FOREACH (shared_ptr<Content> i, _playlist->content()) {
+               shared_ptr<DCPContent> d = dynamic_pointer_cast<DCPContent>(i);
+               if (d && d->reference_audio()) {
+                       return true;
+               }
+       }
+
+       return false;
+}
+
+list<DCPTextTrack>
+Film::closed_caption_tracks () const
+{
+       list<DCPTextTrack> tt;
+       BOOST_FOREACH (shared_ptr<Content> i, content()) {
+               BOOST_FOREACH (shared_ptr<TextContent> j, i->text) {
+                       /* XXX: Empty DCPTextTrack ends up being a magic value here - the "unknown" or "not specified" track */
+                       DCPTextTrack dtt = j->dcp_track().get_value_or(DCPTextTrack());
+                       if (j->type() == TEXT_CLOSED_CAPTION && find(tt.begin(), tt.end(), dtt) == tt.end()) {
+                               tt.push_back (dtt);
+                       }
+               }
+       }
+
+       return tt;
+}
+
+void
+Film::set_marker (dcp::Marker type, DCPTime time)
+{
+       ChangeSignaller<Film> ch (this, MARKERS);
+       _markers[type] = time;
+}
+
+void
+Film::unset_marker (dcp::Marker type)
+{
+       ChangeSignaller<Film> ch (this, MARKERS);
+       _markers.erase (type);
+}
+
+void
+Film::set_ratings (vector<dcp::Rating> r)
+{
+       ChangeSignaller<Film> ch (this, RATINGS);
+       _ratings = r;
+}
+
+void
+Film::set_content_version (string v)
+{
+       ChangeSignaller<Film> ch (this, CONTENT_VERSION);
+       _content_version = v;
+}
+
+optional<DCPTime>
+Film::marker (dcp::Marker type) const
+{
+       map<dcp::Marker, DCPTime>::const_iterator i = _markers.find (type);
+       if (i == _markers.end()) {
+               return optional<DCPTime>();
+       }
+       return i->second;
+}
+
+shared_ptr<InfoFileHandle>
+Film::info_file_handle (DCPTimePeriod period, bool read) const
+{
+       return shared_ptr<InfoFileHandle> (new InfoFileHandle(_info_file_mutex, info_file(period), read));
+}
+
+InfoFileHandle::InfoFileHandle (boost::mutex& mutex, boost::filesystem::path file, bool read)
+       : _lock (mutex)
+       , _file (file)
+{
+       if (read) {
+               _handle = fopen_boost (file, "rb");
+               if (!_handle) {
+                       throw OpenFileError (file, errno, OpenFileError::READ);
+               }
+       } else {
+               bool const exists = boost::filesystem::exists (file);
+               if (exists) {
+                       _handle = fopen_boost (file, "r+b");
+               } else {
+                       _handle = fopen_boost (file, "wb");
+               }
+
+               if (!_handle) {
+                       throw OpenFileError (file, errno, exists ? OpenFileError::READ_WRITE : OpenFileError::WRITE);
+               }
+       }
+}
+
+InfoFileHandle::~InfoFileHandle ()
+{
+       fclose (_handle);
 }