Remove default DCP audio channel setting.
[dcpomatic.git] / src / lib / config.cc
index c37d20e1701e72410e35b9887d2324c8813ec73d..5401839e37743fbe88551e4b5386b852a17f3f6f 100644 (file)
 */
 
 
-#include "cinema.h"
+#include "cinema_list.h"
 #include "colour_conversion.h"
 #include "compose.hpp"
 #include "config.h"
 #include "constants.h"
 #include "cross.h"
 #include "dcp_content_type.h"
-#include "dkdm_recipient.h"
+#include "dkdm_recipient_list.h"
 #include "dkdm_wrapper.h"
 #include "film.h"
 #include "filter.h"
 #include "log.h"
 #include "ratio.h"
+#include "unzipper.h"
+#include "variant.h"
 #include "zipper.h"
 #include <dcp/certificate_chain.h>
 #include <dcp/name_format.h>
@@ -106,7 +108,8 @@ Config::set_defaults ()
        _default_still_length = 10;
        _default_dcp_content_type = DCPContentType::from_isdcf_name ("FTR");
        _default_dcp_audio_channels = 8;
-       _default_j2k_bandwidth = 150000000;
+       _default_video_bit_rate[VideoEncoding::JPEG2000] = 150000000;
+       _default_video_bit_rate[VideoEncoding::MPEG2] = 5000000;
        _default_audio_delay = 0;
        _default_interop = false;
        _default_metadata.clear ();
@@ -125,7 +128,8 @@ Config::set_defaults ()
        _notification_bcc = "";
        _check_for_updates = false;
        _check_for_test_updates = false;
-       _maximum_j2k_bandwidth = 250000000;
+       _maximum_video_bit_rate[VideoEncoding::JPEG2000] = 250000000;
+       _maximum_video_bit_rate[VideoEncoding::MPEG2] = 50000000;
        _log_types = LogEntry::TYPE_GENERAL | LogEntry::TYPE_WARNING | LogEntry::TYPE_ERROR | LogEntry::TYPE_DISK;
        _analyse_ebur128 = true;
        _automatic_audio_analysis = false;
@@ -135,8 +139,8 @@ Config::set_defaults ()
        /* At the moment we don't write these files anywhere new after a version change, so they will be read from
         * ~/.config/dcpomatic2 (or equivalent) and written back there.
         */
-       _cinemas_file = read_path ("cinemas.xml");
-       _dkdm_recipients_file = read_path ("dkdm_recipients.xml");
+       _cinemas_file = read_path("cinemas.sqlite3");
+       _dkdm_recipients_file = read_path("dkdm_recipients.sqlite3");
        _show_hints_before_make_dcp = true;
        _confirm_kdm_email = true;
        _kdm_container_name_format = dcp::NameFormat("KDM_%f_%c");
@@ -176,6 +180,8 @@ Config::set_defaults ()
        _gdc_username = optional<string>();
        _gdc_password = optional<string>();
        _player_mode = PLAYER_MODE_WINDOW;
+       _player_restricted_menus = false;
+       _playlist_editor_restricted_menus = false;
        _image_display = 0;
        _video_view_type = VIDEO_VIEW_SIMPLE;
        _respect_kdm_validity_periods = true;
@@ -196,6 +202,8 @@ Config::set_defaults ()
        _initial_paths["DebugLogPath"] = boost::none;
        _initial_paths["CinemaDatabasePath"] = boost::none;
        _initial_paths["ConfigFilePath"] = boost::none;
+       _initial_paths["Preferences"] = boost::none;
+       _initial_paths["SaveVerificationReport"] = boost::none;
        _use_isdcf_name_by_default = true;
        _write_kdms_to_disk = true;
        _email_kdms = false;
@@ -218,6 +226,10 @@ Config::set_defaults ()
        set_notification_email_to_default ();
        set_cover_sheet_to_default ();
 
+#ifdef DCPOMATIC_GROK
+       _grok = boost::none;
+#endif
+
        _main_divider_sash_position = {};
        _main_content_divider_sash_position = {};
 
@@ -264,7 +276,7 @@ Config::backup ()
                copy_file(path_to_copy, add_number(path_to_copy, n), ec);
        };
 
-       /* Make a backup copy of any config.xml, cinemas.xml, dkdm_recipients.xml that we might be about
+       /* Make a backup copy of any config.xml, cinemas.sqlite3, dkdm_recipients.sqlite3 that we might be about
         * to write over.  This is more intended for the situation where we have a corrupted config.xml,
         * and decide to overwrite it with a new one (possibly losing important details in the corrupted
         * file).  But we might as well back up the other files while we're about it.
@@ -284,15 +296,6 @@ Config::backup ()
 
 void
 Config::read ()
-{
-       read_config();
-       read_cinemas();
-       read_dkdm_recipients();
-}
-
-
-void
-Config::read_config()
 try
 {
        cxml::Document f ("Config");
@@ -365,7 +368,12 @@ try
        _dcp_j2k_comment = f.optional_string_child("DCPJ2KComment").get_value_or("");
 
        _default_still_length = f.optional_number_child<int>("DefaultStillLength").get_value_or (10);
-       _default_j2k_bandwidth = f.optional_number_child<int>("DefaultJ2KBandwidth").get_value_or (200000000);
+       if (auto j2k = f.optional_number_child<int>("DefaultJ2KBandwidth")) {
+               _default_video_bit_rate[VideoEncoding::JPEG2000] = *j2k;
+       } else {
+               _default_video_bit_rate[VideoEncoding::JPEG2000] = f.optional_number_child<int64_t>("DefaultJ2KVideoBitRate").get_value_or(200000000);
+       }
+       _default_video_bit_rate[VideoEncoding::MPEG2] = f.optional_number_child<int64_t>("DefaultMPEG2VideoBitRate").get_value_or(5000000);
        _default_audio_delay = f.optional_number_child<int>("DefaultAudioDelay").get_value_or (0);
        _default_interop = f.optional_bool_child("DefaultInterop").get_value_or (false);
 
@@ -389,11 +397,6 @@ try
 
        _default_kdm_directory = f.optional_string_child("DefaultKDMDirectory");
 
-       /* Read any cinemas that are still lying around in the config file
-        * from an old version.
-        */
-       read_cinemas (f);
-
        _mail_server = f.string_child ("MailServer");
        _mail_port = f.optional_number_child<int> ("MailPort").get_value_or (25);
 
@@ -424,7 +427,7 @@ try
        _kdm_bcc = f.optional_string_child ("KDMBCC").get_value_or ("");
        _kdm_email = f.string_child ("KDMEmail");
 
-       _notification_subject = f.optional_string_child("NotificationSubject").get_value_or(_("DCP-o-matic notification"));
+       _notification_subject = f.optional_string_child("NotificationSubject").get_value_or(variant::insert_dcpomatic(_("%1 notification")));
        _notification_from = f.optional_string_child("NotificationFrom").get_value_or("");
        _notification_to = f.optional_string_child("NotificationTo").get_value_or("");
        for (auto i: f.node_children("NotificationCC")) {
@@ -440,7 +443,12 @@ try
        _check_for_updates = f.optional_bool_child("CheckForUpdates").get_value_or (false);
        _check_for_test_updates = f.optional_bool_child("CheckForTestUpdates").get_value_or (false);
 
-       _maximum_j2k_bandwidth = f.optional_number_child<int> ("MaximumJ2KBandwidth").get_value_or (250000000);
+       if (auto j2k = f.optional_number_child<int>("MaximumJ2KBandwidth")) {
+               _maximum_video_bit_rate[VideoEncoding::JPEG2000] = *j2k;
+       } else {
+               _maximum_video_bit_rate[VideoEncoding::JPEG2000] = f.optional_number_child<int64_t>("MaximumJ2KVideoBitRate").get_value_or(250000000);
+       }
+       _maximum_video_bit_rate[VideoEncoding::MPEG2] = f.optional_number_child<int64_t>("MaximumMPEG2VideoBitRate").get_value_or(50000000);
        _allow_any_dcp_frame_rate = f.optional_bool_child ("AllowAnyDCPFrameRate").get_value_or (false);
        _allow_any_container = f.optional_bool_child ("AllowAnyContainer").get_value_or (false);
        _allow_96khz_audio = f.optional_bool_child("Allow96kHzAudio").get_value_or(false);
@@ -526,8 +534,8 @@ try
                        _dkdms->add (DKDMBase::read (i));
                }
        }
