summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2025-10-20 00:45:17 +0200
committerCarl Hetherington <cth@carlh.net>2026-02-16 01:20:38 +0100
commit4cb6ab669032ef0584fde63e62addfe8a71a484c (patch)
tree7f9fbc6d0981b1e247c3c89545f24d5a3a6ffaaa /src
parenteb6464c1099de3967fc8d3b7de1461da85c7e827 (diff)
Use SQLite for show playlists.
Diffstat (limited to 'src')
-rw-r--r--src/lib/config.cc21
-rw-r--r--src/lib/config.h20
-rw-r--r--src/lib/show_playlist.cc46
-rw-r--r--src/lib/show_playlist.h79
-rw-r--r--src/lib/show_playlist_content_store.cc35
-rw-r--r--src/lib/show_playlist_content_store.h9
-rw-r--r--src/lib/show_playlist_entry.cc158
-rw-r--r--src/lib/show_playlist_entry.h78
-rw-r--r--src/lib/show_playlist_id.h (renamed from src/lib/spl_entry.h)46
-rw-r--r--src/lib/show_playlist_list.cc417
-rw-r--r--src/lib/show_playlist_list.h97
-rw-r--r--src/lib/spl.cc76
-rw-r--r--src/lib/spl.h143
-rw-r--r--src/lib/spl_entry.cc70
-rw-r--r--src/lib/sqlite_statement.cc17
-rw-r--r--src/lib/sqlite_statement.h2
-rw-r--r--src/lib/sqlite_table.cc8
-rw-r--r--src/lib/sqlite_table.h1
-rw-r--r--src/lib/wscript5
-rw-r--r--src/tools/dcpomatic_playlist.cc259
-rw-r--r--src/wx/file_picker_ctrl.h10
-rw-r--r--src/wx/locations_preferences_page.cc19
-rw-r--r--src/wx/locations_preferences_page.h5
-rw-r--r--src/wx/playlist_controls.cc77
-rw-r--r--src/wx/playlist_controls.h14
-rw-r--r--src/wx/show_playlist_entry_dialog.cc (renamed from src/wx/spl_entry_dialog.cc)28
-rw-r--r--src/wx/show_playlist_entry_dialog.h (renamed from src/wx/spl_entry_dialog.h)10
-rw-r--r--src/wx/wscript2
28 files changed, 1163 insertions, 589 deletions
diff --git a/src/lib/config.cc b/src/lib/config.cc
index a2469ed8a..974733e8a 100644
--- a/src/lib/config.cc
+++ b/src/lib/config.cc
@@ -31,6 +31,7 @@
#include "filter.h"
#include "log.h"
#include "ratio.h"
+#include "show_playlist_list.h"
#include "unzipper.h"
#include "variant.h"
#include "zipper.h"
@@ -189,7 +190,7 @@ Config::set_defaults()
_player_debug_log_file = boost::none;
_kdm_debug_log_file = boost::none;
_player_content_directory = boost::none;
- _player_playlist_directory = boost::none;
+ _show_playlists_file = read_path("show_playlists.sqlite3");
_player_kdm_directory = boost::none;
_audio_mapping = boost::none;
_custom_languages.clear();
@@ -615,7 +616,15 @@ try
_player_debug_log_file = f.optional_string_child("PlayerDebugLogFile");
_kdm_debug_log_file = f.optional_string_child("KDMDebugLogFile");
_player_content_directory = f.optional_string_child("PlayerContentDirectory");
- _player_playlist_directory = f.optional_string_child("PlayerPlaylistDirectory");
+ if (auto spl_dir = f.optional_string_child("PlayerPlaylistDirectory")) {
+ ShowPlaylistList spl;
+ spl.read_legacy(*spl_dir);
+ }
+ if (auto spl_file = f.optional_string_child("ShowPlaylistsFile")) {
+ _show_playlists_file = *spl_file;
+ } else {
+ _show_playlists_file = read_path("show_playlists.sqlite3");
+ }
_player_kdm_directory = f.optional_string_child("PlayerKDMDirectory");
if (f.optional_node_child("AudioMapping")) {
@@ -1101,10 +1110,8 @@ Config::write_config() const
/* [XML] PlayerContentDirectory Directory to use for player content in the dual-screen mode. */
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. */
- cxml::add_text_child(root, "PlayerPlaylistDirectory", _player_playlist_directory->string());
- }
+ /* [XML] ShowPlaylistsFile Filename of SQLite3 database containing show playlists for the player dual-screen mode. */
+ cxml::add_text_child(root, "ShowPlaylistsFile", _show_playlists_file.string());
if (_player_kdm_directory) {
/* [XML] PlayerKDMDirectory Directory to use for player KDMs in the dual-screen mode. */
cxml::add_text_child(root, "PlayerKDMDirectory", _player_kdm_directory->string());
@@ -1679,7 +1686,7 @@ Config::load_from_zip(boost::filesystem::path zip_file, CinemasAction action)
changed(Property::SOUND);
changed(Property::SOUND_OUTPUT);
changed(Property::PLAYER_CONTENT_DIRECTORY);
- changed(Property::PLAYER_PLAYLIST_DIRECTORY);
+ changed(Property::SHOW_PLAYLISTS_FILE);
changed(Property::PLAYER_DEBUG_LOG);
changed(Property::HISTORY);
changed(Property::SHOW_EXPERIMENTAL_AUDIO_PROCESSORS);
diff --git a/src/lib/config.h b/src/lib/config.h
index f5e1e3788..c37829146 100644
--- a/src/lib/config.h
+++ b/src/lib/config.h
@@ -111,7 +111,7 @@ public:
SOUND,
SOUND_OUTPUT,
PLAYER_CONTENT_DIRECTORY,
- PLAYER_PLAYLIST_DIRECTORY,
+ SHOW_PLAYLISTS_FILE,
PLAYER_DEBUG_LOG,
KDM_DEBUG_LOG,
HISTORY,
@@ -582,8 +582,8 @@ public:
return _player_content_directory;
}
- boost::optional<boost::filesystem::path> player_playlist_directory() const {
- return _player_playlist_directory;
+ boost::filesystem::path show_playlists_file() const {
+ return _show_playlists_file;
}
boost::optional<boost::filesystem::path> player_kdm_directory() const {
@@ -1157,16 +1157,8 @@ public:
changed(PLAYER_CONTENT_DIRECTORY);
}
- void set_player_playlist_directory(boost::filesystem::path p) {
- maybe_set(_player_playlist_directory, p, PLAYER_PLAYLIST_DIRECTORY);
- }
-
- void unset_player_playlist_directory() {
- if (!_player_playlist_directory) {
- return;
- }
- _player_playlist_directory = boost::none;
- changed(PLAYER_PLAYLIST_DIRECTORY);
+ void set_show_playlists_file(boost::filesystem::path p) {
+ maybe_set(_show_playlists_file, p, SHOW_PLAYLISTS_FILE);
}
void set_player_kdm_directory(boost::filesystem::path p) {
@@ -1489,7 +1481,7 @@ private:
for playback.
*/
boost::optional<boost::filesystem::path> _player_content_directory;
- boost::optional<boost::filesystem::path> _player_playlist_directory;
+ boost::filesystem::path _show_playlists_file;
boost::optional<boost::filesystem::path> _player_kdm_directory;
boost::optional<AudioMapping> _audio_mapping;
std::vector<dcp::LanguageTag> _custom_languages;
diff --git a/src/lib/show_playlist.cc b/src/lib/show_playlist.cc
new file mode 100644
index 000000000..85e420d2c
--- /dev/null
+++ b/src/lib/show_playlist.cc
@@ -0,0 +1,46 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "show_playlist.h"
+
+
+nlohmann::json
+ShowPlaylist::as_json() const
+{
+ nlohmann::json json;
+ json["uuid"] = _uuid;
+ json["name"] = _name;
+ return json;
+}
+
+
+bool
+operator==(ShowPlaylist const& a, ShowPlaylist const& b)
+{
+ return a.uuid() == b.uuid() && a.name() == b.name();
+}
+
+
+bool
+operator!=(ShowPlaylist const& a, ShowPlaylist const& b)
+{
+ return a.uuid() != b.uuid() || a.name() != b.name();
+}
diff --git a/src/lib/show_playlist.h b/src/lib/show_playlist.h
new file mode 100644
index 000000000..8be2ad380
--- /dev/null
+++ b/src/lib/show_playlist.h
@@ -0,0 +1,79 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_SHOW_PLAYLIST_H
+#define DCPOMATIC_SHOW_PLAYLIST_H
+
+
+#include <dcp/util.h>
+#include <nlohmann/json.hpp>
+
+
+/** @class ShowPlaylist
+ *
+ * @brief A "show playlist": what a projection system might play for an entire cinema "show".
+ *
+ * For example, it might contain some adverts, some trailers and a feature.
+ * Each SPL has unique ID, a name, and some ordered entries (the content). The content
+ * is not stored in this class, but can be read from the database by ShowPlaylistList.
+ */
+class ShowPlaylist
+{
+public:
+ ShowPlaylist()
+ : _uuid(dcp::make_uuid())
+ {}
+
+ explicit ShowPlaylist(std::string name)
+ : _uuid(dcp::make_uuid())
+ , _name(name)
+ {}
+
+ ShowPlaylist(std::string uuid, std::string name)
+ : _uuid(uuid)
+ , _name(name)
+ {}
+
+ std::string uuid() const {
+ return _uuid;
+ }
+
+ std::string name() const {
+ return _name;
+ }
+
+ void set_name(std::string name) {
+ _name = name;
+ }
+
+ nlohmann::json as_json() const;
+
+private:
+ std::string _uuid;
+ std::string _name;
+};
+
+
+bool operator==(ShowPlaylist const& a, ShowPlaylist const& b);
+bool operator!=(ShowPlaylist const& a, ShowPlaylist const& b);
+
+
+#endif
diff --git a/src/lib/show_playlist_content_store.cc b/src/lib/show_playlist_content_store.cc
index 97a53bb74..1a9f533af 100644
--- a/src/lib/show_playlist_content_store.cc
+++ b/src/lib/show_playlist_content_store.cc
@@ -26,6 +26,7 @@
#include "examine_content_job.h"
#include "job_manager.h"
#include "show_playlist_content_store.h"
+#include "show_playlist_entry.h"
#include "util.h"
#include <dcp/cpl.h>
#include <dcp/exceptions.h>
@@ -116,27 +117,12 @@ ShowPlaylistContentStore::update(std::function<bool()> pulse)
shared_ptr<Content>
-ShowPlaylistContentStore::get_by_digest(string digest) const
+ShowPlaylistContentStore::get(string const& uuid) const
{
- auto iter = std::find_if(_content.begin(), _content.end(), [digest](shared_ptr<const Content> content) {
- return content->digest() == digest;
- });
-
- if (iter == _content.end()) {
- return {};
- }
-
- return *iter;
-}
-
-
-shared_ptr<Content>
-ShowPlaylistContentStore::get_by_cpl_id(string id) const
-{
- auto iter = std::find_if(_content.begin(), _content.end(), [id](shared_ptr<const Content> content) {
+ auto iter = std::find_if(_content.begin(), _content.end(), [uuid](shared_ptr<const Content> content) {
if (auto dcp = dynamic_pointer_cast<const DCPContent>(content)) {
for (auto cpl: dcp::find_and_resolve_cpls(dcp->directories(), true)) {
- if (cpl->id() == id) {
+ if (cpl->id() == uuid) {
return true;
}
}
@@ -145,6 +131,12 @@ ShowPlaylistContentStore::get_by_cpl_id(string id) const
});
if (iter == _content.end()) {
+ iter = std::find_if(_content.begin(), _content.end(), [uuid](shared_ptr<const Content> content) {
+ return content->digest() == uuid;
+ });
+ }
+
+ if (iter == _content.end()) {
return {};
}
@@ -152,6 +144,13 @@ ShowPlaylistContentStore::get_by_cpl_id(string id) const
}
+shared_ptr<Content>
+ShowPlaylistContentStore::get(ShowPlaylistEntry const& entry) const
+{
+ return get(entry.uuid());
+}
+
+
ShowPlaylistContentStore*
ShowPlaylistContentStore::instance()
{
diff --git a/src/lib/show_playlist_content_store.h b/src/lib/show_playlist_content_store.h
index dbee39902..02e6fc17e 100644
--- a/src/lib/show_playlist_content_store.h
+++ b/src/lib/show_playlist_content_store.h
@@ -25,6 +25,7 @@
class Content;
+class ShowPlaylistEntry;
/** @class ShowPlaylistContentStore
@@ -33,8 +34,12 @@ class Content;
class ShowPlaylistContentStore
{
public:
- std::shared_ptr<Content> get_by_digest(std::string digest) const;
- std::shared_ptr<Content> get_by_cpl_id(std::string id) const;
+ /** @param uuid UUID, which can either be a CPL UUID (for a CPL in a DCP), or
+ * a digest, for other content.
+ * @return Content, or nullptr.
+ */
+ std::shared_ptr<Content> get(std::string const& uuid) const;
+ std::shared_ptr<Content> get(ShowPlaylistEntry const& entry) const;
/** Examine content in the configured directory and update our list.
* @param pulse Called every so often to indicate progress. Return false to cancel the scan.
diff --git a/src/lib/show_playlist_entry.cc b/src/lib/show_playlist_entry.cc
new file mode 100644
index 000000000..1e16df788
--- /dev/null
+++ b/src/lib/show_playlist_entry.cc
@@ -0,0 +1,158 @@
+/*
+ Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "dcp_content.h"
+#include "dcpomatic_assert.h"
+#include "show_playlist_entry.h"
+#include <fmt/format.h>
+
+
+using std::dynamic_pointer_cast;
+using std::shared_ptr;
+using std::string;
+using boost::optional;
+
+
+ShowPlaylistEntry::ShowPlaylistEntry(shared_ptr<Content> content, optional<float> crop_to_ratio)
+ : _kind(dcp::ContentKind::FEATURE)
+ , _crop_to_ratio(crop_to_ratio)
+{
+ if (auto dcp = dynamic_pointer_cast<DCPContent>(content)) {
+ DCPOMATIC_ASSERT(dcp->cpl());
+ _uuid = *dcp->cpl();
+ _name = dcp->name();
+ _kind = dcp->content_kind().get_value_or(dcp::ContentKind::FEATURE);
+ _encrypted = dcp->encrypted();
+ } else {
+ _uuid = content->digest();
+ _name = content->path(0).filename().string();
+ _encrypted = false;
+ }
+
+ auto const hmsf = content->approximate_length().split(24);
+ _approximate_length = fmt::format("{:02d}:{:02d}:{:02d}", hmsf.h, hmsf.m, hmsf.s);
+}
+
+
+ShowPlaylistEntry::ShowPlaylistEntry(
+ string uuid,
+ string name,
+ dcp::ContentKind kind,
+ string approximate_length,
+ bool encrypted,
+ boost::optional<float> crop_to_ratio
+ )
+ : _uuid(std::move(uuid))
+ , _name(std::move(name))
+ , _kind(std::move(kind))
+ , _approximate_length(std::move(approximate_length))
+ , _encrypted(encrypted)
+ , _crop_to_ratio(crop_to_ratio)
+{
+
+}
+
+
+string
+ShowPlaylistEntry::uuid() const
+{
+ return _uuid;
+}
+
+
+string
+ShowPlaylistEntry::name() const
+{
+ return _name;
+}
+
+
+dcp::ContentKind
+ShowPlaylistEntry::kind() const
+{
+ return _kind;
+}
+
+
+bool
+ShowPlaylistEntry::encrypted() const
+{
+ return _encrypted;
+}
+
+
+string
+ShowPlaylistEntry::approximate_length() const
+{
+ return _approximate_length;
+}
+
+
+optional<float>
+ShowPlaylistEntry::crop_to_ratio() const
+{
+ return _crop_to_ratio;
+}
+
+
+void
+ShowPlaylistEntry::set_crop_to_ratio(optional<float> ratio)
+{
+ _crop_to_ratio = ratio;
+}
+
+
+nlohmann::json
+ShowPlaylistEntry::as_json() const
+{
+ nlohmann::json json;
+ json["uuid"] = _uuid;
+ json["name"] = _name;
+ json["kind"] = _kind.name();
+ json["encrypted"] = _encrypted;
+ json["approximate_length"] = _approximate_length;
+ if (_crop_to_ratio) {
+ json["crop_to_ratio"] = static_cast<int>(std::round(*_crop_to_ratio * 100));
+ }
+ return json;
+}
+
+
+
+bool
+operator==(ShowPlaylistEntry const& a, ShowPlaylistEntry const& b)
+{
+ return a.uuid() == b.uuid() &&
+ a.name() == b.name() &&
+ a.kind() == b.kind() &&
+ a.approximate_length() == b.approximate_length() &&
+ a.encrypted() == b.encrypted() &&
+ a.crop_to_ratio() == b.crop_to_ratio();
+
+}
+
+
+bool
+operator!=(ShowPlaylistEntry const& a, ShowPlaylistEntry const& b)
+{
+ return !(a == b);
+}
+
diff --git a/src/lib/show_playlist_entry.h b/src/lib/show_playlist_entry.h
new file mode 100644
index 000000000..77264f878
--- /dev/null
+++ b/src/lib/show_playlist_entry.h
@@ -0,0 +1,78 @@
+/*
+ Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_SHOW_PLAYLIST_ENTRY_H
+#define DCPOMATIC_SHOW_PLAYLIST_ENTRY_H
+
+
+#include <dcp/content_kind.h>
+#include <nlohmann/json.hpp>
+
+
+class Content;
+
+
+/** @class ShowPlaylistEntry
+ *
+ * @brief An entry on a show playlist (SPL).
+ *
+ * Given a UUID from the database, a ShowPlaylistEntry can be obtained from the
+ * ShowPlaylistList.
+ */
+class ShowPlaylistEntry
+{
+public:
+ ShowPlaylistEntry(std::shared_ptr<Content> content, boost::optional<float> crop_to_ratio);
+ ShowPlaylistEntry(
+ std::string uuid,
+ std::string name,
+ dcp::ContentKind kind,
+ std::string approximate_length,
+ bool encrypted,
+ boost::optional<float> crop_to_ratio
+ );
+
+ nlohmann::json as_json() const;
+
+ std::string uuid() const;
+ std::string name() const;
+ dcp::ContentKind kind() const;
+ std::string approximate_length() const;
+ bool encrypted() const;
+ boost::optional<float> crop_to_ratio() const;
+
+ void set_crop_to_ratio(boost::optional<float> ratio);
+
+private:
+ std::string _uuid;
+ std::string _name;
+ dcp::ContentKind _kind;
+ std::string _approximate_length;
+ bool _encrypted;
+ boost::optional<float> _crop_to_ratio;
+};
+
+
+bool operator==(ShowPlaylistEntry const& a, ShowPlaylistEntry const& b);
+bool operator!=(ShowPlaylistEntry const& a, ShowPlaylistEntry const& b);
+
+
+#endif
diff --git a/src/lib/spl_entry.h b/src/lib/show_playlist_id.h
index 6ec5c7540..489501a77 100644
--- a/src/lib/spl_entry.h
+++ b/src/lib/show_playlist_id.h
@@ -1,5 +1,5 @@
/*
- Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
This file is part of DCP-o-matic.
@@ -19,42 +19,28 @@
*/
-#ifndef DCPOMATIC_SPL_ENTRY_H
-#define DCPOMATIC_SPL_ENTRY_H
+#ifndef DCPOMATIC_SHOW_PLAYLIST_ID
+#define DCPOMATIC_SHOW_PLAYLIST_ID
-#include <libcxml/cxml.h>
-#include <dcp/content_kind.h>
-#include <libcxml/cxml.h>
+#include "id.h"
-namespace xmlpp {
- class Element;
-}
-
-class Content;
-
-
-class SPLEntry
+/** @class ShowPlaylistID
+ *
+ * @brief The SQLite ID (not the UUID) of a ShowPlaylist.
+ */
+class ShowPlaylistID : public ID
{
public:
- SPLEntry(std::shared_ptr<Content> c, cxml::ConstNodePtr node = {});
-
- void as_xml(xmlpp::Element* e) const;
-
- std::shared_ptr<Content> content;
- std::string name;
- /** Digest of this content */
- std::string digest;
- /** CPL ID */
- boost::optional<std::string> id;
- boost::optional<dcp::ContentKind> kind;
- bool encrypted = false;
- boost::optional<float> crop_to_ratio;
-
-private:
- void construct(std::shared_ptr<Content> content);
+ explicit ShowPlaylistID(sqlite3_int64 id)
+ : ID(id) {}
+
+ bool operator<(ShowPlaylistID const& other) const {
+ return get() < other.get();
+ }
};
#endif
+
diff --git a/src/lib/show_playlist_list.cc b/src/lib/show_playlist_list.cc
new file mode 100644
index 000000000..d8ec379ce
--- /dev/null
+++ b/src/lib/show_playlist_list.cc
@@ -0,0 +1,417 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#include "config.h"
+#include "show_playlist.h"
+#include "show_playlist_content_store.h"
+#include "show_playlist_entry.h"
+#include "show_playlist_list.h"
+#include "sqlite_statement.h"
+#include "sqlite_transaction.h"
+#include <dcp/filesystem.h>
+
+
+using std::make_pair;
+using std::pair;
+using std::string;
+using std::vector;
+using boost::optional;
+
+
+ShowPlaylistList::ShowPlaylistList()
+ : _show_playlists("show_playlists")
+ , _entries("entries")
+ , _db(Config::instance()->show_playlists_file())
+{
+ setup_tables();
+ setup();
+}
+
+
+ShowPlaylistList::ShowPlaylistList(boost::filesystem::path db_file)
+ : _show_playlists("show_playlists")
+ , _entries("entries")
+ , _db(db_file)
+{
+ setup_tables();
+ setup();
+}
+
+
+void
+ShowPlaylistList::setup_tables()
+{
+ _show_playlists.add_column("uuid", "TEXT");
+ _show_playlists.add_column("name", "TEXT");
+
+ _entries.add_column("show_playlist", "INTEGER");
+ _entries.add_column("uuid", "TEXT");
+ _entries.add_column("name", "TEXT");
+ _entries.add_column("kind", "TEXT");
+ _entries.add_column("approximate_length", "TEXT");
+ _entries.add_column("encrypted", "INTEGER");
+ _entries.add_column("crop_to_ratio", "REAL");
+ _entries.add_column("sort_index", "INTEGER");
+}
+
+
+void
+ShowPlaylistList::setup()
+{
+ SQLiteStatement show_playlists(_db, _show_playlists.create());
+ show_playlists.execute();
+
+ SQLiteStatement entries(_db, _entries.create());
+ entries.execute();
+}
+
+
+ShowPlaylistID
+ShowPlaylistList::add_show_playlist(ShowPlaylist const& playlist)
+{
+ SQLiteStatement statement(_db, _show_playlists.insert());
+
+ statement.bind_text(1, playlist.uuid());
+ statement.bind_text(2, playlist.name());
+
+ statement.execute();
+
+ return ShowPlaylistID(sqlite3_last_insert_rowid(_db.db()));
+}
+
+
+void
+ShowPlaylistList::update_show_playlist(ShowPlaylistID id, ShowPlaylist const& playlist)
+{
+ SQLiteStatement statement(_db, _show_playlists.update("WHERE id=?"));
+
+ statement.bind_text(1, playlist.uuid());
+ statement.bind_text(2, playlist.name());
+ statement.bind_int64(3, id.get());
+
+ statement.execute();
+}
+
+
+void
+ShowPlaylistList::remove_show_playlist(ShowPlaylistID id)
+{
+ SQLiteStatement statement(_db, "DELETE FROM show_playlists WHERE ID=?");
+ statement.bind_int64(1, id.get());
+ statement.execute();
+}
+
+
+static
+vector<pair<ShowPlaylistID, ShowPlaylist>>
+show_playlists_from_result(SQLiteStatement& statement)
+{
+ vector<pair<ShowPlaylistID, ShowPlaylist>> output;
+
+ statement.execute([&output](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 3);
+ ShowPlaylistID const id(statement.column_int64(0));
+ auto const uuid = statement.column_text(1);
+ auto const name = statement.column_text(2);
+ output.push_back(make_pair(id, ShowPlaylist(uuid, name)));
+ });
+
+ return output;
+}
+
+
+vector<pair<ShowPlaylistID, ShowPlaylist>>
+ShowPlaylistList::show_playlists() const
+{
+ SQLiteStatement statement(_db, _show_playlists.select("ORDER BY name COLLATE unicode ASC"));
+ return show_playlists_from_result(statement);
+}
+
+
+optional<ShowPlaylist>
+ShowPlaylistList::show_playlist(ShowPlaylistID id) const
+{
+ SQLiteStatement statement(_db, _show_playlists.select("WHERE id=?"));
+ statement.bind_int64(1, id.get());
+ auto results = show_playlists_from_result(statement);
+ if (results.empty()) {
+ return {};
+ }
+
+ return results[0].second;
+}
+
+
+
+vector<ShowPlaylistEntry>
+ShowPlaylistList::entries(std::string const& where, std::function<void (SQLiteStatement&)> bind) const
+{
+ SQLiteStatement statement(
+ _db,
+ fmt::format(
+ "SELECT entries.uuid,entries.name,entries.kind,entries.approximate_length,entries.encrypted,entries.crop_to_ratio "
+ "FROM entries "
+ "JOIN show_playlists ON entries.show_playlist=show_playlists.id "
+ "{} ORDER BY entries.sort_index", where
+ )
+ );
+
+ bind(statement);
+
+ vector<ShowPlaylistEntry> output;
+
+ statement.execute([&output](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 6);
+ output.push_back(
+ ShowPlaylistEntry(
+ statement.column_text(0),
+ statement.column_text(1),
+ dcp::ContentKind::from_name(statement.column_text(2)),
+ statement.column_text(3),
+ statement.column_int64(4),
+ statement.column_double(5) > 0 ? optional<float>(statement.column_double(5)) : optional<float>()
+ )
+ );
+ });
+
+ return output;
+}
+
+
+vector<ShowPlaylistEntry>
+ShowPlaylistList::entries(ShowPlaylistID show_playlist_id) const
+{
+ return entries("WHERE show_playlists.id=?", [show_playlist_id](SQLiteStatement& statement) { statement.bind_int64(1, show_playlist_id.get()); });
+}
+
+
+vector<ShowPlaylistEntry>
+ShowPlaylistList::entries(string const& show_playlist_uuid) const
+{
+ return entries("WHERE show_playlists.uuid=?", [show_playlist_uuid](SQLiteStatement& statement) { statement.bind_text(1, show_playlist_uuid); });
+}
+
+
+void
+ShowPlaylistList::add_entry(ShowPlaylistID playlist_id, ShowPlaylistEntry const& entry)
+{
+ SQLiteTransaction transaction(_db);
+
+ SQLiteStatement find_last_entry(_db, "SELECT MAX(sort_index) FROM entries WHERE show_playlist=?");
+ find_last_entry.bind_int64(1, playlist_id.get());
+
+ int highest_index = 0;
+ find_last_entry.execute([&highest_index](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 1);
+ highest_index = statement.column_int64(0);
+ });
+
+ SQLiteStatement count_entries(_db, "SELECT COUNT(id) FROM entries WHERE show_playlist=?");
+ count_entries.bind_int64(1, playlist_id.get());
+
+ count_entries.execute([&highest_index](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 1);
+ if (statement.column_int64(0) == 0) {
+ highest_index = -1;
+ }
+ });
+
+ SQLiteStatement add_entry(_db, _entries.insert());
+
+ add_entry.bind_int64(1, playlist_id.get());
+ add_entry.bind_text(2, entry.uuid());
+ add_entry.bind_text(3, entry.name());
+ add_entry.bind_text(4, entry.kind().name());
+ add_entry.bind_text(5, entry.approximate_length());
+ add_entry.bind_int64(6, entry.encrypted());
+ add_entry.bind_double(7, entry.crop_to_ratio().get_value_or(0));
+ add_entry.bind_int64(8, highest_index + 1);
+
+ add_entry.execute();
+
+ transaction.commit();
+}
+
+
+void
+ShowPlaylistList::update_entry(ShowPlaylistID playlist_id, int index, ShowPlaylistEntry const& entry)
+{
+ SQLiteStatement update_entry(_db, _entries.update("WHERE show_playlist=? AND sort_index=?"));
+
+ update_entry.bind_int64(1, playlist_id.get());
+ update_entry.bind_text(2, entry.uuid());
+ update_entry.bind_text(3, entry.name());
+ update_entry.bind_text(4, entry.kind().name());
+ update_entry.bind_text(5, entry.approximate_length());
+ update_entry.bind_int64(6, entry.encrypted());
+ update_entry.bind_double(7, entry.crop_to_ratio().get_value_or(0));
+ update_entry.bind_int64(8, index);
+ update_entry.bind_int64(9, playlist_id.get());
+ update_entry.bind_int64(10, index);
+
+ update_entry.execute();
+}
+
+
+void
+ShowPlaylistList::remove_entry(ShowPlaylistID playlist_id, int index)
+{
+ SQLiteTransaction transaction(_db);
+
+ SQLiteStatement delete_entry(_db, _entries.remove("WHERE show_playlist=? AND sort_index=?"));
+ delete_entry.bind_int64(1, playlist_id.get());
+ delete_entry.bind_int64(2, index);
+
+ delete_entry.execute();
+
+ SQLiteStatement find(_db, "SELECT id FROM entries WHERE show_playlist=? ORDER BY sort_index");
+ find.bind_int64(1, playlist_id.get());
+
+ vector<sqlite3_int64> ids;
+ find.execute([&ids](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 1);
+ ids.push_back(statement.column_int64(0));
+ });
+
+ int new_index = 0;
+ for (auto id: ids) {
+ SQLiteStatement update(_db, "UPDATE entries SET sort_index=? WHERE id=?");
+ update.bind_int64(1, new_index++);
+ update.bind_int64(2, id);
+ update.execute();
+ }
+
+ transaction.commit();
+}
+
+
+/** Swap the entries at index and index + 1 */
+void
+ShowPlaylistList::swap_entries(ShowPlaylistID playlist_id, int index)
+{
+ SQLiteTransaction transaction(_db);
+
+ SQLiteStatement find(_db, "SELECT id,sort_index FROM entries WHERE show_playlist=? ORDER BY sort_index LIMIT 2 OFFSET ?");
+ find.bind_int64(1, playlist_id.get());
+ find.bind_int64(2, index);
+
+ vector<pair<int64_t, int64_t>> rows;
+ find.execute([&rows](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 2);
+ rows.push_back({statement.column_int64(0), statement.column_int64(1)});
+ });
+
+ DCPOMATIC_ASSERT(rows.size() == 2);
+
+ SQLiteStatement swap1(_db, "UPDATE entries SET sort_index=? WHERE id=?");
+ swap1.bind_int64(1, rows[0].second);
+ swap1.bind_int64(2, rows[1].first);
+ swap1.execute();
+
+ SQLiteStatement swap2(_db, "UPDATE entries SET sort_index=? WHERE id=?");
+ swap2.bind_int64(1, rows[1].second);
+ swap2.bind_int64(2, rows[0].first);
+ swap2.execute();
+
+ transaction.commit();
+}
+
+
+void
+ShowPlaylistList::move_entry_up(ShowPlaylistID playlist_id, int index)
+{
+ DCPOMATIC_ASSERT(index >= 1);
+ swap_entries(playlist_id, index - 1);
+}
+
+
+void
+ShowPlaylistList::move_entry_down(ShowPlaylistID playlist_id, int index)
+{
+ swap_entries(playlist_id, index);
+}
+
+
+void
+ShowPlaylistList::read_legacy(boost::filesystem::path dir)
+{
+ auto const store = ShowPlaylistContentStore::instance();
+
+ for (auto playlist: boost::filesystem::directory_iterator(dir)) {
+ cxml::Document doc("SPL");
+ doc.read_file(dcp::filesystem::fix_long_path(playlist));
+ auto const spl_id = add_show_playlist({ doc.string_child("Id"), doc.string_child("Name") });
+ for (auto entry: doc.node_children("Entry")) {
+ auto uuid = entry->optional_string_child("CPL");
+ if (!uuid) {
+ uuid = entry->string_child("Digest");
+ }
+ if (auto content = store->get(*uuid)) {
+ add_entry(spl_id, ShowPlaylistEntry(content, entry->optional_number_child<float>("CropToRatio")));
+ }
+ }
+ }
+}
+
+
+bool
+ShowPlaylistList::missing(string const& playlist_uuid) const
+{
+ auto store = ShowPlaylistContentStore::instance();
+ for (auto entry: entries(playlist_uuid)) {
+ if (!store->get(entry)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+bool
+ShowPlaylistList::missing(ShowPlaylistID playlist_id) const
+{
+ auto store = ShowPlaylistContentStore::instance();
+ for (auto entry: entries(playlist_id)) {
+ if (!store->get(entry)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+optional<ShowPlaylistID>
+ShowPlaylistList::get_show_playlist_id(string const& playlist_uuid) const
+{
+ SQLiteStatement statement(_db, "SELECT id FROM show_playlists WHERE uuid=?");
+ statement.bind_text(1, playlist_uuid);
+
+ optional<ShowPlaylistID> id;
+ statement.execute([&id](SQLiteStatement& statement) {
+ DCPOMATIC_ASSERT(statement.data_count() == 1);
+ id = ShowPlaylistID(statement.column_int64(0));
+ });
+
+ return id;
+}
+
diff --git a/src/lib/show_playlist_list.h b/src/lib/show_playlist_list.h
new file mode 100644
index 000000000..c1849f589
--- /dev/null
+++ b/src/lib/show_playlist_list.h
@@ -0,0 +1,97 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of DCP-o-matic.
+
+ DCP-o-matic is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ DCP-o-matic is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
+
+*/
+
+
+#ifndef DCPOMATIC_SHOW_PLAYLIST_LIST_H
+#define DCPOMATIC_SHOW_PLAYLIST_LIST_H
+
+
+#include "show_playlist_entry.h"
+#include "show_playlist_id.h"
+#include "sqlite_database.h"
+#include "sqlite_table.h"
+
+
+class ShowPlaylist;
+class SQLiteStatement;
+
+
+/** @class ShowPlaylistList
+ *
+ * @brief A list of SPLs (show playlists) stored in a SQLite database.
+ *
+ * A SPL (show playlist) is a list of content (and maybe later automation cues)
+ * that make up a "show" in a cinema/theater. For example, a SPL might contain
+ * some adverts, some trailers and a feature.
+ *
+ * There are two tables: show_playlists and entries. show_playlists contains
+ * just playlist UUIDs with their names.
+ */
+class ShowPlaylistList
+{
+public:
+ ShowPlaylistList();
+ explicit ShowPlaylistList(boost::filesystem::path db_file);
+
+ /** Write a ShowPlaylist to the database, returning its new SQLite ID */
+ ShowPlaylistID add_show_playlist(ShowPlaylist const& show_playlist);
+ void update_show_playlist(ShowPlaylistID id, ShowPlaylist const& show_playlist);
+ void remove_show_playlist(ShowPlaylistID id);
+
+ std::vector<std::pair<ShowPlaylistID, ShowPlaylist>> show_playlists() const;
+ boost::optional<ShowPlaylist> show_playlist(ShowPlaylistID id) const;
+
+ boost::optional<ShowPlaylistID> get_show_playlist_id(std::string const& show_playlist_uuid) const;
+
+ /** @return The entries on a given show playlist, given the playlist's SQLite ID */
+ std::vector<ShowPlaylistEntry> entries(ShowPlaylistID show_playlist_id) const;
+ /** @return The entries on a given show playlist, given the playlist's UUID */
+ std::vector<ShowPlaylistEntry> entries(std::string const& show_playlist_uuid) const;
+
+ bool missing(std::string const& show_playlist_uuid) const;
+ bool missing(ShowPlaylistID id) const;
+
+ /** Write a playlist entry to the database */
+ void add_entry(ShowPlaylistID, ShowPlaylistEntry const& entry);
+ /** Set the values in the database from entry */
+ void update_entry(ShowPlaylistID, int index, ShowPlaylistEntry const& entry);
+ /** Remove a playlist entry from the database */
+ void remove_entry(ShowPlaylistID, int index);
+ /** Move the given playlist entry one place higher (earlier) */
+ void move_entry_up(ShowPlaylistID, int index);
+ /** Move the given playlist entry one place lower (later) */
+ void move_entry_down(ShowPlaylistID, int index);
+
+ void read_legacy(boost::filesystem::path dir);
+
+private:
+ void setup_tables();
+ void setup();
+ std::vector<ShowPlaylistEntry> entries(std::string const& where, std::function<void (SQLiteStatement&)> bind) const;
+ void swap_entries(ShowPlaylistID playlist_id, int index);
+
+ SQLiteTable _show_playlists;
+ SQLiteTable _entries;
+ mutable SQLiteDatabase _db;
+};
+
+
+
+#endif
diff --git a/src/lib/spl.cc b/src/lib/spl.cc
deleted file mode 100644
index e958bf7c9..000000000
--- a/src/lib/spl.cc
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
-
- This file is part of DCP-o-matic.
-
- DCP-o-matic is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- DCP-o-matic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "show_playlist_content_store.h"
-#include "spl.h"
-#include <libcxml/cxml.h>
-#include <dcp/filesystem.h>
-#include <dcp/warnings.h>
-LIBDCP_DISABLE_WARNINGS
-#include <libxml++/libxml++.h>
-LIBDCP_ENABLE_WARNINGS
-#include <iostream>
-
-
-using std::cout;
-using std::string;
-using std::shared_ptr;
-
-
-void
-SPL::read(boost::filesystem::path path, ShowPlaylistContentStore* store)
-{
- _spl.clear ();
- _missing = false;
- cxml::Document doc ("SPL");
- doc.read_file(dcp::filesystem::fix_long_path(path));
- _id = doc.string_child("Id");
- _name = doc.string_child("Name");
- for (auto i: doc.node_children("Entry")) {
- if (auto cpl = i->optional_string_child("CPL")) {
- if (auto c = store->get_by_cpl_id(*cpl)) {
- add(SPLEntry(c, i));
- } else {
- _missing = true;
- }
- } else {
- if (auto c = store->get_by_digest(i->string_child("Digest"))) {
- add(SPLEntry(c, i));
- } else {
- _missing = true;
- }
- }
- }
-}
-
-
-void
-SPL::write (boost::filesystem::path path) const
-{
- xmlpp::Document doc;
- auto root = doc.create_root_node ("SPL");
- cxml::add_text_child(root, "Id", _id);
- cxml::add_text_child(root, "Name", _name);
- for (auto i: _spl) {
- i.as_xml(cxml::add_child(root, "Entry"));
- }
- doc.write_to_file_formatted (path.string());
-}
diff --git a/src/lib/spl.h b/src/lib/spl.h
deleted file mode 100644
index 9d1a9fdbc..000000000
--- a/src/lib/spl.h
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- Copyright (C) 2018-2020 Carl Hetherington <cth@carlh.net>
-
- This file is part of DCP-o-matic.
-
- DCP-o-matic is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- DCP-o-matic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#ifndef DCPOMATIC_SPL_H
-#define DCPOMATIC_SPL_H
-
-
-#include "spl_entry.h"
-#include <dcp/util.h>
-LIBDCP_DISABLE_WARNINGS
-#include <boost/signals2.hpp>
-LIBDCP_ENABLE_WARNINGS
-#include <algorithm>
-
-
-class ShowPlaylistContentStore;
-
-
-class SPL
-{
-public:
- SPL()
- : _id(dcp::make_uuid())
- {}
-
- SPL(std::string name)
- : _id(dcp::make_uuid())
- , _name(name)
- {}
-
- void add(SPLEntry e) {
- _spl.push_back(e);
- }
-
- void remove(std::size_t index) {
- _spl.erase(_spl.begin() + index);
- }
-
- std::vector<SPLEntry> const & get() const {
- return _spl;
- }
-
- SPLEntry const& get(std::size_t index) const {
- return _spl[index];
- }
-
- void set(std::size_t index, SPLEntry entry) {
- _spl[index] = std::move(entry);
- }
-
- void swap(size_t a, size_t b) {
- std::iter_swap(_spl.begin() + a, _spl.begin() + b);
- }
-
- void read(boost::filesystem::path path, ShowPlaylistContentStore* store);
- void write(boost::filesystem::path path) const;
-
- std::string id() const {
- return _id;
- }
-
- std::string name() const {
- return _name;
- }
-
- void set_name(std::string name) {
- _name = name;
- }
-
- bool missing() const {
- return _missing;
- }
-
-private:
- std::string _id;
- std::string _name;
- std::vector<SPLEntry> _spl;
- /** true if any content was missing when read() was last called on this SPL */
- bool _missing = false;
-};
-
-
-class SignalSPL : public SPL
-{
-public:
- enum class Change {
- NAME,
- CONTENT,
- };
-
- SignalSPL() {}
-
- SignalSPL(std::string name)
- : SPL(name)
- {}
-
- void set_name(std::string name) {
- SPL::set_name(name);
- Changed(Change::NAME);
- }
-
- void add(SPLEntry e) {
- SPL::add(e);
- Changed(Change::CONTENT);
- }
-
- void remove(std::size_t index) {
- SPL::remove(index);
- Changed(Change::CONTENT);
- }
-
- void swap(size_t a, size_t b) {
- SPL::swap(a, b);
- Changed(Change::CONTENT);
- }
-
- void set(std::size_t index, SPLEntry entry) {
- SPL::set(index, entry);
- Changed(Change::CONTENT);
- }
-
- boost::signals2::signal<void (Change)> Changed;
-};
-
-#endif
diff --git a/src/lib/spl_entry.cc b/src/lib/spl_entry.cc
deleted file mode 100644
index 2e6860235..000000000
--- a/src/lib/spl_entry.cc
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- Copyright (C) 2018-2021 Carl Hetherington <cth@carlh.net>
-
- This file is part of DCP-o-matic.
-
- DCP-o-matic is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- DCP-o-matic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>.
-
-*/
-
-
-#include "dcp_content.h"
-#include "dcpomatic_assert.h"
-#include "spl_entry.h"
-#include <dcp/warnings.h>
-#include <fmt/format.h>
-LIBDCP_DISABLE_WARNINGS
-#include <libxml++/libxml++.h>
-LIBDCP_ENABLE_WARNINGS
-
-
-using std::dynamic_pointer_cast;
-using std::shared_ptr;
-
-
-SPLEntry::SPLEntry(shared_ptr<Content> c, cxml::ConstNodePtr node)
- : content(c)
- , digest(content->digest())
-{
- if (auto dcp = dynamic_pointer_cast<DCPContent>(content)) {
- name = dcp->name();
- DCPOMATIC_ASSERT(dcp->cpl());
- id = dcp->cpl();
- kind = dcp->content_kind().get_value_or(dcp::ContentKind::FEATURE);
- encrypted = dcp->encrypted();
- } else {
- name = content->path(0).filename().string();
- kind = dcp::ContentKind::FEATURE;
- }
-
- if (node) {
- if (auto crop = node->optional_number_child<float>("CropToRatio")) {
- crop_to_ratio = *crop;
- }
- }
-}
-
-
-void
-SPLEntry::as_xml(xmlpp::Element* e) const
-{
- if (id) {
- cxml::add_text_child(e, "CPL", *id);
- } else {
- cxml::add_text_child(e, "Digest", digest);
- }
- if (crop_to_ratio) {
- cxml::add_text_child(e, "CropToRatio", fmt::to_string(*crop_to_ratio));
- }
-}
diff --git a/src/lib/sqlite_statement.cc b/src/lib/sqlite_statement.cc
index d130da507..72d33d1b4 100644
--- a/src/lib/sqlite_statement.cc
+++ b/src/lib/sqlite_statement.cc
@@ -69,6 +69,16 @@ SQLiteStatement::bind_int64(int index, int64_t value)
void
+SQLiteStatement::bind_double(int index, double value)
+{
+ auto rc = sqlite3_bind_double(_stmt, index, value);
+ if (rc != SQLITE_OK) {
+ throw SQLError(_db, rc);
+ }
+}
+
+
+void
SQLiteStatement::execute(function<void(SQLiteStatement&)> row, function<void()> busy)
{
while (true) {
@@ -104,6 +114,13 @@ SQLiteStatement::column_int64(int index)
}
+double
+SQLiteStatement::column_double(int index)
+{
+ return sqlite3_column_double(_stmt, index);
+}
+
+
string
SQLiteStatement::column_text(int index)
{
diff --git a/src/lib/sqlite_statement.h b/src/lib/sqlite_statement.h
index f1131e899..942348e1a 100644
--- a/src/lib/sqlite_statement.h
+++ b/src/lib/sqlite_statement.h
@@ -38,9 +38,11 @@ public:
void bind_text(int index, std::string const& value);
void bind_int64(int index, int64_t value);
+ void bind_double(int index, double value);
int64_t column_int64(int index);
std::string column_text(int index);
+ double column_double(int index);
void execute(std::function<void(SQLiteStatement&)> row = std::function<void(SQLiteStatement& statement)>(), std::function<void()> busy = std::function<void()>());
diff --git a/src/lib/sqlite_table.cc b/src/lib/sqlite_table.cc
index 81843ee00..79c6f99d0 100644
--- a/src/lib/sqlite_table.cc
+++ b/src/lib/sqlite_table.cc
@@ -76,3 +76,11 @@ SQLiteTable::select(string const& condition) const
{
return fmt::format("SELECT id,{} FROM {} {}", join_strings(_columns, ","), _name, condition);
}
+
+
+string
+SQLiteTable::remove(string const& condition) const
+{
+ DCPOMATIC_ASSERT(!_columns.empty());
+ return fmt::format("DELETE FROM {} {}", _name, condition);
+}
diff --git a/src/lib/sqlite_table.h b/src/lib/sqlite_table.h
index 43c9491ed..218ab4526 100644
--- a/src/lib/sqlite_table.h
+++ b/src/lib/sqlite_table.h
@@ -42,6 +42,7 @@ public:
std::string insert() const;
std::string update(std::string const& condition) const;
std::string select(std::string const& condition) const;
+ std::string remove(std::string const& condition) const;
private:
std::string _name;
diff --git a/src/lib/wscript b/src/lib/wscript
index 4478c2587..ed55ec4d0 100644
--- a/src/lib/wscript
+++ b/src/lib/wscript
@@ -195,10 +195,11 @@ sources = """
send_problem_report_job.cc
server.cc
show_playlist_content_store.cc
+ show_playlist_entry.cc
+ show_playlist_list.cc
+ show_playlist.cc
shuffler.cc
state.cc
- spl.cc
- spl_entry.cc
sqlite_database.cc
sqlite_statement.cc
sqlite_table.cc
diff --git a/src/tools/dcpomatic_playlist.cc b/src/tools/dcpomatic_playlist.cc
index 053927232..9483bb273 100644
--- a/src/tools/dcpomatic_playlist.cc
+++ b/src/tools/dcpomatic_playlist.cc
@@ -24,7 +24,7 @@
#include "wx/dcpomatic_button.h"
#include "wx/i18n_setup.h"
#include "wx/playlist_editor_config_dialog.h"
-#include "wx/spl_entry_dialog.h"
+#include "wx/show_playlist_entry_dialog.h"
#include "wx/wx_signal_manager.h"
#include "wx/wx_util.h"
#include "wx/wx_variant.h"
@@ -33,8 +33,9 @@
#include "lib/cross.h"
#include "lib/dcp_content.h"
#include "lib/film.h"
-#include "lib/spl.h"
-#include "lib/spl_entry.h"
+#include "lib/show_playlist.h"
+#include "lib/show_playlist_entry.h"
+#include "lib/show_playlist_list.h"
#include <dcp/filesystem.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
@@ -63,16 +64,6 @@ using namespace boost::placeholders;
#endif
-static
-void
-save_playlist(shared_ptr<const SPL> playlist)
-{
- if (auto dir = Config::instance()->player_playlist_directory()) {
- playlist->write(*dir / (playlist->id() + ".xml"));
- }
-}
-
-
class ContentDialog : public wxDialog
{
public:
@@ -113,9 +104,9 @@ private:
class PlaylistList
{
public:
- PlaylistList(wxPanel* parent)
+ PlaylistList(wxPanel* parent, shared_ptr<ShowPlaylistList> playlists)
: _sizer(new wxBoxSizer(wxVERTICAL))
- , _parent(parent)
+ , _playlists(playlists)
{
auto label = new wxStaticText(parent, wxID_ANY, wxEmptyString);
label->SetLabelMarkup(_("<b>Playlists</b>"));
@@ -140,7 +131,7 @@ public:
_sizer->Add(list);
- load_playlists();
+ add_playlists_to_view();
_list->Bind(wxEVT_COMMAND_LIST_ITEM_SELECTED, bind(&PlaylistList::selection_changed, this));
_list->Bind(wxEVT_COMMAND_LIST_ITEM_DESELECTED, bind(&PlaylistList::selection_changed, this));
@@ -155,16 +146,16 @@ public:
return _sizer;
}
- shared_ptr<SignalSPL> first_playlist() const
+ void name_changed()
{
- if (_playlists.empty()) {
- return {};
- }
-
- return _playlists.front();
+ /* The order might have changed, so just re-add everything */
+ add_playlists_to_view();
}
- boost::signals2::signal<void (shared_ptr<SignalSPL>)> Edit;
+ /** Signalled as a request to start editing the given playlist, or no playlist if
+ * the optional is empty.
+ */
+ boost::signals2::signal<void (boost::optional<ShowPlaylistID>)> Edit;
private:
void setup_sensitivity()
@@ -172,119 +163,63 @@ private:
_delete->Enable(static_cast<bool>(selected()));
}
- void add_playlist_to_view(shared_ptr<const SignalSPL> playlist)
+ void add_playlist_to_view(ShowPlaylistID id, ShowPlaylist playlist)
{
wxListItem item;
item.SetId(_list->GetItemCount());
+ item.SetData(id.get());
long const N = _list->InsertItem(item);
- _list->SetItem(N, 0, std_to_wx(playlist->name()));
- }
-
- void add_playlist_to_model(shared_ptr<SignalSPL> playlist)
- {
- _playlists.push_back(playlist);
- playlist->Changed.connect(bind(&PlaylistList::changed, this, weak_ptr<SignalSPL>(playlist), _1));
- }
-
- void changed(weak_ptr<SignalSPL> wp, SignalSPL::Change change)
- {
- auto playlist = wp.lock();
- if (!playlist) {
- return;
- }
-
- switch (change) {
- case SignalSPL::Change::NAME:
- {
- int N = 0;
- for (auto i: _playlists) {
- if (i == playlist) {
- _list->SetItem(N, 0, std_to_wx(i->name()));
- }
- ++N;
- }
- break;
- }
- case SignalSPL::Change::CONTENT:
- save_playlist(playlist);
- break;
- }
+ _list->SetItem(N, 0, std_to_wx(playlist.name()));
}
- void load_playlists()
+ void add_playlists_to_view()
{
- auto path = Config::instance()->player_playlist_directory();
- if (!path) {
- return;
- }
-
_list->DeleteAllItems();
- _playlists.clear();
- try {
- for (auto i: dcp::filesystem::directory_iterator(*path)) {
- auto spl = make_shared<SignalSPL>();
- try {
- spl->read(i, ShowPlaylistContentStore::instance());
- add_playlist_to_model(spl);
- } catch (...) {}
- }
- } catch (...) {}
-
- for (auto i: _playlists) {
- add_playlist_to_view(i);
+ for (auto playlist: _playlists->show_playlists()) {
+ add_playlist_to_view(playlist.first, playlist.second);
}
}
void new_playlist()
{
- auto dir = Config::instance()->player_playlist_directory();
- if (!dir) {
- error_dialog(_parent, _("No playlist folder is specified in preferences. Please set one and then try again."));
- return;
- }
-
- auto spl = std::make_shared<SignalSPL>(wx_to_std(_("New Playlist")));
- add_playlist_to_model(spl);
- add_playlist_to_view(spl);
+ ShowPlaylist spl(wx_to_std(_("New Playlist")));
+ auto const id = _playlists->add_show_playlist(spl);
+ add_playlist_to_view(id, spl);
_list->SetItemState(_list->GetItemCount() - 1, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
+ Edit(id);
}
- boost::optional<int> selected() const
+ /** @return index and ShowPlaylistID (i.e. SQLite ID) of selected playlist */
+ boost::optional<std::pair<int, ShowPlaylistID>> selected() const
{
long int selected = _list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
- if (selected < 0 || selected >= int(_playlists.size())) {
+ if (selected < 0 || selected >= _list->GetItemCount()) {
return {};
}
- return selected;
+ return make_pair(static_cast<int>(selected), ShowPlaylistID(static_cast<sqlite3_int64>(_list->GetItemData(selected))));
}
void delete_playlist()
{
- auto index = selected();
- if (!index) {
+ auto const id = selected();
+ if (!id) {
return;
}
- auto dir = Config::instance()->player_playlist_directory();
- if (!dir) {
- return;
- }
+ _playlists->remove_show_playlist(id->second);
+ _list->DeleteItem(id->first);
- dcp::filesystem::remove(*dir / (_playlists[*index]->id() + ".xml"));
- _list->DeleteItem(*index);
- _playlists.erase(_playlists.begin() + *index);
-
- Edit(shared_ptr<SignalSPL>());
+ Edit(optional<ShowPlaylistID>());
}
void selection_changed()
{
long int selected = _list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
- if (selected < 0 || selected >= int(_playlists.size())) {
- Edit(shared_ptr<SignalSPL>());
+ if (selected < 0 || selected >= int(_playlists->show_playlists().size())) {
+ Edit(optional<ShowPlaylistID>());
} else {
- Edit(_playlists[selected]);
+ Edit(_playlists->show_playlists()[selected].first);
}
setup_sensitivity();
@@ -294,18 +229,19 @@ private:
wxListCtrl* _list;
wxButton* _new;
wxButton* _delete;
- vector<shared_ptr<SignalSPL>> _playlists;
- wxWindow* _parent;
+ std::shared_ptr<ShowPlaylistList> _playlists;
};
+/** Editor for the contents of a playlist */
class PlaylistContent
{
public:
- PlaylistContent(wxPanel* parent, ContentDialog* content_dialog)
+ PlaylistContent(wxPanel* parent, ContentDialog* content_dialog, shared_ptr<ShowPlaylistList> playlists)
: _content_dialog(content_dialog)
, _parent(parent)
, _sizer(new wxBoxSizer(wxVERTICAL))
+ , _playlists(playlists)
{
auto title = new wxBoxSizer(wxHORIZONTAL);
auto label = new wxStaticText(parent, wxID_ANY, wxEmptyString);
@@ -365,33 +301,40 @@ public:
return _sizer;
}
- void set(shared_ptr<SignalSPL> playlist)
+ /** Set the playlist to edit */
+ void set(optional<ShowPlaylistID> playlist)
{
_playlist = playlist;
_list->DeleteAllItems();
if (_playlist) {
- for (auto i: _playlist->get()) {
- add(i);
+ if (auto spl = _playlists->show_playlist(*_playlist)) {
+ for (auto i: _playlists->entries(*_playlist)) {
+ add(i);
+ }
+ _name->SetValue(std_to_wx(spl->name()));
}
- _name->SetValue(std_to_wx(_playlist->name()));
} else {
_name->SetValue({});
}
setup_sensitivity();
}
- shared_ptr<SignalSPL> playlist() const
+ optional<ShowPlaylistID> playlist() const
{
return _playlist;
}
+ boost::signals2::signal<void ()> NameChanged;
private:
void save_name_clicked()
{
if (_playlist) {
- _playlist->set_name(wx_to_std(_name->GetValue()));
- save_playlist(_playlist);
+ if (auto spl = _playlists->show_playlist(*_playlist)) {
+ spl->set_name(wx_to_std(_name->GetValue()));
+ _playlists->update_show_playlist(*_playlist, *spl);
+ NameChanged();
+ }
}
setup_sensitivity();
}
@@ -401,21 +344,21 @@ private:
setup_sensitivity();
}
- void add(SPLEntry e)
+ void add(ShowPlaylistEntry entry)
{
wxListItem item;
item.SetId(_list->GetItemCount());
long const N = _list->InsertItem(item);
- set_item(N, e);
+ set_item(N, entry);
}
- void set_item(long N, SPLEntry e)
+ void set_item(long N, ShowPlaylistEntry e)
{
- _list->SetItem(N, 0, std_to_wx(e.name));
- _list->SetItem(N, 1, std_to_wx(e.id.get_value_or("")));
- _list->SetItem(N, 2, std_to_wx(e.kind->name()));
- _list->SetItem(N, 3, e.encrypted ? S_("Question|Y") : S_("Question|N"));
- _list->SetItem(N, 4, e.crop_to_ratio ? wxString::Format(char_to_wx("%.2f"), *e.crop_to_ratio) : char_to_wx(""));
+ _list->SetItem(N, 0, std_to_wx(e.name()));
+ _list->SetItem(N, 1, std_to_wx(e.uuid()));
+ _list->SetItem(N, 2, std_to_wx(e.kind().name()));
+ _list->SetItem(N, 3, e.encrypted() ? S_("Question|Y") : S_("Question|N"));
+ _list->SetItem(N, 4, e.crop_to_ratio() ? wxString::Format(char_to_wx("%.2f"), *e.crop_to_ratio()) : char_to_wx(""));
}
void setup_sensitivity()
@@ -424,10 +367,18 @@ private:
int const num_selected = _list->GetSelectedItemCount();
long int selected = _list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
_name->Enable(have_list);
- _save_name->Enable(_playlist && _playlist->name() != wx_to_std(_name->GetValue()));
+
+ bool can_save_name = false;
+ if (_playlist) {
+ if (auto spl = _playlists->show_playlist(*_playlist)) {
+ can_save_name = spl->name() != wx_to_std(_name->GetValue());
+ }
+ }
+ _save_name->Enable(can_save_name);
+
_list->Enable(have_list);
_up->Enable(have_list && selected > 0);
- _down->Enable(have_list && selected != -1 && selected <(_list->GetItemCount() - 1));
+ _down->Enable(have_list && selected != -1 && selected < (_list->GetItemCount() - 1));
_add->Enable(have_list);
_remove->Enable(have_list && num_selected > 0);
_edit->Enable(have_list && num_selected > 0);
@@ -435,14 +386,12 @@ private:
void add_clicked()
{
- int const r = _content_dialog->ShowModal();
- if (r == wxID_OK) {
- auto content = _content_dialog->selected();
- if (content) {
- SPLEntry e(content);
- add(e);
+ if (_content_dialog->ShowModal() == wxID_OK) {
+ if (auto content = _content_dialog->selected()) {
+ ShowPlaylistEntry entry(content, {});
+ add(entry);
DCPOMATIC_ASSERT(_playlist);
- _playlist->add(e);
+ _playlists->add_entry(*_playlist, entry);
}
}
}
@@ -456,10 +405,11 @@ private:
DCPOMATIC_ASSERT(_playlist);
- _playlist->swap(s, s - 1);
+ _playlists->move_entry_up(*_playlist, s);
- set_item(s - 1, _playlist->get(s - 1));
- set_item(s, _playlist->get(s));
+ auto entries = _playlists->entries(*_playlist);
+ set_item(s - 1, entries[s - 1]);
+ set_item(s, entries[s]);
}
void down_clicked()
@@ -471,10 +421,11 @@ private:
DCPOMATIC_ASSERT(_playlist);
- _playlist->swap(s, s + 1);
+ _playlists->move_entry_down(*_playlist, s);
- set_item(s + 1, _playlist->get(s + 1));
- set_item(s, _playlist->get(s));
+ auto entries = _playlists->entries(*_playlist);
+ set_item(s + 1, entries[s + 1]);
+ set_item(s, entries[s]);
}
void remove_clicked()
@@ -485,7 +436,7 @@ private:
}
DCPOMATIC_ASSERT(_playlist);
- _playlist->remove(s);
+ _playlists->remove_entry(*_playlist, s);
_list->DeleteItem(s);
}
@@ -496,10 +447,13 @@ private:
return;
}
- SPLEntryDialog dialog(_parent, _playlist->get(s));
+ DCPOMATIC_ASSERT(_playlist);
+ auto const entry = _playlists->entries(*_playlist)[s];
+
+ ShowPlaylistEntryDialog dialog(_parent, entry);
if (dialog.ShowModal() == wxID_OK) {
- _playlist->set(s, dialog.get());
- set_item(s, _playlist->get(s));
+ _playlists->update_entry(*_playlist, s, dialog.get());
+ set_item(s, _playlists->entries(*_playlist)[s]);
}
}
@@ -514,7 +468,8 @@ private:
wxButton* _add;
wxButton* _remove;
wxButton* _edit;
- shared_ptr<SignalSPL> _playlist;
+ boost::optional<ShowPlaylistID> _playlist;
+ std::shared_ptr<ShowPlaylistList> _playlists;
};
@@ -525,6 +480,7 @@ public:
: wxFrame(nullptr, wxID_ANY, title)
, _content_dialog(new ContentDialog(this))
, _config_dialog(nullptr)
+ , _playlists(make_shared<ShowPlaylistList>())
{
auto bar = new wxMenuBar;
setup_menu(bar);
@@ -536,8 +492,8 @@ public:
auto overall_panel = new wxPanel(this, wxID_ANY);
auto sizer = new wxBoxSizer(wxVERTICAL);
- _playlist_list = new PlaylistList(overall_panel);
- _playlist_content = new PlaylistContent(overall_panel, _content_dialog);
+ _playlist_list = new PlaylistList(overall_panel, _playlists);
+ _playlist_content = new PlaylistContent(overall_panel, _content_dialog, _playlists);
sizer->Add(_playlist_list->sizer());
sizer->Add(_playlist_content->sizer());
@@ -545,6 +501,7 @@ public:
overall_panel->SetSizer(sizer);
_playlist_list->Edit.connect(bind(&DOMFrame::change_playlist, this, _1));
+ _playlist_content->NameChanged.connect(bind(&DOMFrame::playlist_name_changed, this));
Bind(wxEVT_MENU, boost::bind(&DOMFrame::file_exit, this), wxID_EXIT);
Bind(wxEVT_MENU, boost::bind(&DOMFrame::help_about, this), wxID_ABOUT);
@@ -575,15 +532,17 @@ private:
_config_dialog->Show(this);
}
- void change_playlist(shared_ptr<SignalSPL> playlist)
+ /** Start editing a different playlist, or no playlist if the optional is empty */
+ void change_playlist(optional<ShowPlaylistID> playlist)
{
- auto old = _playlist_content->playlist();
- if (old) {
- save_playlist(old);
- }
_playlist_content->set(playlist);
}
+ void playlist_name_changed()
+ {
+ _playlist_list->name_changed();
+ }
+
void setup_menu(wxMenuBar* m)
{
#ifdef DCPOMATIC_OSX
@@ -625,9 +584,10 @@ private:
this,
wxString::Format(
_("Could not write to config file at %s. Your changes have not been saved."),
- std_to_wx(Config::instance()->cinemas_file().string()).data()
- )
- );
+ std_to_wx(Config::config_write_file().string()).data()
+ )
+ );
+
}
}
@@ -636,6 +596,7 @@ private:
PlaylistContent* _playlist_content;
wxPreferencesEditor* _config_dialog;
boost::signals2::scoped_connection _config_changed_connection;
+ std::shared_ptr<ShowPlaylistList> _playlists;
};
diff --git a/src/wx/file_picker_ctrl.h b/src/wx/file_picker_ctrl.h
index 57363b0c7..df68588b8 100644
--- a/src/wx/file_picker_ctrl.h
+++ b/src/wx/file_picker_ctrl.h
@@ -25,6 +25,9 @@ LIBDCP_DISABLE_WARNINGS
LIBDCP_ENABLE_WARNINGS
#include <boost/filesystem.hpp>
#include <boost/optional.hpp>
+LIBDCP_DISABLE_WARNINGS
+#include <boost/signals2.hpp>
+LIBDCP_ENABLE_WARNINGS
class FilePickerCtrl : public wxPanel
@@ -45,6 +48,13 @@ public:
void set_path(boost::optional<boost::filesystem::path> path);
void set_wildcard(wxString);
+ template <typename... Args>
+ void bind(Args... args) {
+ Changed.connect(boost::bind(std::forward<Args>(args)...));
+ }
+
+ boost::signals2::signal<void ()> Changed;
+
private:
void browse_clicked ();
void set_filename(boost::optional<std::string> filename);
diff --git a/src/wx/locations_preferences_page.cc b/src/wx/locations_preferences_page.cc
index f77f88fb4..0eaa5dca0 100644
--- a/src/wx/locations_preferences_page.cc
+++ b/src/wx/locations_preferences_page.cc
@@ -19,6 +19,7 @@
*/
+#include "file_picker_ctrl.h"
#include "locations_preferences_page.h"
#include "wx_util.h"
#include <wx/filepicker.h>
@@ -68,9 +69,9 @@ LocationsPage::setup()
table->Add(_content_directory, wxGBPosition(r, 1));
++r;
- add_label_to_sizer(table, _panel, _("Playlist directory"), true, wxGBPosition(r, 0));
- _playlist_directory = new wxDirPickerCtrl(_panel, wxID_ANY, wxEmptyString, char_to_wx(wxDirSelectorPromptStr), wxDefaultPosition, wxSize(300, -1));
- table->Add(_playlist_directory, wxGBPosition(r, 1));
+ add_label_to_sizer(table, _panel, _("Show playlists file"), true, wxGBPosition(r, 0));
+ _show_playlists_file = new FilePickerCtrl(_panel, _("Select show playlists file"), char_to_wx("*.sqlite3"), false, true, "ShowPlaylistsPath");
+ table->Add(_show_playlists_file, wxGBPosition(r, 1));
++r;
add_label_to_sizer(table, _panel, _("KDM directory"), true, wxGBPosition(r, 0));
@@ -79,7 +80,7 @@ LocationsPage::setup()
++r;
_content_directory->Bind(wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::content_directory_changed, this));
- _playlist_directory->Bind(wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::playlist_directory_changed, this));
+ _show_playlists_file->bind(&LocationsPage::show_playlists_file_changed, this);
_kdm_directory->Bind(wxEVT_DIRPICKER_CHANGED, bind(&LocationsPage::kdm_directory_changed, this));
}
@@ -91,9 +92,7 @@ LocationsPage::config_changed()
if (config->player_content_directory()) {
checked_set(_content_directory, *config->player_content_directory());
}
- if (config->player_playlist_directory()) {
- checked_set(_playlist_directory, *config->player_playlist_directory());
- }
+ checked_set(_show_playlists_file, config->show_playlists_file());
if (config->player_kdm_directory()) {
checked_set(_kdm_directory, *config->player_kdm_directory());
}
@@ -106,9 +105,11 @@ LocationsPage::content_directory_changed()
}
void
-LocationsPage::playlist_directory_changed()
+LocationsPage::show_playlists_file_changed()
{
- Config::instance()->set_player_playlist_directory(wx_to_std(_playlist_directory->GetPath()));
+ if (auto path = _show_playlists_file->path()) {
+ Config::instance()->set_show_playlists_file(*path);
+ }
}
void
diff --git a/src/wx/locations_preferences_page.h b/src/wx/locations_preferences_page.h
index 8bb9e3e5a..cd3f3593f 100644
--- a/src/wx/locations_preferences_page.h
+++ b/src/wx/locations_preferences_page.h
@@ -22,6 +22,7 @@
#include "preferences_page.h"
+class FilePickerCtrl;
class wxDirPickerCtrl;
@@ -44,11 +45,11 @@ private:
void setup() override;
void config_changed() override;
void content_directory_changed();
- void playlist_directory_changed();
+ void show_playlists_file_changed();
void kdm_directory_changed();
wxDirPickerCtrl* _content_directory;
- wxDirPickerCtrl* _playlist_directory;
+ FilePickerCtrl* _show_playlists_file;
wxDirPickerCtrl* _kdm_directory;
};
diff --git a/src/wx/playlist_controls.cc b/src/wx/playlist_controls.cc
index 0356738e1..380831ab8 100644
--- a/src/wx/playlist_controls.cc
+++ b/src/wx/playlist_controls.cc
@@ -34,6 +34,8 @@
#include "lib/internet.h"
#include "lib/player_video.h"
#include "lib/scoped_temporary.h"
+#include "lib/show_playlist.h"
+#include "lib/show_playlist_list.h"
#include <dcp/exceptions.h>
#include <dcp/warnings.h>
LIBDCP_DISABLE_WARNINGS
@@ -118,10 +120,10 @@ PlaylistControls::PlaylistControls(wxWindow* parent, FilmViewer& viewer)
_spl_view->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&PlaylistControls::spl_selection_changed, this));
_spl_view->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&PlaylistControls::spl_selection_changed, this));
_viewer.Finished.connect(boost::bind(&PlaylistControls::viewer_finished, this));
- _refresh_spl_view->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::update_playlist_directory, this));
+ _refresh_spl_view->Bind(wxEVT_BUTTON, boost::bind(&PlaylistControls::update_playlists, this));
_refresh_content_view->Bind(wxEVT_BUTTON, boost::bind(&ContentView::update, _content_view));
- update_playlist_directory();
+ update_playlists();
}
@@ -219,7 +221,7 @@ PlaylistControls::previous_clicked()
bool
PlaylistControls::can_do_next()
{
- return _selected_playlist && (_selected_playlist_position + 1) < int(_playlists[*_selected_playlist].get().size());
+ return _selected_playlist && (_selected_playlist_position + 1) < static_cast<int>(_playlists->entries(*_selected_playlist).size());
}
@@ -236,15 +238,19 @@ PlaylistControls::next_clicked()
void
-PlaylistControls::add_playlist_to_list(SPL spl)
+PlaylistControls::add_playlist_to_list(ShowPlaylist spl)
{
int const N = _spl_view->GetItemCount();
wxListItem it;
it.SetId(N);
it.SetColumn(0);
+ auto id = _playlists->get_show_playlist_id(spl.uuid());
+ DCPOMATIC_ASSERT(id);
+ it.SetData(id->get());
string t = spl.name();
- if (spl.missing()) {
+
+ if (_playlists->missing(spl.uuid())) {
t += " (content missing)";
}
it.SetText(std_to_wx(t));
@@ -253,33 +259,15 @@ PlaylistControls::add_playlist_to_list(SPL spl)
void
-PlaylistControls::update_playlist_directory()
+PlaylistControls::update_playlists()
{
using namespace boost::filesystem;
_spl_view->DeleteAllItems();
- optional<path> dir = Config::instance()->player_playlist_directory();
- if (!dir) {
- return;
- }
-
- _playlists.clear();
-
- for (directory_iterator i = directory_iterator(*dir); i != directory_iterator(); ++i) {
- try {
- if (is_regular_file(i->path()) && i->path().extension() == ".xml") {
- SPL spl;
- spl.read(i->path(), ShowPlaylistContentStore::instance());
- _playlists.push_back(spl);
- }
- } catch (exception& e) {
- /* Never mind */
- }
- }
+ _playlists.reset(new ShowPlaylistList());
- sort(_playlists.begin(), _playlists.end(), [](SPL const& a, SPL const& b) { return a.name() < b.name(); });
- for (auto i: _playlists) {
- add_playlist_to_list(i);
+ for (auto i: _playlists->show_playlists()) {
+ add_playlist_to_list(i.second);
}
_selected_playlist = boost::none;
@@ -321,29 +309,32 @@ PlaylistControls::spl_selection_changed()
return;
}
- if (_playlists[selected].missing()) {
+ auto const id = ShowPlaylistID(_spl_view->GetItemData(selected));
+
+ if (_playlists->missing(id)) {
error_dialog(this, _("This playlist cannot be loaded as some content is missing."));
deselect_playlist();
return;
}
- if (_playlists[selected].get().empty()) {
+ if (_playlists->entries(id).empty()) {
error_dialog(this, _("This playlist is empty."));
return;
}
- select_playlist(selected, 0);
+ select_playlist(id, 0);
}
void
-PlaylistControls::select_playlist(int selected, int position)
+PlaylistControls::select_playlist(ShowPlaylistID selected, int position)
{
wxProgressDialog dialog(variant::wx::dcpomatic(), _("Loading playlist and KDMs"));
- for (auto const& i: _playlists[selected].get()) {
+ auto const store = ShowPlaylistContentStore::instance();
+ for (auto const& i: _playlists->entries(selected)) {
dialog.Pulse();
- auto dcp = dynamic_pointer_cast<DCPContent>(i.content);
+ auto dcp = dynamic_pointer_cast<DCPContent>(store->get(i));
if (dcp && dcp->needs_kdm()) {
optional<dcp::EncryptedKDM> kdm;
kdm = get_kdm_from_directory(dcp);
@@ -367,11 +358,11 @@ PlaylistControls::select_playlist(int selected, int position)
_current_spl_view->DeleteAllItems();
int N = 0;
- for (auto i: _playlists[selected].get()) {
+ for (auto const& i: _playlists->entries(selected)) {
wxListItem it;
it.SetId(N);
it.SetColumn(0);
- it.SetText(std_to_wx(i.name));
+ it.SetText(std_to_wx(i.name()));
_current_spl_view->InsertItem(it);
++N;
}
@@ -389,10 +380,14 @@ void
PlaylistControls::reset_film()
{
DCPOMATIC_ASSERT(_selected_playlist);
+
auto film = std::make_shared<Film>(optional<boost::filesystem::path>());
- auto entry = _playlists[*_selected_playlist].get(_selected_playlist_position);
- film->add_content(vector<shared_ptr<Content>>{entry.content});
- ResetFilm(film, entry.crop_to_ratio);
+
+ auto const entries = _playlists->entries(*_selected_playlist);
+ DCPOMATIC_ASSERT(_selected_playlist_position < static_cast<int>(entries.size()));
+ auto const entry = entries[_selected_playlist_position];
+ film->add_content(vector<shared_ptr<Content>>{ShowPlaylistContentStore::instance()->get(entry)});
+ ResetFilm(film, entry.crop_to_ratio());
}
@@ -403,8 +398,8 @@ PlaylistControls::config_changed(int property)
if (property == Config::PLAYER_CONTENT_DIRECTORY) {
_content_view->update();
- } else if (property == Config::PLAYER_PLAYLIST_DIRECTORY) {
- update_playlist_directory();
+ } else if (property == Config::SHOW_PLAYLISTS_FILE) {
+ update_playlists();
}
}
@@ -431,7 +426,7 @@ PlaylistControls::viewer_finished()
}
_selected_playlist_position++;
- if (_selected_playlist_position < int(_playlists[*_selected_playlist].get().size())) {
+ if (_selected_playlist_position < int(_playlists->entries(*_selected_playlist).size())) {
/* Next piece of content on the SPL */
update_current_content();
_viewer.start();
diff --git a/src/wx/playlist_controls.h b/src/wx/playlist_controls.h
index e99e54b65..ea9e79cf4 100644
--- a/src/wx/playlist_controls.h
+++ b/src/wx/playlist_controls.h
@@ -20,10 +20,12 @@
#include "controls.h"
-#include "lib/spl.h"
+#include "lib/show_playlist.h"
+#include "lib/show_playlist_id.h"
class DCPContent;
+class ShowPlaylistList;
class PlaylistControls : public Controls
@@ -46,11 +48,11 @@ private:
void stop_clicked();
void next_clicked();
void previous_clicked();
- void add_playlist_to_list(SPL spl);
+ void add_playlist_to_list(ShowPlaylist spl);
void update_content_directory();
- void update_playlist_directory();
+ void update_playlists();
void spl_selection_changed();
- void select_playlist(int selected, int position);
+ void select_playlist(ShowPlaylistID selected, int position);
void started() override;
void stopped() override;
void setup_sensitivity() override;
@@ -76,7 +78,7 @@ private:
wxButton* _refresh_spl_view;
wxListCtrl* _current_spl_view;
- std::vector<SPL> _playlists;
- boost::optional<int> _selected_playlist;
+ std::unique_ptr<ShowPlaylistList> _playlists;
+ boost::optional<ShowPlaylistID> _selected_playlist;
int _selected_playlist_position;
};
diff --git a/src/wx/spl_entry_dialog.cc b/src/wx/show_playlist_entry_dialog.cc
index 332b7840c..07e1d2e92 100644
--- a/src/wx/spl_entry_dialog.cc
+++ b/src/wx/show_playlist_entry_dialog.cc
@@ -21,9 +21,9 @@
#include "check_box.h"
#include "ratio_picker.h"
-#include "spl_entry_dialog.h"
+#include "show_playlist_entry_dialog.h"
#include "wx_util.h"
-#include "lib/spl.h"
+#include "lib/show_playlist_entry.h"
#include <wx/wx.h>
@@ -34,43 +34,43 @@ using namespace boost::placeholders;
-SPLEntryDialog::SPLEntryDialog(wxWindow* parent, SPLEntry entry)
+ShowPlaylistEntryDialog::ShowPlaylistEntryDialog(wxWindow* parent, ShowPlaylistEntry entry)
: TableDialog(parent, _("Playlist item"), 2, 1, true)
, _entry(entry)
{
add(_("Name"), true);
- auto name = _entry.name;
+ auto name = _entry.name();
#ifdef DCPOMATIC_LINUX
boost::replace_all(name, "_", "__");
#endif
add(std_to_wx(name), false);
- add(_("CPL"), true);
- add(std_to_wx(_entry.id.get_value_or("")), false);
+ add(_("UUID"), true);
+ add(std_to_wx(_entry.uuid()), false);
add(_("Type"), true);
- add(std_to_wx(_entry.kind->name()), false);
+ add(std_to_wx(_entry.kind().name()), false);
add(_("Encrypted"), true);
- add(_entry.encrypted ? _("Yes") : _("No"), false);
+ add(_entry.encrypted() ? _("Yes") : _("No"), false);
- _crop = new RatioPicker(this, entry.crop_to_ratio);
+ _crop = new RatioPicker(this, entry.crop_to_ratio());
add(_crop->enable_checkbox(), false);
add(_crop, false);
layout();
- _crop->Changed.connect(boost::bind(&SPLEntryDialog::crop_changed, this, _1));
+ _crop->Changed.connect(boost::bind(&ShowPlaylistEntryDialog::crop_changed, this, _1));
}
-SPLEntry
-SPLEntryDialog::get() const
+ShowPlaylistEntry
+ShowPlaylistEntryDialog::get() const
{
return _entry;
}
void
-SPLEntryDialog::crop_changed(optional<float> ratio)
+ShowPlaylistEntryDialog::crop_changed(optional<float> ratio)
{
- _entry.crop_to_ratio = ratio;
+ _entry.set_crop_to_ratio(ratio);
}
diff --git a/src/wx/spl_entry_dialog.h b/src/wx/show_playlist_entry_dialog.h
index 63e4ba56f..3b644d5ad 100644
--- a/src/wx/spl_entry_dialog.h
+++ b/src/wx/show_playlist_entry_dialog.h
@@ -20,23 +20,23 @@
#include "table_dialog.h"
-#include "lib/spl.h"
+#include "lib/show_playlist_entry.h"
class RatioPicker;
-class SPLEntryDialog : public TableDialog
+class ShowPlaylistEntryDialog : public TableDialog
{
public:
- SPLEntryDialog(wxWindow* parent, SPLEntry entry);
+ ShowPlaylistEntryDialog(wxWindow* parent, ShowPlaylistEntry entry);
- SPLEntry get() const;
+ ShowPlaylistEntry get() const;
private:
void crop_changed(boost::optional<float> ratio);
- SPLEntry _entry;
+ ShowPlaylistEntry _entry;
RatioPicker* _crop;
};
diff --git a/src/wx/wscript b/src/wx/wscript
index d3f3db88a..99a6f4814 100644
--- a/src/wx/wscript
+++ b/src/wx/wscript
@@ -166,7 +166,7 @@ sources = """
simple_video_view.cc
smpte_metadata_dialog.cc
sound_preferences_page.cc
- spl_entry_dialog.cc
+ show_playlist_entry_dialog.cc
standard_controls.cc
static_text.cc
subtag_list_ctrl.cc