diff options
| author | Carl Hetherington <cth@carlh.net> | 2025-10-20 00:45:17 +0200 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2026-02-16 01:20:38 +0100 |
| commit | 4cb6ab669032ef0584fde63e62addfe8a71a484c (patch) | |
| tree | 7f9fbc6d0981b1e247c3c89545f24d5a3a6ffaaa /src/lib | |
| parent | eb6464c1099de3967fc8d3b7de1461da85c7e827 (diff) | |
Use SQLite for show playlists.
Diffstat (limited to 'src/lib')
| -rw-r--r-- | src/lib/config.cc | 21 | ||||
| -rw-r--r-- | src/lib/config.h | 20 | ||||
| -rw-r--r-- | src/lib/show_playlist.cc | 46 | ||||
| -rw-r--r-- | src/lib/show_playlist.h | 79 | ||||
| -rw-r--r-- | src/lib/show_playlist_content_store.cc | 35 | ||||
| -rw-r--r-- | src/lib/show_playlist_content_store.h | 9 | ||||
| -rw-r--r-- | src/lib/show_playlist_entry.cc | 158 | ||||
| -rw-r--r-- | src/lib/show_playlist_entry.h | 78 | ||||
| -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.cc | 417 | ||||
| -rw-r--r-- | src/lib/show_playlist_list.h | 97 | ||||
| -rw-r--r-- | src/lib/spl.cc | 76 | ||||
| -rw-r--r-- | src/lib/spl.h | 143 | ||||
| -rw-r--r-- | src/lib/spl_entry.cc | 70 | ||||
| -rw-r--r-- | src/lib/sqlite_statement.cc | 17 | ||||
| -rw-r--r-- | src/lib/sqlite_statement.h | 2 | ||||
| -rw-r--r-- | src/lib/sqlite_table.cc | 8 | ||||
| -rw-r--r-- | src/lib/sqlite_table.h | 1 | ||||
| -rw-r--r-- | src/lib/wscript | 5 |
19 files changed, 966 insertions, 362 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 |