-       _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.xml").string());
-       _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.xml").string());
+       _cinemas_file = f.optional_string_child("CinemasFile").get_value_or(read_path("cinemas.sqlite3").string());
+       _dkdm_recipients_file = f.optional_string_child("DKDMRecipientsFile").get_value_or(read_path("dkdm_recipients.sqlite3").string());
        _show_hints_before_make_dcp = f.optional_bool_child("ShowHintsBeforeMakeDCP").get_value_or (true);
        _confirm_kdm_email = f.optional_bool_child("ConfirmKDMEmail").get_value_or (true);
        _kdm_container_name_format = dcp::NameFormat (f.optional_string_child("KDMContainerNameFormat").get_value_or ("KDM %f %c"));
@@ -586,6 +594,9 @@ try
                _player_mode = PLAYER_MODE_DUAL;
        }
 
+       _player_restricted_menus = f.optional_bool_child("PlayerRestrictedMenus").get_value_or(false);
+       _playlist_editor_restricted_menus = f.optional_bool_child("PlaylistEditorRestrictedMenus").get_value_or(false);
+
        _image_display = f.optional_number_child<int>("ImageDisplay").get_value_or(0);
        auto vc = f.optional_string_child("VideoViewType");
        if (vc && *vc == "opengl") {
@@ -640,6 +651,12 @@ try
        _allow_smpte_bv20 = f.optional_bool_child("AllowSMPTEBv20").get_value_or(false);
        _isdcf_name_part_length = f.optional_number_child<int>("ISDCFNamePartLength").get_value_or(14);
 
+#ifdef DCPOMATIC_GROK
+       if (auto grok = f.optional_node_child("Grok")) {
+               _grok = Grok(grok);
+       }
+#endif
+
        _export.read(f.optional_node_child("Export"));
 }
 catch (...) {
@@ -657,40 +674,6 @@ catch (...) {
 }
 
 
-void
-Config::read_cinemas()
-{
-       if (dcp::filesystem::exists(_cinemas_file)) {
-               try {
-                       cxml::Document f("Cinemas");
-                       f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
-                       read_cinemas(f);
-               } catch (...) {
-                       backup();
-                       FailedToLoad(LoadFailure::CINEMAS);
-                       write_cinemas();
-               }
-       }
-}
-
-
-void
-Config::read_dkdm_recipients()
-{
-       if (dcp::filesystem::exists(_dkdm_recipients_file)) {
-               try {
-                       cxml::Document f("DKDMRecipients");
-                       f.read_file(dcp::filesystem::fix_long_path(_dkdm_recipients_file));
-                       read_dkdm_recipients(f);
-               } catch (...) {
-                       backup();
-                       FailedToLoad(LoadFailure::DKDM_RECIPIENTS);
-                       write_dkdm_recipients();
-               }
-       }
-}
-
-
 /** @return Singleton instance */
 Config *
 Config::instance ()
@@ -698,6 +681,32 @@ Config::instance ()
        if (_instance == nullptr) {
                _instance = new Config;
                _instance->read ();
+
+               auto cinemas_file = _instance->cinemas_file();
+               if (cinemas_file.extension() == ".xml") {
+                       auto sqlite = cinemas_file;
+                       sqlite.replace_extension(".sqlite3");
+                       bool const had_sqlite = dcp::filesystem::exists(sqlite);
+
+                       _instance->set_cinemas_file(sqlite);
+
+                       if (dcp::filesystem::exists(cinemas_file) && !had_sqlite) {
+                               CinemaList cinemas;
+                               cinemas.read_legacy_file(cinemas_file);
+                       }
+               }
+
+               auto dkdm_recipients_file = _instance->dkdm_recipients_file();
+               if (dkdm_recipients_file.extension() == ".xml") {
+                       auto sqlite = dkdm_recipients_file;
+                       sqlite.replace_extension(".sqlite3");
+
+                       if (dcp::filesystem::exists(dkdm_recipients_file) && !dcp::filesystem::exists(sqlite)) {
+                               _instance->set_dkdm_recipients_file(sqlite);
+                               DKDMRecipientList recipients;
+                               recipients.read_legacy_file(dkdm_recipients_file);
+                       }
+               }
        }
 
        return _instance;
@@ -708,8 +717,6 @@ void
 Config::write () const
 {
        write_config ();
-       write_cinemas ();
-       write_dkdm_recipients ();
 }
 
 void
@@ -719,218 +726,214 @@ Config::write_config () const
        auto root = doc.create_root_node ("Config");
 
        /* [XML] Version The version number of the configuration file format. */
-       root->add_child("Version")->add_child_text (raw_convert<string>(_current_version));
+       cxml::add_text_child(root, "Version", raw_convert<string>(_current_version));
        /* [XML] MasterEncodingThreads Number of encoding threads to use when running as master. */
-       root->add_child("MasterEncodingThreads")->add_child_text (raw_convert<string> (_master_encoding_threads));
+       cxml::add_text_child(root, "MasterEncodingThreads", raw_convert<string>(_master_encoding_threads));
        /* [XML] ServerEncodingThreads Number of encoding threads to use when running as server. */
-       root->add_child("ServerEncodingThreads")->add_child_text (raw_convert<string> (_server_encoding_threads));
+       cxml::add_text_child(root, "ServerEncodingThreads", raw_convert<string>(_server_encoding_threads));
        if (_default_directory) {
                /* [XML:opt] DefaultDirectory Default directory when creating a new film in the GUI. */
-               root->add_child("DefaultDirectory")->add_child_text (_default_directory->string ());
+               cxml::add_text_child(root, "DefaultDirectory", _default_directory->string());
        }
        /* [XML] ServerPortBase Port number to use for frame encoding requests.  <code>ServerPortBase</code> + 1 and
           <code>ServerPortBase</code> + 2 are used for querying servers.  <code>ServerPortBase</code> + 3 is used
           by the batch converter to listen for job requests.
        */
-       root->add_child("ServerPortBase")->add_child_text (raw_convert<string> (_server_port_base));
+       cxml::add_text_child(root, "ServerPortBase", raw_convert<string>(_server_port_base));
        /* [XML] UseAnyServers 1 to broadcast to look for encoding servers to use, 0 to use only those configured. */
-       root->add_child("UseAnyServers")->add_child_text (_use_any_servers ? "1" : "0");
+       cxml::add_text_child(root, "UseAnyServers", _use_any_servers ? "1" : "0");
 
        for (auto i: _servers) {
                /* [XML:opt] Server IP address or hostname of an encoding server to use; you can use as many of these tags
                   as you like.
                */
-               root->add_child("Server")->add_child_text (i);
+               cxml::add_text_child(root, "Server", i);
        }
 
        /* [XML] OnlyServersEncode 1 to set the master to do decoding of source content no JPEG2000 encoding; all encoding
           is done by the encoding servers.  0 to set the master to do some encoding as well as coordinating the job.
        */
-       root->add_child("OnlyServersEncode")->add_child_text (_only_servers_encode ? "1" : "0");
+       cxml::add_text_child(root, "OnlyServersEncode", _only_servers_encode ? "1" : "0");
        /* [XML] TMSProtocol Protocol to use to copy files to a TMS; 0 to use SCP, 1 for FTP. */
-       root->add_child("TMSProtocol")->add_child_text (raw_convert<string> (static_cast<int> (_tms_protocol)));
+       cxml::add_text_child(root, "TMSProtocol", raw_convert<string>(static_cast<int>(_tms_protocol)));
        /* [XML] TMSPassive True to use PASV mode with TMS FTP connections. */
-       root->add_child("TMSPassive")->add_child_text(_tms_passive ? "1" : "0");
+       cxml::add_text_child(root, "TMSPassive", _tms_passive ? "1" : "0");
        /* [XML] TMSIP IP address of TMS. */
-       root->add_child("TMSIP")->add_child_text (_tms_ip);
+       cxml::add_text_child(root, "TMSIP", _tms_ip);
        /* [XML] TMSPath Path on the TMS to copy files to. */
-       root->add_child("TMSPath")->add_child_text (_tms_path);
+       cxml::add_text_child(root, "TMSPath", _tms_path);
        /* [XML] TMSUser Username to log into the TMS with. */
-       root->add_child("TMSUser")->add_child_text (_tms_user);
+       cxml::add_text_child(root, "TMSUser", _tms_user);
        /* [XML] TMSPassword Password to log into the TMS with. */
-       root->add_child("TMSPassword")->add_child_text (_tms_password);
+       cxml::add_text_child(root, "TMSPassword", _tms_password);
        if (_language) {
                /* [XML:opt] Language Language to use in the GUI e.g. <code>fr_FR</code>. */
-               root->add_child("Language")->add_child_text (_language.get());
+               cxml::add_text_child(root, "Language", _language.get());
        }
-       if (_default_dcp_content_type) {
-               /* [XML:opt] DefaultDCPContentType Default content type to use when creating new films (<code>FTR</code>, <code>SHR</code>,
-                  <code>TLR</code>, <code>TST</code>, <code>XSN</code>, <code>RTG</code>, <code>TSR</code>, <code>POL</code>,
-                  <code>PSA</code> or <code>ADV</code>). */
-               root->add_child("DefaultDCPContentType")->add_child_text (_default_dcp_content_type->isdcf_name ());
-       }
-       /* [XML] DefaultDCPAudioChannels Default number of audio channels to use when creating new films. */
-       root->add_child("DefaultDCPAudioChannels")->add_child_text (raw_convert<string> (_default_dcp_audio_channels));
        /* [XML] DCPIssuer Issuer text to write into CPL files. */
-       root->add_child("DCPIssuer")->add_child_text (_dcp_issuer);
+       cxml::add_text_child(root, "DCPIssuer", _dcp_issuer);
        /* [XML] DCPCreator Creator text to write into CPL files. */
-       root->add_child("DCPCreator")->add_child_text (_dcp_creator);
+       cxml::add_text_child(root, "DCPCreator", _dcp_creator);
        /* [XML] Company name to write into MXF files. */
-       root->add_child("DCPCompanyName")->add_child_text (_dcp_company_name);
+       cxml::add_text_child(root, "DCPCompanyName", _dcp_company_name);
        /* [XML] Product name to write into MXF files. */
-       root->add_child("DCPProductName")->add_child_text (_dcp_product_name);
+       cxml::add_text_child(root, "DCPProductName", _dcp_product_name);
        /* [XML] Product version to write into MXF files. */
-       root->add_child("DCPProductVersion")->add_child_text (_dcp_product_version);
+       cxml::add_text_child(root, "DCPProductVersion", _dcp_product_version);
        /* [XML] Comment to write into JPEG2000 data. */
-       root->add_child("DCPJ2KComment")->add_child_text (_dcp_j2k_comment);
+       cxml::add_text_child(root, "DCPJ2KComment", _dcp_j2k_comment);
        /* [XML] UploadAfterMakeDCP 1 to upload to a TMS after making a DCP, 0 for no upload. */
-       root->add_child("UploadAfterMakeDCP")->add_child_text (_upload_after_make_dcp ? "1" : "0");
+       cxml::add_text_child(root, "UploadAfterMakeDCP", _upload_after_make_dcp ? "1" : "0");
 
        /* [XML] DefaultStillLength Default length (in seconds) for still images in new films. */
-       root->add_child("DefaultStillLength")->add_child_text (raw_convert<string> (_default_still_length));
-       /* [XML] DefaultJ2KBandwidth Default bitrate (in bits per second) for JPEG2000 data in new films. */
-       root->add_child("DefaultJ2KBandwidth")->add_child_text (raw_convert<string> (_default_j2k_bandwidth));
+       cxml::add_text_child(root, "DefaultStillLength", raw_convert<string>(_default_still_length));
+       /* [XML] DefaultJ2KVideoBitRate Default bitrate (in bits per second) for JPEG2000 data in new films. */
+       cxml::add_text_child(root, "DefaultJ2KVideoBitRate", raw_convert<string>(_default_video_bit_rate[VideoEncoding::JPEG2000]));
+       /* [XML] DefaultMPEG2VideoBitRate Default bitrate (in bits per second) for MPEG2 data in new films. */
+       cxml::add_text_child(root, "DefaultMPEG2VideoBitRate", raw_convert<string>(_default_video_bit_rate[VideoEncoding::MPEG2]));
        /* [XML] DefaultAudioDelay Default delay to apply to audio (positive moves audio later) in milliseconds. */
-       root->add_child("DefaultAudioDelay")->add_child_text (raw_convert<string> (_default_audio_delay));
+       cxml::add_text_child(root, "DefaultAudioDelay", raw_convert<string>(_default_audio_delay));
        /* [XML] DefaultInterop 1 to default new films to Interop, 0 for SMPTE. */
-       root->add_child("DefaultInterop")->add_child_text (_default_interop ? "1" : "0");
+       cxml::add_text_child(root, "DefaultInterop", _default_interop ? "1" : "0");
        if (_default_audio_language) {
                /* [XML] DefaultAudioLanguage Default audio language to use for new films */
-               root->add_child("DefaultAudioLanguage")->add_child_text(_default_audio_language->to_string());
+               cxml::add_text_child(root, "DefaultAudioLanguage", _default_audio_language->to_string());
        }
        if (_default_territory) {
                /* [XML] DefaultTerritory Default territory to use for new films */
-               root->add_child("DefaultTerritory")->add_child_text(_default_territory->subtag());
+               cxml::add_text_child(root, "DefaultTerritory", _default_territory->subtag());
        }
        for (auto const& i: _default_metadata) {
-               auto c = root->add_child("DefaultMetadata");
+               auto c = cxml::add_child(root, "DefaultMetadata");
                c->set_attribute("key", i.first);
                c->add_child_text(i.second);
        }
        if (_default_kdm_directory) {
                /* [XML:opt] DefaultKDMDirectory Default directory to write KDMs to. */
-               root->add_child("DefaultKDMDirectory")->add_child_text (_default_kdm_directory->string ());
+               cxml::add_text_child(root, "DefaultKDMDirectory", _default_kdm_directory->string ());
        }
-       _default_kdm_duration.as_xml(root->add_child("DefaultKDMDuration"));
+       _default_kdm_duration.as_xml(cxml::add_child(root, "DefaultKDMDuration"));
        /* [XML] MailServer Hostname of SMTP server to use. */
-       root->add_child("MailServer")->add_child_text (_mail_server);
+       cxml::add_text_child(root, "MailServer", _mail_server);
        /* [XML] MailPort Port number to use on SMTP server. */
-       root->add_child("MailPort")->add_child_text (raw_convert<string> (_mail_port));
+       cxml::add_text_child(root, "MailPort", raw_convert<string>(_mail_port));
        /* [XML] MailProtocol Protocol to use on SMTP server (Auto, Plain, STARTTLS or SSL) */
        switch (_mail_protocol) {
        case EmailProtocol::AUTO:
-               root->add_child("MailProtocol")->add_child_text("Auto");
+               cxml::add_text_child(root, "MailProtocol", "Auto");
                break;
        case EmailProtocol::PLAIN:
-               root->add_child("MailProtocol")->add_child_text("Plain");
+               cxml::add_text_child(root, "MailProtocol", "Plain");
                break;
        case EmailProtocol::STARTTLS:
-               root->add_child("MailProtocol")->add_child_text("STARTTLS");
+               cxml::add_text_child(root, "MailProtocol", "STARTTLS");
                break;
        case EmailProtocol::SSL:
-               root->add_child("MailProtocol")->add_child_text("SSL");
+               cxml::add_text_child(root, "MailProtocol", "SSL");
                break;
        }
        /* [XML] MailUser Username to use on SMTP server. */
-       root->add_child("MailUser")->add_child_text (_mail_user);
+       cxml::add_text_child(root, "MailUser", _mail_user);
        /* [XML] MailPassword Password to use on SMTP server. */
-       root->add_child("MailPassword")->add_child_text (_mail_password);
+       cxml::add_text_child(root, "MailPassword", _mail_password);
 
        /* [XML] KDMSubject Subject to use for KDM emails. */
-       root->add_child("KDMSubject")->add_child_text (_kdm_subject);
+       cxml::add_text_child(root, "KDMSubject", _kdm_subject);
        /* [XML] KDMFrom From address to use for KDM emails. */
-       root->add_child("KDMFrom")->add_child_text (_kdm_from);
+       cxml::add_text_child(root, "KDMFrom", _kdm_from);
        for (auto i: _kdm_cc) {
                /* [XML] KDMCC CC address to use for KDM emails; you can use as many of these tags as you like. */
-               root->add_child("KDMCC")->add_child_text (i);
+               cxml::add_text_child(root, "KDMCC", i);
        }
        /* [XML] KDMBCC BCC address to use for KDM emails. */
-       root->add_child("KDMBCC")->add_child_text (_kdm_bcc);
+       cxml::add_text_child(root, "KDMBCC", _kdm_bcc);
        /* [XML] KDMEmail Text of KDM email. */
-       root->add_child("KDMEmail")->add_child_text (_kdm_email);
+       cxml::add_text_child(root, "KDMEmail", _kdm_email);
 
        /* [XML] NotificationSubject Subject to use for notification emails. */
-       root->add_child("NotificationSubject")->add_child_text (_notification_subject);
+       cxml::add_text_child(root, "NotificationSubject", _notification_subject);
        /* [XML] NotificationFrom From address to use for notification emails. */
-       root->add_child("NotificationFrom")->add_child_text (_notification_from);
+       cxml::add_text_child(root, "NotificationFrom", _notification_from);
        /* [XML] NotificationFrom To address to use for notification emails. */
-       root->add_child("NotificationTo")->add_child_text (_notification_to);
+       cxml::add_text_child(root, "NotificationTo", _notification_to);
        for (auto i: _notification_cc) {
                /* [XML] NotificationCC CC address to use for notification emails; you can use as many of these tags as you like. */
-               root->add_child("NotificationCC")->add_child_text (i);
+               cxml::add_text_child(root, "NotificationCC", i);
        }
        /* [XML] NotificationBCC BCC address to use for notification emails. */
-       root->add_child("NotificationBCC")->add_child_text (_notification_bcc);
+       cxml::add_text_child(root, "NotificationBCC", _notification_bcc);
        /* [XML] NotificationEmail Text of notification email. */
-       root->add_child("NotificationEmail")->add_child_text (_notification_email);
+       cxml::add_text_child(root, "NotificationEmail", _notification_email);
 
        /* [XML] CheckForUpdates 1 to check dcpomatic.com for new versions, 0 to check only on request. */
-       root->add_child("CheckForUpdates")->add_child_text (_check_for_updates ? "1" : "0");
+       cxml::add_text_child(root, "CheckForUpdates", _check_for_updates ? "1" : "0");
        /* [XML] CheckForUpdates 1 to check dcpomatic.com for new text versions, 0 to check only on request. */
-       root->add_child("CheckForTestUpdates")->add_child_text (_check_for_test_updates ? "1" : "0");
+       cxml::add_text_child(root, "CheckForTestUpdates", _check_for_test_updates ? "1" : "0");
 
-       /* [XML] MaximumJ2KBandwidth Maximum J2K bandwidth (in bits per second) that can be specified in the GUI. */
-       root->add_child("MaximumJ2KBandwidth")->add_child_text (raw_convert<string> (_maximum_j2k_bandwidth));
+       /* [XML] MaximumJ2KVideoBitRate Maximum video bit rate (in bits per second) that can be specified in the GUI for JPEG2000 encodes. */
+       cxml::add_text_child(root, "MaximumJ2KVideoBitRate", raw_convert<string>(_maximum_video_bit_rate[VideoEncoding::JPEG2000]));
+       /* [XML] MaximumMPEG2VideoBitRate Maximum video bit rate (in bits per second) that can be specified in the GUI for MPEG2 encodes. */
+       cxml::add_text_child(root, "MaximumMPEG2VideoBitRate", raw_convert<string>(_maximum_video_bit_rate[VideoEncoding::MPEG2]));
        /* [XML] AllowAnyDCPFrameRate 1 to allow users to specify any frame rate when creating DCPs, 0 to limit the GUI to standard rates. */
-       root->add_child("AllowAnyDCPFrameRate")->add_child_text (_allow_any_dcp_frame_rate ? "1" : "0");
+       cxml::add_text_child(root, "AllowAnyDCPFrameRate", _allow_any_dcp_frame_rate ? "1" : "0");
        /* [XML] AllowAnyContainer 1 to allow users to user any container ratio for their DCP, 0 to limit the GUI to DCI Flat/Scope */
-       root->add_child("AllowAnyContainer")->add_child_text (_allow_any_container ? "1" : "0");
+       cxml::add_text_child(root, "AllowAnyContainer", _allow_any_container ? "1" : "0");
        /* [XML] Allow96kHzAudio 1 to allow users to make DCPs with 96kHz audio, 0 to always make 48kHz DCPs */
-       root->add_child("Allow96kHzAudio")->add_child_text(_allow_96khz_audio ? "1" : "0");
+       cxml::add_text_child(root, "Allow96kHzAudio", _allow_96khz_audio ? "1" : "0");
        /* [XML] UseAllAudioChannels 1 to allow users to map audio to all 16 DCP channels, 0 to limit to the channels used in standard DCPs */
-       root->add_child("UseAllAudioChannels")->add_child_text(_use_all_audio_channels ? "1" : "0");
+       cxml::add_text_child(root, "UseAllAudioChannels", _use_all_audio_channels ? "1" : "0");
        /* [XML] ShowExperimentalAudioProcessors 1 to offer users the (experimental) audio upmixer processors, 0 to hide them */
-       root->add_child("ShowExperimentalAudioProcessors")->add_child_text (_show_experimental_audio_processors ? "1" : "0");
+       cxml::add_text_child(root, "ShowExperimentalAudioProcessors", _show_experimental_audio_processors ? "1" : "0");
        /* [XML] LogTypes Types of logging to write; a bitfield where 1 is general notes, 2 warnings, 4 errors, 8 debug information related
           to 3D, 16 debug information related to encoding, 32 debug information for timing purposes, 64 debug information related
           to sending email, 128 debug information related to the video view, 256 information about disk writing, 512 debug information
           related to the player, 1024 debug information related to audio analyses.
        */
-       root->add_child("LogTypes")->add_child_text (raw_convert<string> (_log_types));
+       cxml::add_text_child(root, "LogTypes", raw_convert<string> (_log_types));
        /* [XML] AnalyseEBUR128 1 to do EBUR128 analyses when analysing audio, otherwise 0. */
-       root->add_child("AnalyseEBUR128")->add_child_text (_analyse_ebur128 ? "1" : "0");
+       cxml::add_text_child(root, "AnalyseEBUR128", _analyse_ebur128 ? "1" : "0");
        /* [XML] AutomaticAudioAnalysis 1 to run audio analysis automatically when audio content is added to the film, otherwise 0. */
-       root->add_child("AutomaticAudioAnalysis")->add_child_text (_automatic_audio_analysis ? "1" : "0");
+       cxml::add_text_child(root, "AutomaticAudioAnalysis", _automatic_audio_analysis ? "1" : "0");
 #ifdef DCPOMATIC_WINDOWS
        if (_win32_console) {
                /* [XML] Win32Console 1 to open a console when running on Windows, otherwise 0.
                 * We only write this if it's true, which is a bit of a hack to allow unit tests to work
                 * more easily on Windows (without a platform-specific reference in config_write_utf8_test)
                 */
-               root->add_child("Win32Console")->add_child_text ("1");
+               cxml::add_text_child(root, "Win32Console", "1");
        }
 #endif
 
        /* [XML] Signer Certificate chain and private key to use when signing DCPs and KDMs.  Should contain <code>&lt;Certificate&gt;</code>
           tags in order and a <code>&lt;PrivateKey&gt;</code> tag all containing PEM-encoded certificates or private keys as appropriate.
        */
-       auto signer = root->add_child ("Signer");
+       auto signer = cxml::add_child(root, "Signer");
        DCPOMATIC_ASSERT (_signer_chain);
        for (auto const& i: _signer_chain->unordered()) {
-               signer->add_child("Certificate")->add_child_text (i.certificate (true));
+               cxml::add_text_child(signer, "Certificate", i.certificate (true));
        }
-       signer->add_child("PrivateKey")->add_child_text (_signer_chain->key().get ());
+       cxml::add_text_child(signer, "PrivateKey", _signer_chain->key().get ());
 
        /* [XML] Decryption Certificate chain and private key to use when decrypting KDMs */
-       auto decryption = root->add_child ("Decryption");
+       auto decryption = cxml::add_child(root, "Decryption");
        DCPOMATIC_ASSERT (_decryption_chain);
        for (auto const& i: _decryption_chain->unordered()) {
-               decryption->add_child("Certificate")->add_child_text (i.certificate (true));
+               cxml::add_text_child(decryption, "Certificate", i.certificate (true));
        }
-       decryption->add_child("PrivateKey")->add_child_text (_decryption_chain->key().get ());
+       cxml::add_text_child(decryption, "PrivateKey", _decryption_chain->key().get());
 
        /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the GUI; there can be more than one
           of these tags.
        */
        for (auto i: _history) {
-               root->add_child("History")->add_child_text (i.string ());
+               cxml::add_text_child(root, "History", i.string());
        }
 
        /* [XML] History Filename of DCP to present in the <guilabel>File</guilabel> menu of the player; there can be more than one
           of these tags.
        */
        for (auto i: _player_history) {
-               root->add_child("PlayerHistory")->add_child_text (i.string ());
+               cxml::add_text_child(root, "PlayerHistory", i.string());
        }
 
        /* [XML] DKDMGroup A group of DKDMs, each with a <code>Name</code> attribute, containing other <code>&lt;DKDMGroup&gt;</code>
@@ -940,53 +943,53 @@ Config::write_config () const
        _dkdms->as_xml (root);
 
        /* [XML] CinemasFile Filename of cinemas list file. */
-       root->add_child("CinemasFile")->add_child_text (_cinemas_file.string());
+       cxml::add_text_child(root, "CinemasFile", _cinemas_file.string());
        /* [XML] DKDMRecipientsFile Filename of DKDM recipients list file. */
-       root->add_child("DKDMRecipientsFile")->add_child_text (_dkdm_recipients_file.string());
+       cxml::add_text_child(root, "DKDMRecipientsFile", _dkdm_recipients_file.string());
        /* [XML] ShowHintsBeforeMakeDCP 1 to show hints in the GUI before making a DCP, otherwise 0. */
-       root->add_child("ShowHintsBeforeMakeDCP")->add_child_text (_show_hints_before_make_dcp ? "1" : "0");
+       cxml::add_text_child(root, "ShowHintsBeforeMakeDCP", _show_hints_before_make_dcp ? "1" : "0");
        /* [XML] ConfirmKDMEmail 1 to confirm before sending KDM emails in the GUI, otherwise 0. */
-       root->add_child("ConfirmKDMEmail")->add_child_text (_confirm_kdm_email ? "1" : "0");
+       cxml::add_text_child(root, "ConfirmKDMEmail", _confirm_kdm_email ? "1" : "0");
        /* [XML] KDMFilenameFormat Format for KDM filenames. */
-       root->add_child("KDMFilenameFormat")->add_child_text (_kdm_filename_format.specification ());
+       cxml::add_text_child(root, "KDMFilenameFormat", _kdm_filename_format.specification());
        /* [XML] KDMFilenameFormat Format for DKDM filenames. */
-       root->add_child("DKDMFilenameFormat")->add_child_text(_dkdm_filename_format.specification());
+       cxml::add_text_child(root, "DKDMFilenameFormat", _dkdm_filename_format.specification());
        /* [XML] KDMContainerNameFormat Format for KDM containers (directories or ZIP files). */
-       root->add_child("KDMContainerNameFormat")->add_child_text (_kdm_container_name_format.specification ());
+       cxml::add_text_child(root, "KDMContainerNameFormat", _kdm_container_name_format.specification());
        /* [XML] DCPMetadataFilenameFormat Format for DCP metadata filenames. */
-       root->add_child("DCPMetadataFilenameFormat")->add_child_text (_dcp_metadata_filename_format.specification ());
+       cxml::add_text_child(root, "DCPMetadataFilenameFormat", _dcp_metadata_filename_format.specification());
        /* [XML] DCPAssetFilenameFormat Format for DCP asset filenames. */
-       root->add_child("DCPAssetFilenameFormat")->add_child_text (_dcp_asset_filename_format.specification ());
+       cxml::add_text_child(root, "DCPAssetFilenameFormat", _dcp_asset_filename_format.specification());
        /* [XML] JumpToSelected 1 to make the GUI jump to the start of content when it is selected, otherwise 0. */
-       root->add_child("JumpToSelected")->add_child_text (_jump_to_selected ? "1" : "0");
+       cxml::add_text_child(root, "JumpToSelected", _jump_to_selected ? "1" : "0");
        /* [XML] Nagged 1 if a particular nag screen has been shown and should not be shown again, otherwise 0. */
        for (int i = 0; i < NAG_COUNT; ++i) {
-               xmlpp::Element* e = root->add_child ("Nagged");
+               auto e = cxml::add_child(root, "Nagged");
                e->set_attribute("id", raw_convert<string>(i));
                e->add_child_text (_nagged[i] ? "1" : "0");
        }
        /* [XML] PreviewSound 1 to use sound in the GUI preview and player, otherwise 0. */
-       root->add_child("PreviewSound")->add_child_text (_sound ? "1" : "0");
+       cxml::add_text_child(root, "PreviewSound", _sound ? "1" : "0");
        if (_sound_output) {
                /* [XML:opt] PreviewSoundOutput Name of the audio output to use. */
-               root->add_child("PreviewSoundOutput")->add_child_text (_sound_output.get());
+               cxml::add_text_child(root, "PreviewSoundOutput", _sound_output.get());
        }
        /* [XML] CoverSheet Text of the cover sheet to write when making DCPs. */
-       root->add_child("CoverSheet")->add_child_text (_cover_sheet);
+       cxml::add_text_child(root, "CoverSheet", _cover_sheet);
        if (_last_player_load_directory) {
-               root->add_child("LastPlayerLoadDirectory")->add_child_text(_last_player_load_directory->string());
+               cxml::add_text_child(root, "LastPlayerLoadDirectory", _last_player_load_directory->string());
        }
        /* [XML] LastKDMWriteType Last type of KDM-write: <code>flat</code> for a flat file, <code>folder</code> for a folder or <code>zip</code> for a ZIP file. */
        if (_last_kdm_write_type) {
                switch (_last_kdm_write_type.get()) {
                case KDM_WRITE_FLAT:
-                       root->add_child("LastKDMWriteType")->add_child_text("flat");
+                       cxml::add_text_child(root, "LastKDMWriteType", "flat");
                        break;
                case KDM_WRITE_FOLDER:
-                       root->add_child("LastKDMWriteType")->add_child_text("folder");
+                       cxml::add_text_child(root, "LastKDMWriteType", "folder");
                        break;
                case KDM_WRITE_ZIP:
-                       root->add_child("LastKDMWriteType")->add_child_text("zip");
+                       cxml::add_text_child(root, "LastKDMWriteType", "zip");
                        break;
                }
        }
@@ -994,58 +997,58 @@ Config::write_config () const
        if (_last_dkdm_write_type) {
                switch (_last_dkdm_write_type.get()) {
                case DKDM_WRITE_INTERNAL:
-                       root->add_child("LastDKDMWriteType")->add_child_text("internal");
+                       cxml::add_text_child(root, "LastDKDMWriteType", "internal");
                        break;
                case DKDM_WRITE_FILE:
-                       root->add_child("LastDKDMWriteType")->add_child_text("file");
+                       cxml::add_text_child(root, "LastDKDMWriteType", "file");
                        break;
                }
        }
        /* [XML] FramesInMemoryMultiplier value to multiply the encoding threads count by to get the maximum number of
           frames to be held in memory at once.
        */
-       root->add_child("FramesInMemoryMultiplier")->add_child_text(raw_convert<string>(_frames_in_memory_multiplier));
+       cxml::add_text_child(root, "FramesInMemoryMultiplier", raw_convert<string>(_frames_in_memory_multiplier));
 
        /* [XML] DecodeReduction power of 2 to reduce DCP images by before decoding in the player. */
        if (_decode_reduction) {
-               root->add_child("DecodeReduction")->add_child_text(raw_convert<string>(_decode_reduction.get()));
+               cxml::add_text_child(root, "DecodeReduction", raw_convert<string>(_decode_reduction.get()));
        }
 
        /* [XML] DefaultNotify 1 to default jobs to notify when complete, otherwise 0. */
-       root->add_child("DefaultNotify")->add_child_text(_default_notify ? "1" : "0");
+       cxml::add_text_child(root, "DefaultNotify", _default_notify ? "1" : "0");
 
        /* [XML] Notification 1 if a notification type is enabled, otherwise 0. */
        for (int i = 0; i < NOTIFICATION_COUNT; ++i) {
-               xmlpp::Element* e = root->add_child ("Notification");
-               e->set_attribute("id", raw_convert<string>(i));
+               auto e = cxml::add_child(root, "Notification");
+               e->set_attribute ("id", raw_convert<string>(i));
                e->add_child_text (_notification[i] ? "1" : "0");
        }
 
        if (_barco_username) {
                /* [XML] BarcoUsername Username for logging into Barco's servers when downloading server certificates. */
-               root->add_child("BarcoUsername")->add_child_text(*_barco_username);
+               cxml::add_text_child(root, "BarcoUsername", *_barco_username);
        }
        if (_barco_password) {
                /* [XML] BarcoPassword Password for logging into Barco's servers when downloading server certificates. */
-               root->add_child("BarcoPassword")->add_child_text(*_barco_password);
+               cxml::add_text_child(root, "BarcoPassword", *_barco_password);
        }
 
        if (_christie_username) {
                /* [XML] ChristieUsername Username for logging into Christie's servers when downloading server certificates. */
-               root->add_child("ChristieUsername")->add_child_text(*_christie_username);
+               cxml::add_text_child(root, "ChristieUsername", *_christie_username);
        }
        if (_christie_password) {
                /* [XML] ChristiePassword Password for logging into Christie's servers when downloading server certificates. */
-               root->add_child("ChristiePassword")->add_child_text(*_christie_password);
+               cxml::add_text_child(root, "ChristiePassword", *_christie_password);
        }
 
        if (_gdc_username) {
                /* [XML] GDCUsername Username for logging into GDC's servers when downloading server certificates. */
-               root->add_child("GDCUsername")->add_child_text(*_gdc_username);
+               cxml::add_text_child(root, "GDCUsername", *_gdc_username);
        }
        if (_gdc_password) {
                /* [XML] GDCPassword Password for logging into GDC's servers when downloading server certificates. */
-               root->add_child("GDCPassword")->add_child_text(*_gdc_password);
+               cxml::add_text_child(root, "GDCPassword", *_gdc_password);
        }
 
        /* [XML] PlayerMode <code>window</code> for a single window, <code>full</code> for full-screen and <code>dual</code> for full screen playback
@@ -1053,80 +1056,94 @@ Config::write_config () const
        */
        switch (_player_mode) {
        case PLAYER_MODE_WINDOW:
-               root->add_child("PlayerMode")->add_child_text("window");
+               cxml::add_text_child(root, "PlayerMode", "window");
                break;
        case PLAYER_MODE_FULL:
-               root->add_child("PlayerMode")->add_child_text("full");
+               cxml::add_text_child(root, "PlayerMode", "full");
                break;
        case PLAYER_MODE_DUAL:
-               root->add_child("PlayerMode")->add_child_text("dual");
+               cxml::add_text_child(root, "PlayerMode", "dual");
                break;
        }
 
+       if (_player_restricted_menus) {
+               cxml::add_text_child(root, "PlayerRestrictedMenus", "1");
+       }
+
+       if (_playlist_editor_restricted_menus) {
+               cxml::add_text_child(root, "PlaylistEditorRestrictedMenus", "1");
+       }
+
        /* [XML] ImageDisplay Screen number to put image on in dual-screen player mode. */
-       root->add_child("ImageDisplay")->add_child_text(raw_convert<string>(_image_display));
+       cxml::add_text_child(root, "ImageDisplay", raw_convert<string>(_image_display));
        switch (_video_view_type) {
        case VIDEO_VIEW_SIMPLE:
-               root->add_child("VideoViewType")->add_child_text("simple");
+               cxml::add_text_child(root, "VideoViewType", "simple");
                break;
        case VIDEO_VIEW_OPENGL:
-               root->add_child("VideoViewType")->add_child_text("opengl");
+               cxml::add_text_child(root, "VideoViewType", "opengl");
                break;
        }
        /* [XML] RespectKDMValidityPeriods 1 to refuse to use KDMs that are out of date, 0 to ignore KDM dates. */
-       root->add_child("RespectKDMValidityPeriods")->add_child_text(_respect_kdm_validity_periods ? "1" : "0");
+       cxml::add_text_child(root, "RespectKDMValidityPeriods", _respect_kdm_validity_periods ? "1" : "0");
        if (_player_debug_log_file) {
                /* [XML] PlayerLogFile Filename to use for player debug logs. */
-               root->add_child("PlayerDebugLogFile")->add_child_text(_player_debug_log_file->string());
+               cxml::add_text_child(root, "PlayerDebugLogFile", _player_debug_log_file->string());
        }
        if (_player_content_directory) {
                /* [XML] PlayerContentDirectory Directory to use for player content in the dual-screen mode. */
-               root->add_child("PlayerContentDirectory")->add_child_text(_player_content_directory->string());
+               cxml::add_text_child(root, "PlayerContentDirectory", _player_content_directory->string());
        }
        if (_player_playlist_directory) {
                /* [XML] PlayerPlaylistDirectory Directory to use for player playlists in the dual-screen mode. */
-               root->add_child("PlayerPlaylistDirectory")->add_child_text(_player_playlist_directory->string());
+               cxml::add_text_child(root, "PlayerPlaylistDirectory", _player_playlist_directory->string());
        }
        if (_player_kdm_directory) {
                /* [XML] PlayerKDMDirectory Directory to use for player KDMs in the dual-screen mode. */
-               root->add_child("PlayerKDMDirectory")->add_child_text(_player_kdm_directory->string());
+               cxml::add_text_child(root, "PlayerKDMDirectory", _player_kdm_directory->string());
        }
        if (_audio_mapping) {
-               _audio_mapping->as_xml (root->add_child("AudioMapping"));
+               _audio_mapping->as_xml(cxml::add_child(root, "AudioMapping"));
        }
        for (auto const& i: _custom_languages) {
-               root->add_child("CustomLanguage")->add_child_text(i.to_string());
+               cxml::add_text_child(root, "CustomLanguage", i.to_string());
        }
        for (auto const& initial: _initial_paths) {
                if (initial.second) {
-                       root->add_child(initial.first)->add_child_text(initial.second->string());
+                       cxml::add_text_child(root, initial.first, initial.second->string());
                }
        }
-       root->add_child("UseISDCFNameByDefault")->add_child_text(_use_isdcf_name_by_default ? "1" : "0");
-       root->add_child("WriteKDMsToDisk")->add_child_text(_write_kdms_to_disk ? "1" : "0");
-       root->add_child("EmailKDMs")->add_child_text(_email_kdms ? "1" : "0");
-       root->add_child("DefaultKDMType")->add_child_text(dcp::formulation_to_string(_default_kdm_type));
-       root->add_child("AutoCropThreshold")->add_child_text(raw_convert<string>(_auto_crop_threshold));
+       cxml::add_text_child(root, "UseISDCFNameByDefault", _use_isdcf_name_by_default ? "1" : "0");
+       cxml::add_text_child(root, "WriteKDMsToDisk", _write_kdms_to_disk ? "1" : "0");
+       cxml::add_text_child(root, "EmailKDMs", _email_kdms ? "1" : "0");
+       cxml::add_text_child(root, "DefaultKDMType", dcp::formulation_to_string(_default_kdm_type));
+       cxml::add_text_child(root, "AutoCropThreshold", raw_convert<string>(_auto_crop_threshold));
        if (_last_release_notes_version) {
-               root->add_child("LastReleaseNotesVersion")->add_child_text(*_last_release_notes_version);
+               cxml::add_text_child(root, "LastReleaseNotesVersion", *_last_release_notes_version);
        }
        if (_main_divider_sash_position) {
-               root->add_child("MainDividerSashPosition")->add_child_text(raw_convert<string>(*_main_divider_sash_position));
+               cxml::add_text_child(root, "MainDividerSashPosition", raw_convert<string>(*_main_divider_sash_position));
        }
        if (_main_content_divider_sash_position) {
-               root->add_child("MainContentDividerSashPosition")->add_child_text(raw_convert<string>(*_main_content_divider_sash_position));
+               cxml::add_text_child(root, "MainContentDividerSashPosition", raw_convert<string>(*_main_content_divider_sash_position));
        }
 
-       root->add_child("DefaultAddFileLocation")->add_child_text(
+       cxml::add_text_child(root, "DefaultAddFileLocation",
                _default_add_file_location == DefaultAddFileLocation::SAME_AS_LAST_TIME ? "last" : "project"
                );
 
        /* [XML] AllowSMPTEBv20 1 to allow the user to choose SMPTE (Bv2.0 only) as a standard, otherwise 0 */
-       root->add_child("AllowSMPTEBv20")->add_child_text(_allow_smpte_bv20 ? "1" : "0");
+       cxml::add_text_child(root, "AllowSMPTEBv20", _allow_smpte_bv20 ? "1" : "0");
        /* [XML] ISDCFNamePartLength Maximum length of the "name" part of an ISDCF name, which should be 14 according to the standard */
-       root->add_child("ISDCFNamePartLength")->add_child_text(raw_convert<string>(_isdcf_name_part_length));
+       cxml::add_text_child(root, "ISDCFNamePartLength", raw_convert<string>(_isdcf_name_part_length));
+
+#ifdef DCPOMATIC_GROK
+       if (_grok) {
+               _grok->as_xml(cxml::add_child(root, "Grok"));
+       }
+#endif
 
-       _export.write(root->add_child("Export"));
+       _export.write(cxml::add_child(root, "Export"));
 
        auto target = config_write_file();
 
@@ -1155,10 +1172,10 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t
 {
        xmlpp::Document doc;
        auto root = doc.create_root_node (root_node);
-       root->add_child("Version")->add_child_text(version);
+       cxml::add_text_child(root, "Version", version);
 
        for (auto i: things) {
-               i->as_xml (root->add_child(node));
+               i->as_xml(cxml::add_child(root, node));
        }
 
        try {
@@ -1173,20 +1190,6 @@ write_file (string root_node, string node, string version, list<shared_ptr<T>> t
 }
 
 
-void
-Config::write_cinemas () const
-{
-       write_file ("Cinemas", "Cinema", "1", _cinemas, _cinemas_file);
-}
-
-
-void
-Config::write_dkdm_recipients () const
-{
-       write_file ("DKDMRecipients", "DKDMRecipient", "1", _dkdm_recipients, _dkdm_recipients_file);
-}
-
-
 boost::filesystem::path
 Config::default_directory_or (boost::filesystem::path a) const
 {
@@ -1219,7 +1222,7 @@ void
 Config::drop ()
 {
        delete _instance;
-       _instance = 0;
+       _instance = nullptr;
 }
 
 void
@@ -1233,20 +1236,20 @@ Config::set_kdm_email_to_default ()
 {
        _kdm_subject = _("KDM delivery: $CPL_NAME");
 
-       _kdm_email = _(
+       _kdm_email = variant::insert_dcpomatic(_(
                "Dear Projectionist\n\n"
                "Please find attached KDMs for $CPL_NAME.\n\n"
                "Cinema: $CINEMA_NAME\n"
                "Screen(s): $SCREENS\n\n"
                "The KDMs are valid from $START_TIME until $END_TIME.\n\n"
-               "Best regards,\nDCP-o-matic"
-               );
+               "Best regards,\n%1"
+               ));
 }
 
 void
 Config::set_notification_email_to_default ()
 {
-       _notification_subject = _("DCP-o-matic notification");
+       _notification_subject = variant::insert_dcpomatic(_("%1 notification"));
 
        _notification_email = _(
                "$JOB_NAME: $JOB_STATUS"
@@ -1317,7 +1320,7 @@ Config::add_to_history_internal (vector<boost::filesystem::path>& h, boost::file
 
        h.insert (h.begin (), p);
        if (h.size() > HISTORY_SIZE) {
-               h.pop_back ();
+               h.resize(HISTORY_SIZE);
        }
 
        changed (HISTORY);
@@ -1347,20 +1350,6 @@ Config::have_existing (string file)
 }
 
 
-void
-Config::read_cinemas (cxml::Document const & f)
-{
-       _cinemas.clear ();
-       for (auto i: f.node_children("Cinema")) {
-               /* Slightly grotty two-part construction of Cinema here so that we can use
-                  shared_from_this.
-               */
-               auto cinema = make_shared<Cinema>(i);
-               cinema->read_screens (i);
-               _cinemas.push_back (cinema);
-       }
-}
-
 void
 Config::set_cinemas_file (boost::filesystem::path file)
 {
@@ -1370,25 +1359,27 @@ Config::set_cinemas_file (boost::filesystem::path file)
 
        _cinemas_file = file;
 
-       if (dcp::filesystem::exists(_cinemas_file)) {
-               /* Existing file; read it in */
-               cxml::Document f ("Cinemas");
-               f.read_file(dcp::filesystem::fix_long_path(_cinemas_file));
-               read_cinemas (f);
-       }
-
-       changed (CINEMAS);
        changed (OTHER);
 }
 
 
 void
-Config::read_dkdm_recipients (cxml::Document const & f)
+Config::set_dkdm_recipients_file(boost::filesystem::path file)
 {
-       _dkdm_recipients.clear ();
-       for (auto i: f.node_children("DKDMRecipient")) {
-               _dkdm_recipients.push_back (make_shared<DKDMRecipient>(i));
+       if (file == _dkdm_recipients_file) {
+               return;
        }
+
+       _dkdm_recipients_file = file;
+
+       changed(OTHER);
+}
+
+
+void
+Config::save_default_template(shared_ptr<const Film> film) const
+{
+       film->write_template(write_path("default.xml"));
 }
 
 
@@ -1399,14 +1390,14 @@ Config::save_template (shared_ptr<const Film> film, string name) const
 }
 
 
-list<string>
+vector<string>
 Config::templates () const
 {
        if (!dcp::filesystem::exists(read_path("templates"))) {
                return {};
        }
 
-       list<string> n;
+       vector<string> n;
        for (auto const& i: dcp::filesystem::directory_iterator(read_path("templates"))) {
                n.push_back (i.path().filename().string());
        }
@@ -1427,6 +1418,18 @@ Config::template_read_path (string name) const
 }
 
 
+boost::filesystem::path
+Config::default_template_read_path() const
+{
+       if (!boost::filesystem::exists(read_path("default.xml"))) {
+               auto film = std::make_shared<const Film>(optional<boost::filesystem::path>());
+               save_default_template(film);
+       }
+
+       return read_path("default.xml");
+}
+
+
 boost::filesystem::path
 Config::template_write_path (string name) const
 {
@@ -1498,7 +1501,7 @@ void
 Config::link (boost::filesystem::path new_file) const
 {
        xmlpp::Document doc;
-       doc.create_root_node("Config")->add_child("Link")->add_child_text(new_file.string());
+       cxml::add_text_child(doc.create_root_node("Config"), "Link", new_file.string());
        try {
                doc.write_to_file_formatted(write_path("config.xml").string());
        } catch (xmlpp::exception& e) {
@@ -1621,16 +1624,84 @@ save_all_config_as_zip (boost::filesystem::path zip_file)
        auto config = Config::instance();
        zipper.add ("config.xml", dcp::file_to_string(config->config_read_file()));
        if (dcp::filesystem::exists(config->cinemas_file())) {
-               zipper.add ("cinemas.xml", dcp::file_to_string(config->cinemas_file()));
+               zipper.add("cinemas.sqlite3", dcp::file_to_string(config->cinemas_file()));
        }
        if (dcp::filesystem::exists(config->dkdm_recipients_file())) {
-               zipper.add ("dkdm_recipients.xml", dcp::file_to_string(config->dkdm_recipients_file()));
+               zipper.add("dkdm_recipients.sqlite3", dcp::file_to_string(config->dkdm_recipients_file()));
        }
 
        zipper.close ();
 }
 
 
+void
+Config::load_from_zip(boost::filesystem::path zip_file, CinemasAction action)
+{
+       backup();
+
+       auto const current_cinemas = cinemas_file();
+       /* This is (unfortunately) a full path, and the user can't change it, so
+        * we always want to use that same path in the future no matter what is in the
+        * config.xml that we are about to load.
+        */
+       auto const current_dkdm_recipients = dkdm_recipients_file();
+
+       Unzipper unzipper(zip_file);
+       dcp::write_string_to_file(unzipper.get("config.xml"), config_write_file());
+
+       if (action == CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) {
+               /* Read the zipped config, so that the cinemas file path is the new one and
+                * we write the cinemas to it.
+                */
+               read();
+               boost::filesystem::create_directories(cinemas_file().parent_path());
+               set_dkdm_recipients_file(current_dkdm_recipients);
+       }
+
+       if (unzipper.contains("cinemas.xml") && action != CinemasAction::IGNORE) {
+               CinemaList cinemas;
+               cinemas.clear();
+               cinemas.read_legacy_string(unzipper.get("cinemas.xml"));
+       }
+
+       if (unzipper.contains("dkdm_recipients.xml")) {
+               DKDMRecipientList recipients;
+               recipients.clear();
+               recipients.read_legacy_string(unzipper.get("dkdm_recipients.xml"));
+       }
+
+       if (unzipper.contains("cinemas.sqlite3") && action != CinemasAction::IGNORE) {
+               dcp::write_string_to_file(unzipper.get("cinemas.sqlite3"), cinemas_file());
+       }
+
+       if (unzipper.contains("dkdm_recipients.sqlite3")) {
+               dcp::write_string_to_file(unzipper.get("dkdm_recipients.sqlite3"), dkdm_recipients_file());
+       }
+
+       if (action != CinemasAction::WRITE_TO_PATH_IN_ZIPPED_CONFIG) {
+               /* Read the zipped config, then reset the cinemas file to be the old one */
+               read();
+               set_cinemas_file(current_cinemas);
+               set_dkdm_recipients_file(current_dkdm_recipients);
+       }
+
+       changed(Property::USE_ANY_SERVERS);
+       changed(Property::SERVERS);
+       changed(Property::SOUND);
+       changed(Property::SOUND_OUTPUT);
+       changed(Property::PLAYER_CONTENT_DIRECTORY);
+       changed(Property::PLAYER_PLAYLIST_DIRECTORY);
+       changed(Property::PLAYER_DEBUG_LOG);
+       changed(Property::HISTORY);
+       changed(Property::SHOW_EXPERIMENTAL_AUDIO_PROCESSORS);
+       changed(Property::AUDIO_MAPPING);
+       changed(Property::AUTO_CROP_THRESHOLD);
+       changed(Property::ALLOW_SMPTE_BV20);
+       changed(Property::ISDCF_NAME_PART_LENGTH);
+       changed(Property::OTHER);
+}
+
+
 void
 Config::set_initial_path(string id, boost::filesystem::path path)
 {
@@ -1645,7 +1716,63 @@ optional<boost::filesystem::path>
 Config::initial_path(string id) const
 {
        auto iter = _initial_paths.find(id);
-       DCPOMATIC_ASSERT(iter != _initial_paths.end());
+       if (iter == _initial_paths.end()) {
+               return {};
+       }
        return iter->second;
 }
 
+
+bool
+Config::zip_contains_cinemas(boost::filesystem::path zip)
+{
+       Unzipper unzipper(zip);
+       return unzipper.contains("cinemas.sqlite3") || unzipper.contains("cinemas.xml");
+}
+
+
+boost::filesystem::path
+Config::cinemas_file_from_zip(boost::filesystem::path zip)
+{
+       Unzipper unzipper(zip);
+       DCPOMATIC_ASSERT(unzipper.contains("config.xml"));
+       cxml::Document document("Config");
+       document.read_string(unzipper.get("config.xml"));
+       return document.string_child("CinemasFile");
+}
+
+
+#ifdef DCPOMATIC_GROK
+
+Config::Grok::Grok(cxml::ConstNodePtr node)
+       : enable(node->bool_child("Enable"))
+       , binary_location(node->string_child("BinaryLocation"))
+       , selected(node->number_child<int>("Selected"))
+       , licence_server(node->string_child("LicenceServer"))
+       , licence_port(node->number_child<int>("LicencePort"))
+       , licence(node->string_child("Licence"))
+{
+
+}
+
+
+void
+Config::Grok::as_xml(xmlpp::Element* node) const
+{
+       node->add_child("BinaryLocation")->add_child_text(binary_location.string());
+       node->add_child("Enable")->add_child_text((enable ? "1" : "0"));
+       node->add_child("Selected")->add_child_text(raw_convert<string>(selected));
+       node->add_child("LicenceServer")->add_child_text(licence_server);
+       node->add_child("LicencePort")->add_child_text(raw_convert<string>(licence_port));
+       node->add_child("Licence")->add_child_text(licence);
+}
+
+
+void
+Config::set_grok(Grok const& grok)
+{
+       _grok = grok;
+       changed(GROK);
+}
+
+#endif