aboutsummaryrefslogtreecommitdiffstats
path: root/src/db/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'src/db/plugins')
-rw-r--r--src/db/plugins/LazyDatabase.cxx108
-rw-r--r--src/db/plugins/LazyDatabase.hxx69
-rw-r--r--src/db/plugins/ProxyDatabasePlugin.cxx816
-rw-r--r--src/db/plugins/ProxyDatabasePlugin.hxx27
-rw-r--r--src/db/plugins/simple/DatabaseSave.cxx159
-rw-r--r--src/db/plugins/simple/DatabaseSave.hxx35
-rw-r--r--src/db/plugins/simple/Directory.cxx277
-rw-r--r--src/db/plugins/simple/Directory.hxx285
-rw-r--r--src/db/plugins/simple/DirectorySave.cxx206
-rw-r--r--src/db/plugins/simple/DirectorySave.hxx35
-rw-r--r--src/db/plugins/simple/Mount.cxx96
-rw-r--r--src/db/plugins/simple/Mount.hxx36
-rw-r--r--src/db/plugins/simple/PrefixedLightSong.hxx41
-rw-r--r--src/db/plugins/simple/SimpleDatabasePlugin.cxx495
-rw-r--r--src/db/plugins/simple/SimpleDatabasePlugin.hxx144
-rw-r--r--src/db/plugins/simple/Song.cxx111
-rw-r--r--src/db/plugins/simple/Song.hxx129
-rw-r--r--src/db/plugins/simple/SongSort.cxx108
-rw-r--r--src/db/plugins/simple/SongSort.hxx30
-rw-r--r--src/db/plugins/upnp/ContentDirectoryService.cxx204
-rw-r--r--src/db/plugins/upnp/Directory.cxx262
-rw-r--r--src/db/plugins/upnp/Directory.hxx66
-rw-r--r--src/db/plugins/upnp/Object.cxx25
-rw-r--r--src/db/plugins/upnp/Object.hxx85
-rw-r--r--src/db/plugins/upnp/Tags.cxx33
-rw-r--r--src/db/plugins/upnp/Tags.hxx28
-rw-r--r--src/db/plugins/upnp/UpnpDatabasePlugin.cxx786
-rw-r--r--src/db/plugins/upnp/UpnpDatabasePlugin.hxx27
28 files changed, 4723 insertions, 0 deletions
diff --git a/src/db/plugins/LazyDatabase.cxx b/src/db/plugins/LazyDatabase.cxx
new file mode 100644
index 000000000..bc52395c5
--- /dev/null
+++ b/src/db/plugins/LazyDatabase.cxx
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "LazyDatabase.hxx"
+#include "db/Interface.hxx"
+
+#include <assert.h>
+
+LazyDatabase::LazyDatabase(Database *_db)
+ :Database(_db->GetPlugin()), db(_db), open(false) {}
+
+LazyDatabase::~LazyDatabase()
+{
+ assert(!open);
+
+ delete db;
+}
+
+bool
+LazyDatabase::EnsureOpen(Error &error) const
+{
+ if (open)
+ return true;
+
+ if (!db->Open(error))
+ return false;
+
+ open = true;
+ return true;
+}
+
+void
+LazyDatabase::Close()
+{
+ if (open) {
+ open = false;
+ db->Close();
+ }
+}
+
+const LightSong *
+LazyDatabase::GetSong(const char *uri, Error &error) const
+{
+ return EnsureOpen(error)
+ ? db->GetSong(uri, error)
+ : nullptr;
+}
+
+void
+LazyDatabase::ReturnSong(const LightSong *song) const
+{
+ assert(open);
+
+ db->ReturnSong(song);
+}
+
+bool
+LazyDatabase::Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ return EnsureOpen(error) &&
+ db->Visit(selection, visit_directory, visit_song,
+ visit_playlist, error);
+}
+
+bool
+LazyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const
+{
+ return EnsureOpen(error) &&
+ db->VisitUniqueTags(selection, tag_type, group_mask, visit_tag,
+ error);
+}
+
+bool
+LazyDatabase::GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats, Error &error) const
+{
+ return EnsureOpen(error) && db->GetStats(selection, stats, error);
+}
+
+time_t
+LazyDatabase::GetUpdateStamp() const
+{
+ return open ? db->GetUpdateStamp() : 0;
+}
diff --git a/src/db/plugins/LazyDatabase.hxx b/src/db/plugins/LazyDatabase.hxx
new file mode 100644
index 000000000..ae1b961d0
--- /dev/null
+++ b/src/db/plugins/LazyDatabase.hxx
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_LAZY_DATABASE_PLUGIN_HXX
+#define MPD_LAZY_DATABASE_PLUGIN_HXX
+
+#include "db/Interface.hxx"
+#include "Compiler.h"
+
+/**
+ * A wrapper for a #Database object that gets opened on the first
+ * database access. This works around daemonization problems with
+ * some plugins.
+ */
+class LazyDatabase final : public Database {
+ Database *const db;
+
+ mutable bool open;
+
+public:
+ gcc_nonnull_all
+ LazyDatabase(Database *_db);
+
+ virtual ~LazyDatabase();
+
+ virtual void Close() override;
+
+ virtual const LightSong *GetSong(const char *uri_utf8,
+ Error &error) const override;
+ virtual void ReturnSong(const LightSong *song) const;
+
+ virtual bool Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const override;
+
+ virtual bool VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const override;
+
+ virtual bool GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats,
+ Error &error) const override;
+
+ virtual time_t GetUpdateStamp() const override;
+
+private:
+ bool EnsureOpen(Error &error) const;
+};
+
+#endif
diff --git a/src/db/plugins/ProxyDatabasePlugin.cxx b/src/db/plugins/ProxyDatabasePlugin.cxx
new file mode 100644
index 000000000..918e6be49
--- /dev/null
+++ b/src/db/plugins/ProxyDatabasePlugin.cxx
@@ -0,0 +1,816 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "ProxyDatabasePlugin.hxx"
+#include "db/Interface.hxx"
+#include "db/DatabasePlugin.hxx"
+#include "db/DatabaseListener.hxx"
+#include "db/Selection.hxx"
+#include "db/DatabaseError.hxx"
+#include "db/PlaylistInfo.hxx"
+#include "db/LightDirectory.hxx"
+#include "db/LightSong.hxx"
+#include "db/Stats.hxx"
+#include "SongFilter.hxx"
+#include "Compiler.h"
+#include "config/ConfigData.hxx"
+#include "tag/TagBuilder.hxx"
+#include "tag/Tag.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "protocol/Ack.hxx"
+#include "event/SocketMonitor.hxx"
+#include "event/IdleMonitor.hxx"
+#include "Log.hxx"
+
+#include <mpd/client.h>
+#include <mpd/async.h>
+
+#include <cassert>
+#include <string>
+#include <list>
+
+class ProxySong : public LightSong {
+ Tag tag2;
+
+public:
+ explicit ProxySong(const mpd_song *song);
+};
+
+class AllocatedProxySong : public ProxySong {
+ mpd_song *const song;
+
+public:
+ explicit AllocatedProxySong(mpd_song *_song)
+ :ProxySong(_song), song(_song) {}
+
+ ~AllocatedProxySong() {
+ mpd_song_free(song);
+ }
+};
+
+class ProxyDatabase final : public Database, SocketMonitor, IdleMonitor {
+ DatabaseListener &listener;
+
+ std::string host;
+ unsigned port;
+
+ struct mpd_connection *connection;
+
+ /* this is mutable because GetStats() must be "const" */
+ mutable time_t update_stamp;
+
+ /**
+ * The libmpdclient idle mask that was removed from the other
+ * MPD. This will be handled by the next OnIdle() call.
+ */
+ unsigned idle_received;
+
+ /**
+ * Is the #connection currently "idle"? That is, did we send
+ * the "idle" command to it?
+ */
+ bool is_idle;
+
+public:
+ ProxyDatabase(EventLoop &_loop, DatabaseListener &_listener)
+ :Database(proxy_db_plugin),
+ SocketMonitor(_loop), IdleMonitor(_loop),
+ listener(_listener) {}
+
+ static Database *Create(EventLoop &loop, DatabaseListener &listener,
+ const config_param &param,
+ Error &error);
+
+ virtual bool Open(Error &error) override;
+ virtual void Close() override;
+ virtual const LightSong *GetSong(const char *uri_utf8,
+ Error &error) const override;
+ virtual void ReturnSong(const LightSong *song) const;
+
+ virtual bool Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const override;
+
+ virtual bool VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const override;
+
+ virtual bool GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats,
+ Error &error) const override;
+
+ virtual time_t GetUpdateStamp() const override {
+ return update_stamp;
+ }
+
+private:
+ bool Configure(const config_param &param, Error &error);
+
+ bool Connect(Error &error);
+ bool CheckConnection(Error &error);
+ bool EnsureConnected(Error &error);
+
+ void Disconnect();
+
+ /* virtual methods from SocketMonitor */
+ virtual bool OnSocketReady(unsigned flags) override;
+
+ /* virtual methods from IdleMonitor */
+ virtual void OnIdle() override;
+};
+
+static constexpr Domain libmpdclient_domain("libmpdclient");
+
+static constexpr struct {
+ TagType d;
+ enum mpd_tag_type s;
+} tag_table[] = {
+ { TAG_ARTIST, MPD_TAG_ARTIST },
+ { TAG_ALBUM, MPD_TAG_ALBUM },
+ { TAG_ALBUM_ARTIST, MPD_TAG_ALBUM_ARTIST },
+ { TAG_TITLE, MPD_TAG_TITLE },
+ { TAG_TRACK, MPD_TAG_TRACK },
+ { TAG_NAME, MPD_TAG_NAME },
+ { TAG_GENRE, MPD_TAG_GENRE },
+ { TAG_DATE, MPD_TAG_DATE },
+ { TAG_COMPOSER, MPD_TAG_COMPOSER },
+ { TAG_PERFORMER, MPD_TAG_PERFORMER },
+ { TAG_COMMENT, MPD_TAG_COMMENT },
+ { TAG_DISC, MPD_TAG_DISC },
+ { TAG_MUSICBRAINZ_ARTISTID, MPD_TAG_MUSICBRAINZ_ARTISTID },
+ { TAG_MUSICBRAINZ_ALBUMID, MPD_TAG_MUSICBRAINZ_ALBUMID },
+ { TAG_MUSICBRAINZ_ALBUMARTISTID,
+ MPD_TAG_MUSICBRAINZ_ALBUMARTISTID },
+ { TAG_MUSICBRAINZ_TRACKID, MPD_TAG_MUSICBRAINZ_TRACKID },
+ { TAG_NUM_OF_ITEM_TYPES, MPD_TAG_COUNT }
+};
+
+static void
+Copy(TagBuilder &tag, TagType d_tag,
+ const struct mpd_song *song, enum mpd_tag_type s_tag)
+{
+
+ for (unsigned i = 0;; ++i) {
+ const char *value = mpd_song_get_tag(song, s_tag, i);
+ if (value == nullptr)
+ break;
+
+ tag.AddItem(d_tag, value);
+ }
+}
+
+ProxySong::ProxySong(const mpd_song *song)
+{
+ directory = nullptr;
+ uri = mpd_song_get_uri(song);
+ real_uri = nullptr;
+ tag = &tag2;
+ mtime = mpd_song_get_last_modified(song);
+
+#if LIBMPDCLIENT_CHECK_VERSION(2,3,0)
+ start_ms = mpd_song_get_start(song) * 1000;
+ end_ms = mpd_song_get_end(song) * 1000;
+#else
+ start_ms = end_ms = 0;
+#endif
+
+ TagBuilder tag_builder;
+ tag_builder.SetTime(mpd_song_get_duration(song));
+
+ for (const auto *i = &tag_table[0]; i->d != TAG_NUM_OF_ITEM_TYPES; ++i)
+ Copy(tag_builder, i->d, song, i->s);
+
+ tag_builder.Commit(tag2);
+}
+
+gcc_const
+static enum mpd_tag_type
+Convert(TagType tag_type)
+{
+ for (auto i = &tag_table[0]; i->d != TAG_NUM_OF_ITEM_TYPES; ++i)
+ if (i->d == tag_type)
+ return i->s;
+
+ return MPD_TAG_COUNT;
+}
+
+static bool
+CheckError(struct mpd_connection *connection, Error &error)
+{
+ const auto code = mpd_connection_get_error(connection);
+ if (code == MPD_ERROR_SUCCESS)
+ return true;
+
+ if (code == MPD_ERROR_SERVER) {
+ /* libmpdclient's "enum mpd_server_error" is the same
+ as our "enum ack" */
+ const auto server_error =
+ mpd_connection_get_server_error(connection);
+ error.Set(ack_domain, (int)server_error,
+ mpd_connection_get_error_message(connection));
+ } else {
+ error.Set(libmpdclient_domain, (int)code,
+ mpd_connection_get_error_message(connection));
+ }
+
+ mpd_connection_clear_error(connection);
+ return false;
+}
+
+static bool
+SendConstraints(mpd_connection *connection, const SongFilter::Item &item)
+{
+ switch (item.GetTag()) {
+ mpd_tag_type tag;
+
+#if LIBMPDCLIENT_CHECK_VERSION(2,9,0)
+ case LOCATE_TAG_BASE_TYPE:
+ if (mpd_connection_cmp_server_version(connection, 0, 18, 0) < 0)
+ /* requires MPD 0.18 */
+ return true;
+
+ return mpd_search_add_base_constraint(connection,
+ MPD_OPERATOR_DEFAULT,
+ item.GetValue().c_str());
+#endif
+
+ case LOCATE_TAG_FILE_TYPE:
+ return mpd_search_add_uri_constraint(connection,
+ MPD_OPERATOR_DEFAULT,
+ item.GetValue().c_str());
+
+ case LOCATE_TAG_ANY_TYPE:
+ return mpd_search_add_any_tag_constraint(connection,
+ MPD_OPERATOR_DEFAULT,
+ item.GetValue().c_str());
+
+ default:
+ tag = Convert(TagType(item.GetTag()));
+ if (tag == MPD_TAG_COUNT)
+ return true;
+
+ return mpd_search_add_tag_constraint(connection,
+ MPD_OPERATOR_DEFAULT,
+ tag,
+ item.GetValue().c_str());
+ }
+}
+
+static bool
+SendConstraints(mpd_connection *connection, const SongFilter &filter)
+{
+ for (const auto &i : filter.GetItems())
+ if (!SendConstraints(connection, i))
+ return false;
+
+ return true;
+}
+
+static bool
+SendConstraints(mpd_connection *connection, const DatabaseSelection &selection)
+{
+#if LIBMPDCLIENT_CHECK_VERSION(2,9,0)
+ if (!selection.uri.empty() &&
+ mpd_connection_cmp_server_version(connection, 0, 18, 0) >= 0) {
+ /* requires MPD 0.18 */
+ if (!mpd_search_add_base_constraint(connection,
+ MPD_OPERATOR_DEFAULT,
+ selection.uri.c_str()))
+ return false;
+ }
+#endif
+
+ if (selection.filter != nullptr &&
+ !SendConstraints(connection, *selection.filter))
+ return false;
+
+ return true;
+}
+
+Database *
+ProxyDatabase::Create(EventLoop &loop, DatabaseListener &listener,
+ const config_param &param, Error &error)
+{
+ ProxyDatabase *db = new ProxyDatabase(loop, listener);
+ if (!db->Configure(param, error)) {
+ delete db;
+ db = nullptr;
+ }
+
+ return db;
+}
+
+bool
+ProxyDatabase::Configure(const config_param &param, gcc_unused Error &error)
+{
+ host = param.GetBlockValue("host", "");
+ port = param.GetBlockValue("port", 0u);
+
+ return true;
+}
+
+bool
+ProxyDatabase::Open(Error &error)
+{
+ if (!Connect(error))
+ return false;
+
+ update_stamp = 0;
+
+ return true;
+}
+
+void
+ProxyDatabase::Close()
+{
+ if (connection != nullptr)
+ Disconnect();
+}
+
+bool
+ProxyDatabase::Connect(Error &error)
+{
+ const char *_host = host.empty() ? nullptr : host.c_str();
+ connection = mpd_connection_new(_host, port, 0);
+ if (connection == nullptr) {
+ error.Set(libmpdclient_domain, (int)MPD_ERROR_OOM,
+ "Out of memory");
+ return false;
+ }
+
+ if (!CheckError(connection, error)) {
+ mpd_connection_free(connection);
+ connection = nullptr;
+
+ return false;
+ }
+
+ idle_received = unsigned(-1);
+ is_idle = false;
+
+ SocketMonitor::Open(mpd_async_get_fd(mpd_connection_get_async(connection)));
+ IdleMonitor::Schedule();
+
+ return true;
+}
+
+bool
+ProxyDatabase::CheckConnection(Error &error)
+{
+ assert(connection != nullptr);
+
+ if (!mpd_connection_clear_error(connection)) {
+ Disconnect();
+ return Connect(error);
+ }
+
+ if (is_idle) {
+ unsigned idle = mpd_run_noidle(connection);
+ if (idle == 0 && !CheckError(connection, error)) {
+ Disconnect();
+ return false;
+ }
+
+ idle_received |= idle;
+ is_idle = false;
+ IdleMonitor::Schedule();
+ }
+
+ return true;
+}
+
+bool
+ProxyDatabase::EnsureConnected(Error &error)
+{
+ return connection != nullptr
+ ? CheckConnection(error)
+ : Connect(error);
+}
+
+void
+ProxyDatabase::Disconnect()
+{
+ assert(connection != nullptr);
+
+ IdleMonitor::Cancel();
+ SocketMonitor::Steal();
+
+ mpd_connection_free(connection);
+ connection = nullptr;
+}
+
+bool
+ProxyDatabase::OnSocketReady(gcc_unused unsigned flags)
+{
+ assert(connection != nullptr);
+
+ if (!is_idle) {
+ // TODO: can this happen?
+ IdleMonitor::Schedule();
+ return false;
+ }
+
+ unsigned idle = (unsigned)mpd_recv_idle(connection, false);
+ if (idle == 0) {
+ Error error;
+ if (!CheckError(connection, error)) {
+ LogError(error);
+ Disconnect();
+ return false;
+ }
+ }
+
+ /* let OnIdle() handle this */
+ idle_received |= idle;
+ is_idle = false;
+ IdleMonitor::Schedule();
+ return false;
+}
+
+void
+ProxyDatabase::OnIdle()
+{
+ assert(connection != nullptr);
+
+ /* handle previous idle events */
+
+ if (idle_received & MPD_IDLE_DATABASE)
+ listener.OnDatabaseModified();
+
+ idle_received = 0;
+
+ /* send a new idle command to the other MPD */
+
+ if (is_idle)
+ // TODO: can this happen?
+ return;
+
+ if (!mpd_send_idle_mask(connection, MPD_IDLE_DATABASE)) {
+ Error error;
+ if (!CheckError(connection, error))
+ LogError(error);
+
+ SocketMonitor::Steal();
+ mpd_connection_free(connection);
+ connection = nullptr;
+ return;
+ }
+
+ is_idle = true;
+ SocketMonitor::ScheduleRead();
+}
+
+const LightSong *
+ProxyDatabase::GetSong(const char *uri, Error &error) const
+{
+ // TODO: eliminate the const_cast
+ if (!const_cast<ProxyDatabase *>(this)->EnsureConnected(error))
+ return nullptr;
+
+ if (!mpd_send_list_meta(connection, uri)) {
+ CheckError(connection, error);
+ return nullptr;
+ }
+
+ struct mpd_song *song = mpd_recv_song(connection);
+ if (!mpd_response_finish(connection) &&
+ !CheckError(connection, error)) {
+ if (song != nullptr)
+ mpd_song_free(song);
+ return nullptr;
+ }
+
+ if (song == nullptr) {
+ error.Format(db_domain, DB_NOT_FOUND, "No such song: %s", uri);
+ return nullptr;
+ }
+
+ return new AllocatedProxySong(song);
+}
+
+void
+ProxyDatabase::ReturnSong(const LightSong *_song) const
+{
+ assert(_song != nullptr);
+
+ AllocatedProxySong *song = (AllocatedProxySong *)
+ const_cast<LightSong *>(_song);
+ delete song;
+}
+
+static bool
+Visit(struct mpd_connection *connection, const char *uri,
+ bool recursive, const SongFilter *filter,
+ VisitDirectory visit_directory, VisitSong visit_song,
+ VisitPlaylist visit_playlist, Error &error);
+
+static bool
+Visit(struct mpd_connection *connection,
+ bool recursive, const SongFilter *filter,
+ const struct mpd_directory *directory,
+ VisitDirectory visit_directory, VisitSong visit_song,
+ VisitPlaylist visit_playlist, Error &error)
+{
+ const char *path = mpd_directory_get_path(directory);
+#if LIBMPDCLIENT_CHECK_VERSION(2,9,0)
+ time_t mtime = mpd_directory_get_last_modified(directory);
+#else
+ time_t mtime = 0;
+#endif
+
+ if (visit_directory &&
+ !visit_directory(LightDirectory(path, mtime), error))
+ return false;
+
+ if (recursive &&
+ !Visit(connection, path, recursive, filter,
+ visit_directory, visit_song, visit_playlist, error))
+ return false;
+
+ return true;
+}
+
+gcc_pure
+static bool
+Match(const SongFilter *filter, const LightSong &song)
+{
+ return filter == nullptr || filter->Match(song);
+}
+
+static bool
+Visit(const SongFilter *filter,
+ const mpd_song *_song,
+ VisitSong visit_song, Error &error)
+{
+ if (!visit_song)
+ return true;
+
+ const ProxySong song(_song);
+ return !Match(filter, song) || visit_song(song, error);
+}
+
+static bool
+Visit(const struct mpd_playlist *playlist,
+ VisitPlaylist visit_playlist, Error &error)
+{
+ if (!visit_playlist)
+ return true;
+
+ PlaylistInfo p(mpd_playlist_get_path(playlist),
+ mpd_playlist_get_last_modified(playlist));
+
+ return visit_playlist(p, LightDirectory::Root(), error);
+}
+
+class ProxyEntity {
+ struct mpd_entity *entity;
+
+public:
+ explicit ProxyEntity(struct mpd_entity *_entity)
+ :entity(_entity) {}
+
+ ProxyEntity(const ProxyEntity &other) = delete;
+
+ ProxyEntity(ProxyEntity &&other)
+ :entity(other.entity) {
+ other.entity = nullptr;
+ }
+
+ ~ProxyEntity() {
+ if (entity != nullptr)
+ mpd_entity_free(entity);
+ }
+
+ ProxyEntity &operator=(const ProxyEntity &other) = delete;
+
+ operator const struct mpd_entity *() const {
+ return entity;
+ }
+};
+
+static std::list<ProxyEntity>
+ReceiveEntities(struct mpd_connection *connection)
+{
+ std::list<ProxyEntity> entities;
+ struct mpd_entity *entity;
+ while ((entity = mpd_recv_entity(connection)) != nullptr)
+ entities.push_back(ProxyEntity(entity));
+
+ mpd_response_finish(connection);
+ return entities;
+}
+
+static bool
+Visit(struct mpd_connection *connection, const char *uri,
+ bool recursive, const SongFilter *filter,
+ VisitDirectory visit_directory, VisitSong visit_song,
+ VisitPlaylist visit_playlist, Error &error)
+{
+ if (!mpd_send_list_meta(connection, uri))
+ return CheckError(connection, error);
+
+ std::list<ProxyEntity> entities(ReceiveEntities(connection));
+ if (!CheckError(connection, error))
+ return false;
+
+ for (const auto &entity : entities) {
+ switch (mpd_entity_get_type(entity)) {
+ case MPD_ENTITY_TYPE_UNKNOWN:
+ break;
+
+ case MPD_ENTITY_TYPE_DIRECTORY:
+ if (!Visit(connection, recursive, filter,
+ mpd_entity_get_directory(entity),
+ visit_directory, visit_song, visit_playlist,
+ error))
+ return false;
+ break;
+
+ case MPD_ENTITY_TYPE_SONG:
+ if (!Visit(filter,
+ mpd_entity_get_song(entity), visit_song,
+ error))
+ return false;
+ break;
+
+ case MPD_ENTITY_TYPE_PLAYLIST:
+ if (!Visit(mpd_entity_get_playlist(entity),
+ visit_playlist, error))
+ return false;
+ break;
+ }
+ }
+
+ return CheckError(connection, error);
+}
+
+static bool
+SearchSongs(struct mpd_connection *connection,
+ const DatabaseSelection &selection,
+ VisitSong visit_song,
+ Error &error)
+{
+ assert(selection.recursive);
+ assert(visit_song);
+
+ const bool exact = selection.filter == nullptr ||
+ !selection.filter->HasFoldCase();
+
+ if (!mpd_search_db_songs(connection, exact) ||
+ !SendConstraints(connection, selection) ||
+ !mpd_search_commit(connection))
+ return CheckError(connection, error);
+
+ bool result = true;
+ struct mpd_song *song;
+ while (result && (song = mpd_recv_song(connection)) != nullptr) {
+ AllocatedProxySong song2(song);
+
+ result = !Match(selection.filter, song2) ||
+ visit_song(song2, error);
+ }
+
+ mpd_response_finish(connection);
+ return result && CheckError(connection, error);
+}
+
+/**
+ * Check whether we can use the "base" constraint. Requires
+ * libmpdclient 2.9 and MPD 0.18.
+ */
+gcc_pure
+static bool
+ServerSupportsSearchBase(const struct mpd_connection *connection)
+{
+#if LIBMPDCLIENT_CHECK_VERSION(2,9,0)
+ return mpd_connection_cmp_server_version(connection, 0, 18, 0) >= 0;
+#else
+ (void)connection;
+
+ return false;
+#endif
+}
+
+bool
+ProxyDatabase::Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ // TODO: eliminate the const_cast
+ if (!const_cast<ProxyDatabase *>(this)->EnsureConnected(error))
+ return nullptr;
+
+ if (!visit_directory && !visit_playlist && selection.recursive &&
+ (ServerSupportsSearchBase(connection)
+ ? !selection.IsEmpty()
+ : selection.HasOtherThanBase()))
+ /* this optimized code path can only be used under
+ certain conditions */
+ return ::SearchSongs(connection, selection, visit_song, error);
+
+ /* fall back to recursive walk (slow!) */
+ return ::Visit(connection, selection.uri.c_str(),
+ selection.recursive, selection.filter,
+ visit_directory, visit_song, visit_playlist,
+ error);
+}
+
+bool
+ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type,
+ gcc_unused uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const
+{
+ // TODO: eliminate the const_cast
+ if (!const_cast<ProxyDatabase *>(this)->EnsureConnected(error))
+ return nullptr;
+
+ enum mpd_tag_type tag_type2 = Convert(tag_type);
+ if (tag_type2 == MPD_TAG_COUNT) {
+ error.Set(libmpdclient_domain, "Unsupported tag");
+ return false;
+ }
+
+ if (!mpd_search_db_tags(connection, tag_type2))
+ return CheckError(connection, error);
+
+ if (!SendConstraints(connection, selection))
+ return CheckError(connection, error);
+
+ // TODO: use group_mask
+
+ if (!mpd_search_commit(connection))
+ return CheckError(connection, error);
+
+ bool result = true;
+
+ struct mpd_pair *pair;
+ while (result &&
+ (pair = mpd_recv_pair_tag(connection, tag_type2)) != nullptr) {
+ TagBuilder tag;
+ tag.AddItem(tag_type, pair->value);
+ result = visit_tag(tag.Commit(), error);
+ mpd_return_pair(connection, pair);
+ }
+
+ return mpd_response_finish(connection) &&
+ CheckError(connection, error) &&
+ result;
+}
+
+bool
+ProxyDatabase::GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats, Error &error) const
+{
+ // TODO: match
+ (void)selection;
+
+ // TODO: eliminate the const_cast
+ if (!const_cast<ProxyDatabase *>(this)->EnsureConnected(error))
+ return nullptr;
+
+ struct mpd_stats *stats2 =
+ mpd_run_stats(connection);
+ if (stats2 == nullptr)
+ return CheckError(connection, error);
+
+ update_stamp = (time_t)mpd_stats_get_db_update_time(stats2);
+
+ stats.song_count = mpd_stats_get_number_of_songs(stats2);
+ stats.total_duration = mpd_stats_get_db_play_time(stats2);
+ stats.artist_count = mpd_stats_get_number_of_artists(stats2);
+ stats.album_count = mpd_stats_get_number_of_albums(stats2);
+ mpd_stats_free(stats2);
+
+ return true;
+}
+
+const DatabasePlugin proxy_db_plugin = {
+ "proxy",
+ DatabasePlugin::FLAG_REQUIRE_STORAGE,
+ ProxyDatabase::Create,
+};
diff --git a/src/db/plugins/ProxyDatabasePlugin.hxx b/src/db/plugins/ProxyDatabasePlugin.hxx
new file mode 100644
index 000000000..699d374b5
--- /dev/null
+++ b/src/db/plugins/ProxyDatabasePlugin.hxx
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_PROXY_DATABASE_PLUGIN_HXX
+#define MPD_PROXY_DATABASE_PLUGIN_HXX
+
+struct DatabasePlugin;
+
+extern const DatabasePlugin proxy_db_plugin;
+
+#endif
diff --git a/src/db/plugins/simple/DatabaseSave.cxx b/src/db/plugins/simple/DatabaseSave.cxx
new file mode 100644
index 000000000..62034a0b8
--- /dev/null
+++ b/src/db/plugins/simple/DatabaseSave.cxx
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "DatabaseSave.hxx"
+#include "db/DatabaseLock.hxx"
+#include "db/DatabaseError.hxx"
+#include "Directory.hxx"
+#include "DirectorySave.hxx"
+#include "fs/TextFile.hxx"
+#include "tag/Tag.hxx"
+#include "tag/TagSettings.h"
+#include "fs/Charset.hxx"
+#include "util/StringUtil.hxx"
+#include "util/Error.hxx"
+#include "Log.hxx"
+
+#include <string.h>
+#include <stdlib.h>
+
+#define DIRECTORY_INFO_BEGIN "info_begin"
+#define DIRECTORY_INFO_END "info_end"
+#define DB_FORMAT_PREFIX "format: "
+#define DIRECTORY_MPD_VERSION "mpd_version: "
+#define DIRECTORY_FS_CHARSET "fs_charset: "
+#define DB_TAG_PREFIX "tag: "
+
+static constexpr unsigned DB_FORMAT = 2;
+
+/**
+ * The oldest database format understood by this MPD version.
+ */
+static constexpr unsigned OLDEST_DB_FORMAT = 1;
+
+void
+db_save_internal(FILE *fp, const Directory &music_root)
+{
+ fprintf(fp, "%s\n", DIRECTORY_INFO_BEGIN);
+ fprintf(fp, DB_FORMAT_PREFIX "%u\n", DB_FORMAT);
+ fprintf(fp, "%s%s\n", DIRECTORY_MPD_VERSION, VERSION);
+ fprintf(fp, "%s%s\n", DIRECTORY_FS_CHARSET, GetFSCharset());
+
+ for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i)
+ if (!ignore_tag_items[i])
+ fprintf(fp, DB_TAG_PREFIX "%s\n", tag_item_names[i]);
+
+ fprintf(fp, "%s\n", DIRECTORY_INFO_END);
+
+ directory_save(fp, music_root);
+}
+
+bool
+db_load_internal(TextFile &file, Directory &music_root, Error &error)
+{
+ char *line;
+ unsigned format = 0;
+ bool found_charset = false, found_version = false;
+ bool success;
+ bool tags[TAG_NUM_OF_ITEM_TYPES];
+
+ /* get initial info */
+ line = file.ReadLine();
+ if (line == nullptr || strcmp(DIRECTORY_INFO_BEGIN, line) != 0) {
+ error.Set(db_domain, "Database corrupted");
+ return false;
+ }
+
+ memset(tags, false, sizeof(tags));
+
+ while ((line = file.ReadLine()) != nullptr &&
+ strcmp(line, DIRECTORY_INFO_END) != 0) {
+ if (StringStartsWith(line, DB_FORMAT_PREFIX)) {
+ format = atoi(line + sizeof(DB_FORMAT_PREFIX) - 1);
+ } else if (StringStartsWith(line, DIRECTORY_MPD_VERSION)) {
+ if (found_version) {
+ error.Set(db_domain, "Duplicate version line");
+ return false;
+ }
+
+ found_version = true;
+ } else if (StringStartsWith(line, DIRECTORY_FS_CHARSET)) {
+ const char *new_charset;
+
+ if (found_charset) {
+ error.Set(db_domain, "Duplicate charset line");
+ return false;
+ }
+
+ found_charset = true;
+
+ new_charset = line + sizeof(DIRECTORY_FS_CHARSET) - 1;
+ const char *const old_charset = GetFSCharset();
+ if (*old_charset != 0
+ && strcmp(new_charset, old_charset) != 0) {
+ error.Format(db_domain,
+ "Existing database has charset "
+ "\"%s\" instead of \"%s\"; "
+ "discarding database file",
+ new_charset, old_charset);
+ return false;
+ }
+ } else if (StringStartsWith(line, DB_TAG_PREFIX)) {
+ const char *name = line + sizeof(DB_TAG_PREFIX) - 1;
+ TagType tag = tag_name_parse(name);
+ if (tag == TAG_NUM_OF_ITEM_TYPES) {
+ error.Format(db_domain,
+ "Unrecognized tag '%s', "
+ "discarding database file",
+ name);
+ return false;
+ }
+
+ tags[tag] = true;
+ } else {
+ error.Format(db_domain, "Malformed line: %s", line);
+ return false;
+ }
+ }
+
+ if (format < OLDEST_DB_FORMAT || format > DB_FORMAT) {
+ error.Set(db_domain,
+ "Database format mismatch, "
+ "discarding database file");
+ return false;
+ }
+
+ for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) {
+ if (!ignore_tag_items[i] && !tags[i]) {
+ error.Set(db_domain,
+ "Tag list mismatch, "
+ "discarding database file");
+ return false;
+ }
+ }
+
+ LogDebug(db_domain, "reading DB");
+
+ db_lock();
+ success = directory_load(file, music_root, error);
+ db_unlock();
+
+ return success;
+}
diff --git a/src/db/plugins/simple/DatabaseSave.hxx b/src/db/plugins/simple/DatabaseSave.hxx
new file mode 100644
index 000000000..3bd3377ae
--- /dev/null
+++ b/src/db/plugins/simple/DatabaseSave.hxx
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_DATABASE_SAVE_HXX
+#define MPD_DATABASE_SAVE_HXX
+
+#include <stdio.h>
+
+struct Directory;
+class TextFile;
+class Error;
+
+void
+db_save_internal(FILE *file, const Directory &root);
+
+bool
+db_load_internal(TextFile &file, Directory &root, Error &error);
+
+#endif
diff --git a/src/db/plugins/simple/Directory.cxx b/src/db/plugins/simple/Directory.cxx
new file mode 100644
index 000000000..218652b03
--- /dev/null
+++ b/src/db/plugins/simple/Directory.cxx
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Directory.hxx"
+#include "SongSort.hxx"
+#include "Song.hxx"
+#include "Mount.hxx"
+#include "db/LightDirectory.hxx"
+#include "db/LightSong.hxx"
+#include "db/Uri.hxx"
+#include "db/DatabaseLock.hxx"
+#include "db/Interface.hxx"
+#include "SongFilter.hxx"
+#include "lib/icu/Collate.hxx"
+#include "fs/Traits.hxx"
+#include "util/Alloc.hxx"
+#include "util/Error.hxx"
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+
+Directory::Directory(std::string &&_path_utf8, Directory *_parent)
+ :parent(_parent),
+ mtime(0),
+ inode(0), device(0),
+ path(std::move(_path_utf8)),
+ mounted_database(nullptr)
+{
+}
+
+Directory::~Directory()
+{
+ delete mounted_database;
+
+ songs.clear_and_dispose(Song::Disposer());
+ children.clear_and_dispose(Disposer());
+}
+
+void
+Directory::Delete()
+{
+ assert(holding_db_lock());
+ assert(parent != nullptr);
+
+ parent->children.erase_and_dispose(parent->children.iterator_to(*this),
+ Disposer());
+}
+
+const char *
+Directory::GetName() const
+{
+ assert(!IsRoot());
+
+ return PathTraitsUTF8::GetBase(path.c_str());
+}
+
+Directory *
+Directory::CreateChild(const char *name_utf8)
+{
+ assert(holding_db_lock());
+ assert(name_utf8 != nullptr);
+ assert(*name_utf8 != 0);
+
+ std::string path_utf8 = IsRoot()
+ ? std::string(name_utf8)
+ : PathTraitsUTF8::Build(GetPath(), name_utf8);
+
+ Directory *child = new Directory(std::move(path_utf8), this);
+ children.push_back(*child);
+ return child;
+}
+
+const Directory *
+Directory::FindChild(const char *name) const
+{
+ assert(holding_db_lock());
+
+ for (const auto &child : children)
+ if (strcmp(child.GetName(), name) == 0)
+ return &child;
+
+ return nullptr;
+}
+
+void
+Directory::PruneEmpty()
+{
+ assert(holding_db_lock());
+
+ for (auto child = children.begin(), end = children.end();
+ child != end;) {
+ child->PruneEmpty();
+
+ if (child->IsEmpty())
+ child = children.erase_and_dispose(child, Disposer());
+ else
+ ++child;
+ }
+}
+
+Directory::LookupResult
+Directory::LookupDirectory(const char *uri)
+{
+ assert(holding_db_lock());
+ assert(uri != nullptr);
+
+ if (isRootDirectory(uri))
+ return { this, nullptr };
+
+ char *duplicated = xstrdup(uri), *name = duplicated;
+
+ Directory *d = this;
+ while (true) {
+ char *slash = strchr(name, '/');
+ if (slash == name)
+ break;
+
+ if (slash != nullptr)
+ *slash = '\0';
+
+ Directory *tmp = d->FindChild(name);
+ if (tmp == nullptr)
+ /* not found */
+ break;
+
+ d = tmp;
+
+ if (slash == nullptr) {
+ /* found everything */
+ name = nullptr;
+ break;
+ }
+
+ name = slash + 1;
+ }
+
+ free(duplicated);
+
+ const char *rest = name == nullptr
+ ? nullptr
+ : uri + (name - duplicated);
+
+ return { d, rest };
+}
+
+void
+Directory::AddSong(Song *song)
+{
+ assert(holding_db_lock());
+ assert(song != nullptr);
+ assert(song->parent == this);
+
+ songs.push_back(*song);
+}
+
+void
+Directory::RemoveSong(Song *song)
+{
+ assert(holding_db_lock());
+ assert(song != nullptr);
+ assert(song->parent == this);
+
+ songs.erase(songs.iterator_to(*song));
+}
+
+const Song *
+Directory::FindSong(const char *name_utf8) const
+{
+ assert(holding_db_lock());
+ assert(name_utf8 != nullptr);
+
+ for (auto &song : songs) {
+ assert(song.parent == this);
+
+ if (strcmp(song.uri, name_utf8) == 0)
+ return &song;
+ }
+
+ return nullptr;
+}
+
+gcc_pure
+static bool
+directory_cmp(const Directory &a, const Directory &b)
+{
+ return IcuCollate(a.path.c_str(), b.path.c_str()) < 0;
+}
+
+void
+Directory::Sort()
+{
+ assert(holding_db_lock());
+
+ children.sort(directory_cmp);
+ song_list_sort(songs);
+
+ for (auto &child : children)
+ child.Sort();
+}
+
+bool
+Directory::Walk(bool recursive, const SongFilter *filter,
+ VisitDirectory visit_directory, VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ assert(!error.IsDefined());
+
+ if (IsMount()) {
+ assert(IsEmpty());
+
+ /* TODO: eliminate this unlock/lock; it is necessary
+ because the child's SimpleDatabasePlugin::Visit()
+ call will lock it again */
+ db_unlock();
+ bool result = WalkMount(GetPath(), *mounted_database,
+ recursive, filter,
+ visit_directory, visit_song,
+ visit_playlist,
+ error);
+ db_lock();
+ return result;
+ }
+
+ if (visit_song) {
+ for (auto &song : songs){
+ const LightSong song2 = song.Export();
+ if ((filter == nullptr || filter->Match(song2)) &&
+ !visit_song(song2, error))
+ return false;
+ }
+ }
+
+ if (visit_playlist) {
+ for (const PlaylistInfo &p : playlists)
+ if (!visit_playlist(p, Export(), error))
+ return false;
+ }
+
+ for (auto &child : children) {
+ if (visit_directory &&
+ !visit_directory(child.Export(), error))
+ return false;
+
+ if (recursive &&
+ !child.Walk(recursive, filter,
+ visit_directory, visit_song, visit_playlist,
+ error))
+ return false;
+ }
+
+ return true;
+}
+
+LightDirectory
+Directory::Export() const
+{
+ return LightDirectory(GetPath(), mtime);
+}
diff --git a/src/db/plugins/simple/Directory.hxx b/src/db/plugins/simple/Directory.hxx
new file mode 100644
index 000000000..acef62143
--- /dev/null
+++ b/src/db/plugins/simple/Directory.hxx
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_DIRECTORY_HXX
+#define MPD_DIRECTORY_HXX
+
+#include "check.h"
+#include "Compiler.h"
+#include "db/Visitor.hxx"
+#include "db/PlaylistVector.hxx"
+#include "Song.hxx"
+
+#include <boost/intrusive/list.hpp>
+
+#include <string>
+
+/**
+ * Virtual directory that is really an archive file or a folder inside
+ * the archive (special value for Directory::device).
+ */
+static constexpr unsigned DEVICE_INARCHIVE = -1;
+
+/**
+ * Virtual directory that is really a song file with one or more "sub"
+ * songs as specified by DecoderPlugin::container_scan() (special
+ * value for Directory::device).
+ */
+static constexpr unsigned DEVICE_CONTAINER = -2;
+
+struct db_visitor;
+class SongFilter;
+class Error;
+class Database;
+
+struct Directory {
+ static constexpr auto link_mode = boost::intrusive::normal_link;
+ typedef boost::intrusive::link_mode<link_mode> LinkMode;
+ typedef boost::intrusive::list_member_hook<LinkMode> Hook;
+
+ struct Disposer {
+ void operator()(Directory *directory) const {
+ delete directory;
+ }
+ };
+
+ /**
+ * Pointers to the siblings of this directory within the
+ * parent directory. It is unused (undefined) in the root
+ * directory.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ Hook siblings;
+
+ typedef boost::intrusive::member_hook<Directory, Hook,
+ &Directory::siblings> SiblingsHook;
+ typedef boost::intrusive::list<Directory, SiblingsHook,
+ boost::intrusive::constant_time_size<false>> List;
+
+ /**
+ * A doubly linked list of child directories.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ List children;
+
+ /**
+ * A doubly linked list of songs within this directory.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ SongList songs;
+
+ PlaylistVector playlists;
+
+ Directory *parent;
+ time_t mtime;
+ unsigned inode, device;
+
+ std::string path;
+
+ /**
+ * If this is not nullptr, then this directory does not really
+ * exist, but is a mount point for another #Database.
+ */
+ Database *mounted_database;
+
+public:
+ Directory(std::string &&_path_utf8, Directory *_parent);
+ ~Directory();
+
+ /**
+ * Create a new root #Directory object.
+ */
+ gcc_malloc
+ static Directory *NewRoot() {
+ return new Directory(std::string(), nullptr);
+ }
+
+ bool IsMount() const {
+ return mounted_database != nullptr;
+ }
+
+ /**
+ * Remove this #Directory object from its parent and free it. This
+ * must not be called with the root Directory.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ void Delete();
+
+ /**
+ * Create a new #Directory object as a child of the given one.
+ *
+ * Caller must lock the #db_mutex.
+ *
+ * @param name_utf8 the UTF-8 encoded name of the new sub directory
+ */
+ gcc_malloc
+ Directory *CreateChild(const char *name_utf8);
+
+ /**
+ * Caller must lock the #db_mutex.
+ */
+ gcc_pure
+ const Directory *FindChild(const char *name) const;
+
+ gcc_pure
+ Directory *FindChild(const char *name) {
+ const Directory *cthis = this;
+ return const_cast<Directory *>(cthis->FindChild(name));
+ }
+
+ /**
+ * Look up a sub directory, and create the object if it does not
+ * exist.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ Directory *MakeChild(const char *name_utf8) {
+ Directory *child = FindChild(name_utf8);
+ if (child == nullptr)
+ child = CreateChild(name_utf8);
+ return child;
+ }
+
+ struct LookupResult {
+ /**
+ * The last directory that was found. If the given
+ * URI could not be resolved at all, then this is the
+ * root directory.
+ */
+ Directory *directory;
+
+ /**
+ * The remaining URI part (without leading slash) or
+ * nullptr if the given URI was consumed completely.
+ */
+ const char *uri;
+ };
+
+ /**
+ * Looks up a directory by its relative URI.
+ *
+ * @param uri the relative URI
+ * @return the Directory, or nullptr if none was found
+ */
+ gcc_pure
+ LookupResult LookupDirectory(const char *uri);
+
+ gcc_pure
+ bool IsEmpty() const {
+ return children.empty() &&
+ songs.empty() &&
+ playlists.empty();
+ }
+
+ gcc_pure
+ const char *GetPath() const {
+ return path.c_str();
+ }
+
+ /**
+ * Returns the base name of the directory.
+ */
+ gcc_pure
+ const char *GetName() const;
+
+ /**
+ * Is this the root directory of the music database?
+ */
+ gcc_pure
+ bool IsRoot() const {
+ return parent == nullptr;
+ }
+
+ template<typename T>
+ void ForEachChildSafe(T &&t) {
+ const auto end = children.end();
+ for (auto i = children.begin(), next = i; i != end; i = next) {
+ next = std::next(i);
+ t(*i);
+ }
+ }
+
+ template<typename T>
+ void ForEachSongSafe(T &&t) {
+ const auto end = songs.end();
+ for (auto i = songs.begin(), next = i; i != end; i = next) {
+ next = std::next(i);
+ t(*i);
+ }
+ }
+
+ /**
+ * Look up a song in this directory by its name.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ gcc_pure
+ const Song *FindSong(const char *name_utf8) const;
+
+ gcc_pure
+ Song *FindSong(const char *name_utf8) {
+ const Directory *cthis = this;
+ return const_cast<Song *>(cthis->FindSong(name_utf8));
+ }
+
+ /**
+ * Add a song object to this directory. Its "parent" attribute must
+ * be set already.
+ */
+ void AddSong(Song *song);
+
+ /**
+ * Remove a song object from this directory (which effectively
+ * invalidates the song object, because the "parent" attribute becomes
+ * stale), but does not free it.
+ */
+ void RemoveSong(Song *song);
+
+ /**
+ * Caller must lock the #db_mutex.
+ */
+ void PruneEmpty();
+
+ /**
+ * Sort all directory entries recursively.
+ *
+ * Caller must lock the #db_mutex.
+ */
+ void Sort();
+
+ /**
+ * Caller must lock #db_mutex.
+ */
+ bool Walk(bool recursive, const SongFilter *match,
+ VisitDirectory visit_directory, VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const;
+
+ gcc_pure
+ LightDirectory Export() const;
+};
+
+#endif
diff --git a/src/db/plugins/simple/DirectorySave.cxx b/src/db/plugins/simple/DirectorySave.cxx
new file mode 100644
index 000000000..9d3ebbac2
--- /dev/null
+++ b/src/db/plugins/simple/DirectorySave.cxx
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "DirectorySave.hxx"
+#include "Directory.hxx"
+#include "Song.hxx"
+#include "SongSave.hxx"
+#include "DetachedSong.hxx"
+#include "PlaylistDatabase.hxx"
+#include "fs/TextFile.hxx"
+#include "util/StringUtil.hxx"
+#include "util/NumberParser.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+
+#include <stddef.h>
+#include <string.h>
+
+#define DIRECTORY_DIR "directory: "
+#define DIRECTORY_TYPE "type: "
+#define DIRECTORY_MTIME "mtime: "
+#define DIRECTORY_BEGIN "begin: "
+#define DIRECTORY_END "end: "
+
+static constexpr Domain directory_domain("directory");
+
+gcc_const
+static const char *
+DeviceToTypeString(unsigned device)
+{
+ switch (device) {
+ case DEVICE_INARCHIVE:
+ return "archive";
+
+ case DEVICE_CONTAINER:
+ return "container";
+
+ default:
+ return nullptr;
+ }
+}
+
+gcc_pure
+static unsigned
+ParseTypeString(const char *type)
+{
+ if (strcmp(type, "archive") == 0)
+ return DEVICE_INARCHIVE;
+ else if (strcmp(type, "container") == 0)
+ return DEVICE_CONTAINER;
+ else
+ return 0;
+}
+
+void
+directory_save(FILE *fp, const Directory &directory)
+{
+ if (!directory.IsRoot()) {
+ const char *type = DeviceToTypeString(directory.device);
+ if (type != nullptr)
+ fprintf(fp, DIRECTORY_TYPE "%s\n", type);
+
+ if (directory.mtime != 0)
+ fprintf(fp, DIRECTORY_MTIME "%lu\n",
+ (unsigned long)directory.mtime);
+
+ fprintf(fp, "%s%s\n", DIRECTORY_BEGIN, directory.GetPath());
+ }
+
+ for (const auto &child : directory.children) {
+ fprintf(fp, DIRECTORY_DIR "%s\n", child.GetName());
+
+ if (!child.IsMount())
+ directory_save(fp, child);
+
+ if (ferror(fp))
+ return;
+ }
+
+ for (const auto &song : directory.songs)
+ song_save(fp, song);
+
+ playlist_vector_save(fp, directory.playlists);
+
+ if (!directory.IsRoot())
+ fprintf(fp, DIRECTORY_END "%s\n", directory.GetPath());
+}
+
+static bool
+ParseLine(Directory &directory, const char *line)
+{
+ if (StringStartsWith(line, DIRECTORY_MTIME)) {
+ directory.mtime =
+ ParseUint64(line + sizeof(DIRECTORY_MTIME) - 1);
+ } else if (StringStartsWith(line, DIRECTORY_TYPE)) {
+ directory.device =
+ ParseTypeString(line + sizeof(DIRECTORY_TYPE) - 1);
+ } else
+ return false;
+
+ return true;
+}
+
+static Directory *
+directory_load_subdir(TextFile &file, Directory &parent, const char *name,
+ Error &error)
+{
+ bool success;
+
+ if (parent.FindChild(name) != nullptr) {
+ error.Format(directory_domain,
+ "Duplicate subdirectory '%s'", name);
+ return nullptr;
+ }
+
+ Directory *directory = parent.CreateChild(name);
+
+ while (true) {
+ const char *line = file.ReadLine();
+ if (line == nullptr) {
+ error.Set(directory_domain, "Unexpected end of file");
+ directory->Delete();
+ return nullptr;
+ }
+
+ if (StringStartsWith(line, DIRECTORY_BEGIN))
+ break;
+
+ if (!ParseLine(*directory, line)) {
+ error.Format(directory_domain,
+ "Malformed line: %s", line);
+ directory->Delete();
+ return nullptr;
+ }
+ }
+
+ success = directory_load(file, *directory, error);
+ if (!success) {
+ directory->Delete();
+ return nullptr;
+ }
+
+ return directory;
+}
+
+bool
+directory_load(TextFile &file, Directory &directory, Error &error)
+{
+ const char *line;
+
+ while ((line = file.ReadLine()) != nullptr &&
+ !StringStartsWith(line, DIRECTORY_END)) {
+ if (StringStartsWith(line, DIRECTORY_DIR)) {
+ Directory *subdir =
+ directory_load_subdir(file, directory,
+ line + sizeof(DIRECTORY_DIR) - 1,
+ error);
+ if (subdir == nullptr)
+ return false;
+ } else if (StringStartsWith(line, SONG_BEGIN)) {
+ const char *name = line + sizeof(SONG_BEGIN) - 1;
+
+ if (directory.FindSong(name) != nullptr) {
+ error.Format(directory_domain,
+ "Duplicate song '%s'", name);
+ return false;
+ }
+
+ DetachedSong *song = song_load(file, name, error);
+ if (song == nullptr)
+ return false;
+
+ directory.AddSong(Song::NewFrom(std::move(*song),
+ directory));
+ delete song;
+ } else if (StringStartsWith(line, PLAYLIST_META_BEGIN)) {
+ const char *name = line + sizeof(PLAYLIST_META_BEGIN) - 1;
+ if (!playlist_metadata_load(file, directory.playlists,
+ name, error))
+ return false;
+ } else {
+ error.Format(directory_domain,
+ "Malformed line: %s", line);
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/src/db/plugins/simple/DirectorySave.hxx b/src/db/plugins/simple/DirectorySave.hxx
new file mode 100644
index 000000000..07e9e158b
--- /dev/null
+++ b/src/db/plugins/simple/DirectorySave.hxx
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_DIRECTORY_SAVE_HXX
+#define MPD_DIRECTORY_SAVE_HXX
+
+#include <stdio.h>
+
+struct Directory;
+class TextFile;
+class Error;
+
+void
+directory_save(FILE *fp, const Directory &directory);
+
+bool
+directory_load(TextFile &file, Directory &directory, Error &error);
+
+#endif
diff --git a/src/db/plugins/simple/Mount.cxx b/src/db/plugins/simple/Mount.cxx
new file mode 100644
index 000000000..96c7bbb5c
--- /dev/null
+++ b/src/db/plugins/simple/Mount.cxx
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Mount.hxx"
+#include "PrefixedLightSong.hxx"
+#include "db/Selection.hxx"
+#include "db/LightDirectory.hxx"
+#include "db/LightSong.hxx"
+#include "db/Interface.hxx"
+#include "fs/Traits.hxx"
+#include "util/Error.hxx"
+
+#include <string>
+
+struct PrefixedLightDirectory : LightDirectory {
+ std::string buffer;
+
+ PrefixedLightDirectory(const LightDirectory &directory,
+ const char *base)
+ :LightDirectory(directory),
+ buffer(IsRoot()
+ ? std::string(base)
+ : PathTraitsUTF8::Build(base, uri)) {
+ uri = buffer.c_str();
+ }
+};
+
+static bool
+PrefixVisitDirectory(const char *base, const VisitDirectory &visit_directory,
+ const LightDirectory &directory, Error &error)
+{
+ return visit_directory(PrefixedLightDirectory(directory, base), error);
+}
+
+static bool
+PrefixVisitSong(const char *base, const VisitSong &visit_song,
+ const LightSong &song, Error &error)
+{
+ return visit_song(PrefixedLightSong(song, base), error);
+}
+
+static bool
+PrefixVisitPlaylist(const char *base, const VisitPlaylist &visit_playlist,
+ const PlaylistInfo &playlist,
+ const LightDirectory &directory,
+ Error &error)
+{
+ return visit_playlist(playlist,
+ PrefixedLightDirectory(directory, base),
+ error);
+}
+
+bool
+WalkMount(const char *base, const Database &db,
+ bool recursive, const SongFilter *filter,
+ const VisitDirectory &visit_directory, const VisitSong &visit_song,
+ const VisitPlaylist &visit_playlist,
+ Error &error)
+{
+ using namespace std::placeholders;
+
+ VisitDirectory vd;
+ if (visit_directory)
+ vd = std::bind(PrefixVisitDirectory,
+ base, std::ref(visit_directory), _1, _2);
+
+ VisitSong vs;
+ if (visit_song)
+ vs = std::bind(PrefixVisitSong,
+ base, std::ref(visit_song), _1, _2);
+
+ VisitPlaylist vp;
+ if (visit_playlist)
+ vp = std::bind(PrefixVisitPlaylist,
+ base, std::ref(visit_playlist), _1, _2, _3);
+
+ return db.Visit(DatabaseSelection("", recursive, filter),
+ vd, vs, vp, error);
+}
diff --git a/src/db/plugins/simple/Mount.hxx b/src/db/plugins/simple/Mount.hxx
new file mode 100644
index 000000000..a4690114c
--- /dev/null
+++ b/src/db/plugins/simple/Mount.hxx
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_DB_SIMPLE_MOUNT_HXX
+#define MPD_DB_SIMPLE_MOUNT_HXX
+
+#include "db/Visitor.hxx"
+
+class Database;
+class SongFilter;
+class Error;
+
+bool
+WalkMount(const char *base, const Database &db,
+ bool recursive, const SongFilter *filter,
+ const VisitDirectory &visit_directory, const VisitSong &visit_song,
+ const VisitPlaylist &visit_playlist,
+ Error &error);
+
+#endif
diff --git a/src/db/plugins/simple/PrefixedLightSong.hxx b/src/db/plugins/simple/PrefixedLightSong.hxx
new file mode 100644
index 000000000..3664de001
--- /dev/null
+++ b/src/db/plugins/simple/PrefixedLightSong.hxx
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_DB_SIMPLE_PREFIXED_LIGHT_SONG_HXX
+#define MPD_DB_SIMPLE_PREFIXED_LIGHT_SONG_HXX
+
+#include "check.h"
+#include "db/LightSong.hxx"
+#include "fs/Traits.hxx"
+
+#include <string>
+
+class PrefixedLightSong : public LightSong {
+ std::string buffer;
+
+public:
+ PrefixedLightSong(const LightSong &song, const char *base)
+ :LightSong(song),
+ buffer(PathTraitsUTF8::Build(base, GetURI().c_str())) {
+ uri = buffer.c_str();
+ directory = nullptr;
+ }
+};
+
+#endif
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
new file mode 100644
index 000000000..97d3e52df
--- /dev/null
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "SimpleDatabasePlugin.hxx"
+#include "PrefixedLightSong.hxx"
+#include "db/DatabasePlugin.hxx"
+#include "db/Selection.hxx"
+#include "db/Helpers.hxx"
+#include "db/UniqueTags.hxx"
+#include "db/LightDirectory.hxx"
+#include "Directory.hxx"
+#include "Song.hxx"
+#include "SongFilter.hxx"
+#include "DatabaseSave.hxx"
+#include "db/DatabaseLock.hxx"
+#include "db/DatabaseError.hxx"
+#include "fs/TextFile.hxx"
+#include "config/ConfigData.hxx"
+#include "fs/FileSystem.hxx"
+#include "util/CharUtil.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "Log.hxx"
+
+#include <errno.h>
+
+static constexpr Domain simple_db_domain("simple_db");
+
+inline SimpleDatabase::SimpleDatabase()
+ :Database(simple_db_plugin),
+ path(AllocatedPath::Null()),
+ cache_path(AllocatedPath::Null()),
+ prefixed_light_song(nullptr) {}
+
+inline SimpleDatabase::SimpleDatabase(AllocatedPath &&_path)
+ :Database(simple_db_plugin),
+ path(std::move(_path)),
+ path_utf8(path.ToUTF8()),
+ cache_path(AllocatedPath::Null()),
+ prefixed_light_song(nullptr) {
+}
+
+Database *
+SimpleDatabase::Create(gcc_unused EventLoop &loop,
+ gcc_unused DatabaseListener &listener,
+ const config_param &param, Error &error)
+{
+ SimpleDatabase *db = new SimpleDatabase();
+ if (!db->Configure(param, error)) {
+ delete db;
+ db = nullptr;
+ }
+
+ return db;
+}
+
+bool
+SimpleDatabase::Configure(const config_param &param, Error &error)
+{
+ path = param.GetBlockPath("path", error);
+ if (path.IsNull()) {
+ if (!error.IsDefined())
+ error.Set(simple_db_domain,
+ "No \"path\" parameter specified");
+ return false;
+ }
+
+ path_utf8 = path.ToUTF8();
+
+ cache_path = param.GetBlockPath("cache_directory", error);
+ if (path.IsNull() && error.IsDefined())
+ return false;
+
+ return true;
+}
+
+bool
+SimpleDatabase::Check(Error &error) const
+{
+ assert(!path.IsNull());
+
+ /* Check if the file exists */
+ if (!CheckAccess(path)) {
+ /* If the file doesn't exist, we can't check if we can write
+ * it, so we are going to try to get the directory path, and
+ * see if we can write a file in that */
+ const auto dirPath = path.GetDirectoryName();
+
+ /* Check that the parent part of the path is a directory */
+ struct stat st;
+ if (!StatFile(dirPath, st)) {
+ error.FormatErrno("Couldn't stat parent directory of db file "
+ "\"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ if (!S_ISDIR(st.st_mode)) {
+ error.Format(simple_db_domain,
+ "Couldn't create db file \"%s\" because the "
+ "parent path is not a directory",
+ path_utf8.c_str());
+ return false;
+ }
+
+#ifndef WIN32
+ /* Check if we can write to the directory */
+ if (!CheckAccess(dirPath, X_OK | W_OK)) {
+ const int e = errno;
+ const std::string dirPath_utf8 = dirPath.ToUTF8();
+ error.FormatErrno(e, "Can't create db file in \"%s\"",
+ dirPath_utf8.c_str());
+ return false;
+ }
+#endif
+ return true;
+ }
+
+ /* Path exists, now check if it's a regular file */
+ struct stat st;
+ if (!StatFile(path, st)) {
+ error.FormatErrno("Couldn't stat db file \"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ if (!S_ISREG(st.st_mode)) {
+ error.Format(simple_db_domain,
+ "db file \"%s\" is not a regular file",
+ path_utf8.c_str());
+ return false;
+ }
+
+#ifndef WIN32
+ /* And check that we can write to it */
+ if (!CheckAccess(path, R_OK | W_OK)) {
+ error.FormatErrno("Can't open db file \"%s\" for reading/writing",
+ path_utf8.c_str());
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+bool
+SimpleDatabase::Load(Error &error)
+{
+ assert(!path.IsNull());
+ assert(root != nullptr);
+
+ TextFile file(path);
+ if (file.HasFailed()) {
+ error.FormatErrno("Failed to open database file \"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ if (!db_load_internal(file, *root, error))
+ return false;
+
+ struct stat st;
+ if (StatFile(path, st))
+ mtime = st.st_mtime;
+
+ return true;
+}
+
+bool
+SimpleDatabase::Open(Error &error)
+{
+ assert(prefixed_light_song == nullptr);
+
+ root = Directory::NewRoot();
+ mtime = 0;
+
+#ifndef NDEBUG
+ borrowed_song_count = 0;
+#endif
+
+ if (!Load(error)) {
+ delete root;
+
+ LogError(error);
+ error.Clear();
+
+ if (!Check(error))
+ return false;
+
+ root = Directory::NewRoot();
+ }
+
+ return true;
+}
+
+void
+SimpleDatabase::Close()
+{
+ assert(root != nullptr);
+ assert(prefixed_light_song == nullptr);
+ assert(borrowed_song_count == 0);
+
+ delete root;
+}
+
+const LightSong *
+SimpleDatabase::GetSong(const char *uri, Error &error) const
+{
+ assert(root != nullptr);
+ assert(prefixed_light_song == nullptr);
+ assert(borrowed_song_count == 0);
+
+ db_lock();
+
+ auto r = root->LookupDirectory(uri);
+
+ if (r.directory->IsMount()) {
+ /* pass the request to the mounted database */
+ db_unlock();
+
+ const LightSong *song =
+ r.directory->mounted_database->GetSong(r.uri, error);
+ if (song == nullptr)
+ return nullptr;
+
+ prefixed_light_song =
+ new PrefixedLightSong(*song, r.directory->GetPath());
+ return prefixed_light_song;
+ }
+
+ if (r.uri == nullptr) {
+ /* it's a directory */
+ db_unlock();
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No such song: %s", uri);
+ return nullptr;
+ }
+
+ if (strchr(r.uri, '/') != nullptr) {
+ /* refers to a URI "below" the actual song */
+ db_unlock();
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No such song: %s", uri);
+ return nullptr;
+ }
+
+ const Song *song = r.directory->FindSong(r.uri);
+ db_unlock();
+ if (song == nullptr) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No such song: %s", uri);
+ return nullptr;
+ }
+
+ light_song = song->Export();
+
+#ifndef NDEBUG
+ ++borrowed_song_count;
+#endif
+
+ return &light_song;
+}
+
+void
+SimpleDatabase::ReturnSong(gcc_unused const LightSong *song) const
+{
+ assert(song != nullptr);
+ assert(song == &light_song || song == prefixed_light_song);
+
+ delete prefixed_light_song;
+ prefixed_light_song = nullptr;
+
+#ifndef NDEBUG
+ if (song == &light_song) {
+ assert(borrowed_song_count > 0);
+ --borrowed_song_count;
+ }
+#endif
+}
+
+bool
+SimpleDatabase::Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ ScopeDatabaseLock protect;
+
+ auto r = root->LookupDirectory(selection.uri.c_str());
+ if (r.uri == nullptr) {
+ /* it's a directory */
+
+ if (selection.recursive && visit_directory &&
+ !visit_directory(r.directory->Export(), error))
+ return false;
+
+ return r.directory->Walk(selection.recursive, selection.filter,
+ visit_directory, visit_song,
+ visit_playlist,
+ error);
+ }
+
+ if (strchr(r.uri, '/') == nullptr) {
+ if (visit_song) {
+ Song *song = r.directory->FindSong(r.uri);
+ if (song != nullptr) {
+ const LightSong song2 = song->Export();
+ return !selection.Match(song2) ||
+ visit_song(song2, error);
+ }
+ }
+ }
+
+ error.Set(db_domain, DB_NOT_FOUND, "No such directory");
+ return false;
+}
+
+bool
+SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const
+{
+ return ::VisitUniqueTags(*this, selection, tag_type, group_mask,
+ visit_tag,
+ error);
+}
+
+bool
+SimpleDatabase::GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats, Error &error) const
+{
+ return ::GetStats(*this, selection, stats, error);
+}
+
+bool
+SimpleDatabase::Save(Error &error)
+{
+ db_lock();
+
+ LogDebug(simple_db_domain, "removing empty directories from DB");
+ root->PruneEmpty();
+
+ LogDebug(simple_db_domain, "sorting DB");
+ root->Sort();
+
+ db_unlock();
+
+ LogDebug(simple_db_domain, "writing DB");
+
+ FILE *fp = FOpen(path, FOpenMode::WriteText);
+ if (!fp) {
+ error.FormatErrno("unable to write to db file \"%s\"",
+ path_utf8.c_str());
+ return false;
+ }
+
+ db_save_internal(fp, *root);
+
+ if (ferror(fp)) {
+ error.SetErrno("Failed to write to database file");
+ fclose(fp);
+ return false;
+ }
+
+ fclose(fp);
+
+ struct stat st;
+ if (StatFile(path, st))
+ mtime = st.st_mtime;
+
+ return true;
+}
+
+bool
+SimpleDatabase::Mount(const char *uri, Database *db, Error &error)
+{
+ assert(uri != nullptr);
+ assert(*uri != 0);
+ assert(db != nullptr);
+
+ ScopeDatabaseLock protect;
+
+ auto r = root->LookupDirectory(uri);
+ if (r.uri == nullptr) {
+ error.Format(db_domain, DB_CONFLICT,
+ "Already exists: %s", uri);
+ return nullptr;
+ }
+
+ if (strchr(r.uri, '/') != nullptr) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "Parent not found: %s", uri);
+ return nullptr;
+ }
+
+ Directory *mnt = r.directory->CreateChild(r.uri);
+ mnt->mounted_database = db;
+ return true;
+}
+
+static constexpr bool
+IsSafeChar(char ch)
+{
+ return IsAlphaNumericASCII(ch) || ch == '-' || ch == '_' || ch == '%';
+}
+
+static constexpr bool
+IsUnsafeChar(char ch)
+{
+ return !IsSafeChar(ch);
+}
+
+bool
+SimpleDatabase::Mount(const char *local_uri, const char *storage_uri,
+ Error &error)
+{
+ if (cache_path.IsNull()) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No 'cache_directory' configured");
+ return nullptr;
+ }
+
+ std::string name(storage_uri);
+ std::replace_if(name.begin(), name.end(), IsUnsafeChar, '_');
+
+ auto db = new SimpleDatabase(AllocatedPath::Build(cache_path,
+ name.c_str()));
+ if (!db->Open(error)) {
+ delete db;
+ return false;
+ }
+
+ // TODO: update the new database instance?
+
+ if (!Mount(local_uri, db, error)) {
+ db->Close();
+ delete db;
+ return false;
+ }
+
+ return true;
+}
+
+Database *
+SimpleDatabase::LockUmountSteal(const char *uri)
+{
+ ScopeDatabaseLock protect;
+
+ auto r = root->LookupDirectory(uri);
+ if (r.uri != nullptr || !r.directory->IsMount())
+ return nullptr;
+
+ Database *db = r.directory->mounted_database;
+ r.directory->mounted_database = nullptr;
+ r.directory->Delete();
+
+ return db;
+}
+
+bool
+SimpleDatabase::Unmount(const char *uri)
+{
+ Database *db = LockUmountSteal(uri);
+ if (db == nullptr)
+ return false;
+
+ db->Close();
+ delete db;
+ return true;
+}
+
+const DatabasePlugin simple_db_plugin = {
+ "simple",
+ DatabasePlugin::FLAG_REQUIRE_STORAGE,
+ SimpleDatabase::Create,
+};
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.hxx b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
new file mode 100644
index 000000000..e27b3d956
--- /dev/null
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_SIMPLE_DATABASE_PLUGIN_HXX
+#define MPD_SIMPLE_DATABASE_PLUGIN_HXX
+
+#include "db/Interface.hxx"
+#include "fs/AllocatedPath.hxx"
+#include "db/LightSong.hxx"
+#include "Compiler.h"
+
+#include <cassert>
+
+struct config_param;
+struct Directory;
+struct DatabasePlugin;
+class EventLoop;
+class DatabaseListener;
+class PrefixedLightSong;
+
+class SimpleDatabase : public Database {
+ AllocatedPath path;
+ std::string path_utf8;
+
+ /**
+ * The path where cache files for Mount() are located.
+ */
+ AllocatedPath cache_path;
+
+ Directory *root;
+
+ time_t mtime;
+
+ /**
+ * A buffer for GetSong() when prefixing the #LightSong
+ * instance from a mounted #Database.
+ */
+ mutable PrefixedLightSong *prefixed_light_song;
+
+ /**
+ * A buffer for GetSong().
+ */
+ mutable LightSong light_song;
+
+#ifndef NDEBUG
+ mutable unsigned borrowed_song_count;
+#endif
+
+ SimpleDatabase();
+
+ SimpleDatabase(AllocatedPath &&_path);
+
+public:
+ static Database *Create(EventLoop &loop, DatabaseListener &listener,
+ const config_param &param,
+ Error &error);
+
+ gcc_pure
+ Directory &GetRoot() {
+ assert(root != NULL);
+
+ return *root;
+ }
+
+ bool Save(Error &error);
+
+ /**
+ * Returns true if there is a valid database file on the disk.
+ */
+ bool FileExists() const {
+ return mtime > 0;
+ }
+
+ /**
+ * @param db the #Database to be mounted; must be "open"; on
+ * success, this object gains ownership of the given #Database
+ */
+ gcc_nonnull_all
+ bool Mount(const char *uri, Database *db, Error &error);
+
+ gcc_nonnull_all
+ bool Mount(const char *local_uri, const char *storage_uri,
+ Error &error);
+
+ gcc_nonnull_all
+ bool Unmount(const char *uri);
+
+ /* virtual methods from class Database */
+ virtual bool Open(Error &error) override;
+ virtual void Close() override;
+
+ virtual const LightSong *GetSong(const char *uri_utf8,
+ Error &error) const override;
+ virtual void ReturnSong(const LightSong *song) const;
+
+ virtual bool Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const override;
+
+ virtual bool VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const override;
+
+ virtual bool GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats,
+ Error &error) const override;
+
+ virtual time_t GetUpdateStamp() const override {
+ return mtime;
+ }
+
+private:
+ bool Configure(const config_param &param, Error &error);
+
+ gcc_pure
+ bool Check(Error &error) const;
+
+ bool Load(Error &error);
+
+ Database *LockUmountSteal(const char *uri);
+};
+
+extern const DatabasePlugin simple_db_plugin;
+
+#endif
diff --git a/src/db/plugins/simple/Song.cxx b/src/db/plugins/simple/Song.cxx
new file mode 100644
index 000000000..3bd3d8316
--- /dev/null
+++ b/src/db/plugins/simple/Song.cxx
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Song.hxx"
+#include "Directory.hxx"
+#include "tag/Tag.hxx"
+#include "util/VarSize.hxx"
+#include "DetachedSong.hxx"
+#include "db/LightSong.hxx"
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+
+inline Song::Song(const char *_uri, size_t uri_length, Directory &_parent)
+ :parent(&_parent), mtime(0), start_ms(0), end_ms(0)
+{
+ memcpy(uri, _uri, uri_length + 1);
+}
+
+inline Song::~Song()
+{
+}
+
+static Song *
+song_alloc(const char *uri, Directory &parent)
+{
+ size_t uri_length;
+
+ assert(uri);
+ uri_length = strlen(uri);
+ assert(uri_length);
+
+ return NewVarSize<Song>(sizeof(Song::uri),
+ uri_length + 1,
+ uri, uri_length, parent);
+}
+
+Song *
+Song::NewFrom(DetachedSong &&other, Directory &parent)
+{
+ Song *song = song_alloc(other.GetURI(), parent);
+ song->tag = std::move(other.WritableTag());
+ song->mtime = other.GetLastModified();
+ song->start_ms = other.GetStartMS();
+ song->end_ms = other.GetEndMS();
+ return song;
+}
+
+Song *
+Song::NewFile(const char *path, Directory &parent)
+{
+ return song_alloc(path, parent);
+}
+
+void
+Song::Free()
+{
+ DeleteVarSize(this);
+}
+
+std::string
+Song::GetURI() const
+{
+ assert(*uri);
+
+ if (parent->IsRoot())
+ return std::string(uri);
+ else {
+ const char *path = parent->GetPath();
+
+ std::string result;
+ result.reserve(strlen(path) + 1 + strlen(uri));
+ result.assign(path);
+ result.push_back('/');
+ result.append(uri);
+ return result;
+ }
+}
+
+LightSong
+Song::Export() const
+{
+ LightSong dest;
+ dest.directory = parent->IsRoot()
+ ? nullptr : parent->GetPath();
+ dest.uri = uri;
+ dest.real_uri = nullptr;
+ dest.tag = &tag;
+ dest.mtime = mtime;
+ dest.start_ms = start_ms;
+ dest.end_ms = end_ms;
+ return dest;
+}
diff --git a/src/db/plugins/simple/Song.hxx b/src/db/plugins/simple/Song.hxx
new file mode 100644
index 000000000..b2e85aa6b
--- /dev/null
+++ b/src/db/plugins/simple/Song.hxx
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_SONG_HXX
+#define MPD_SONG_HXX
+
+#include "tag/Tag.hxx"
+#include "Compiler.h"
+
+#include <boost/intrusive/list.hpp>
+
+#include <string>
+
+#include <assert.h>
+#include <time.h>
+
+struct LightSong;
+struct Directory;
+class DetachedSong;
+class Storage;
+
+/**
+ * A song file inside the configured music directory. Internal
+ * #SimpleDatabase class.
+ */
+struct Song {
+ static constexpr auto link_mode = boost::intrusive::normal_link;
+ typedef boost::intrusive::link_mode<link_mode> LinkMode;
+ typedef boost::intrusive::list_member_hook<LinkMode> Hook;
+
+ struct Disposer {
+ void operator()(Song *song) const {
+ song->Free();
+ }
+ };
+
+ /**
+ * Pointers to the siblings of this directory within the
+ * parent directory. It is unused (undefined) if this song is
+ * not in the database.
+ *
+ * This attribute is protected with the global #db_mutex.
+ * Read access in the update thread does not need protection.
+ */
+ Hook siblings;
+
+ Tag tag;
+
+ /**
+ * The #Directory that contains this song. Must be
+ * non-nullptr. directory this way.
+ */
+ Directory *const parent;
+
+ time_t mtime;
+
+ /**
+ * Start of this sub-song within the file in milliseconds.
+ */
+ unsigned start_ms;
+
+ /**
+ * End of this sub-song within the file in milliseconds.
+ * Unused if zero.
+ */
+ unsigned end_ms;
+
+ /**
+ * The file name.
+ */
+ char uri[sizeof(int)];
+
+ Song(const char *_uri, size_t uri_length, Directory &parent);
+ ~Song();
+
+ gcc_malloc
+ static Song *NewFrom(DetachedSong &&other, Directory &parent);
+
+ /** allocate a new song with a local file name */
+ gcc_malloc
+ static Song *NewFile(const char *path_utf8, Directory &parent);
+
+ /**
+ * allocate a new song structure with a local file name and attempt to
+ * load its metadata. If all decoder plugin fail to read its meta
+ * data, nullptr is returned.
+ */
+ gcc_malloc
+ static Song *LoadFile(Storage &storage, const char *name_utf8,
+ Directory &parent);
+
+ void Free();
+
+ bool UpdateFile(Storage &storage);
+ bool UpdateFileInArchive(const Storage &storage);
+
+ /**
+ * Returns the URI of the song in UTF-8 encoding, including its
+ * location within the music directory.
+ */
+ gcc_pure
+ std::string GetURI() const;
+
+ gcc_pure
+ LightSong Export() const;
+};
+
+typedef boost::intrusive::list<Song,
+ boost::intrusive::member_hook<Song, Song::Hook,
+ &Song::siblings>,
+ boost::intrusive::constant_time_size<false>> SongList;
+
+#endif
diff --git a/src/db/plugins/simple/SongSort.cxx b/src/db/plugins/simple/SongSort.cxx
new file mode 100644
index 000000000..4b7144937
--- /dev/null
+++ b/src/db/plugins/simple/SongSort.cxx
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "SongSort.hxx"
+#include "Song.hxx"
+#include "tag/Tag.hxx"
+#include "lib/icu/Collate.hxx"
+
+#include <stdlib.h>
+
+static int
+compare_utf8_string(const char *a, const char *b)
+{
+ if (a == nullptr)
+ return b == nullptr ? 0 : -1;
+
+ if (b == nullptr)
+ return 1;
+
+ return IcuCollate(a, b);
+}
+
+/**
+ * Compare two string tag values, ignoring case. Either one may be
+ * nullptr.
+ */
+static int
+compare_string_tag_item(const Tag &a, const Tag &b,
+ TagType type)
+{
+ return compare_utf8_string(a.GetValue(type),
+ b.GetValue(type));
+}
+
+/**
+ * Compare two tag values which should contain an integer value
+ * (e.g. disc or track number). Either one may be nullptr.
+ */
+static int
+compare_number_string(const char *a, const char *b)
+{
+ long ai = a == nullptr ? 0 : strtol(a, nullptr, 10);
+ long bi = b == nullptr ? 0 : strtol(b, nullptr, 10);
+
+ if (ai <= 0)
+ return bi <= 0 ? 0 : -1;
+
+ if (bi <= 0)
+ return 1;
+
+ return ai - bi;
+}
+
+static int
+compare_tag_item(const Tag &a, const Tag &b, TagType type)
+{
+ return compare_number_string(a.GetValue(type),
+ b.GetValue(type));
+}
+
+/* Only used for sorting/searchin a songvec, not general purpose compares */
+gcc_pure
+static bool
+song_cmp(const Song &a, const Song &b)
+{
+ int ret;
+
+ /* first sort by album */
+ ret = compare_string_tag_item(a.tag, b.tag, TAG_ALBUM);
+ if (ret != 0)
+ return ret < 0;
+
+ /* then sort by disc */
+ ret = compare_tag_item(a.tag, b.tag, TAG_DISC);
+ if (ret != 0)
+ return ret < 0;
+
+ /* then by track number */
+ ret = compare_tag_item(a.tag, b.tag, TAG_TRACK);
+ if (ret != 0)
+ return ret < 0;
+
+ /* still no difference? compare file name */
+ return IcuCollate(a.uri, b.uri) < 0;
+}
+
+void
+song_list_sort(SongList &songs)
+{
+ songs.sort(song_cmp);
+}
diff --git a/src/db/plugins/simple/SongSort.hxx b/src/db/plugins/simple/SongSort.hxx
new file mode 100644
index 000000000..2a0c4383b
--- /dev/null
+++ b/src/db/plugins/simple/SongSort.hxx
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_SONG_SORT_HXX
+#define MPD_SONG_SORT_HXX
+
+#include "Song.hxx"
+
+struct list_head;
+
+void
+song_list_sort(SongList &songs);
+
+#endif
diff --git a/src/db/plugins/upnp/ContentDirectoryService.cxx b/src/db/plugins/upnp/ContentDirectoryService.cxx
new file mode 100644
index 000000000..c097f7644
--- /dev/null
+++ b/src/db/plugins/upnp/ContentDirectoryService.cxx
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "lib/upnp/ContentDirectoryService.hxx"
+#include "lib/upnp/Domain.hxx"
+#include "lib/upnp/ixmlwrap.hxx"
+#include "lib/upnp/Action.hxx"
+#include "Directory.hxx"
+#include "util/NumberParser.hxx"
+#include "util/Error.hxx"
+
+#include <stdio.h>
+
+static bool
+ReadResultTag(UPnPDirContent &dirbuf, IXML_Document *response, Error &error)
+{
+ const char *p = ixmlwrap::getFirstElementValue(response, "Result");
+ if (p == nullptr)
+ p = "";
+
+ return dirbuf.parse(p, error);
+}
+
+inline bool
+ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl,
+ const char *objectId, unsigned offset,
+ unsigned count, UPnPDirContent &dirbuf,
+ unsigned &didreadp, unsigned &totalp,
+ Error &error) const
+{
+ // Create request
+ char ofbuf[100], cntbuf[100];
+ sprintf(ofbuf, "%u", offset);
+ sprintf(cntbuf, "%u", count);
+ // Some devices require an empty SortCriteria, else bad params
+ IXML_Document *request =
+ MakeActionHelper("Browse", m_serviceType.c_str(),
+ "ObjectID", objectId,
+ "BrowseFlag", "BrowseDirectChildren",
+ "Filter", "*",
+ "SortCriteria", "",
+ "StartingIndex", ofbuf,
+ "RequestedCount", cntbuf);
+ if (request == nullptr) {
+ error.Set(upnp_domain, "UpnpMakeAction() failed");
+ return false;
+ }
+
+ IXML_Document *response;
+ int code = UpnpSendAction(hdl, m_actionURL.c_str(), m_serviceType.c_str(),
+ 0 /*devUDN*/, request, &response);
+ ixmlDocument_free(request);
+ if (code != UPNP_E_SUCCESS) {
+ error.Format(upnp_domain, code,
+ "UpnpSendAction() failed: %s",
+ UpnpGetErrorMessage(code));
+ return false;
+ }
+
+ const char *value = ixmlwrap::getFirstElementValue(response, "NumberReturned");
+ didreadp = value != nullptr
+ ? ParseUnsigned(value)
+ : 0;
+
+ value = ixmlwrap::getFirstElementValue(response, "TotalMatches");
+ if (value != nullptr)
+ totalp = ParseUnsigned(value);
+
+ bool success = ReadResultTag(dirbuf, response, error);
+ ixmlDocument_free(response);
+ return success;
+}
+
+bool
+ContentDirectoryService::readDir(UpnpClient_Handle handle,
+ const char *objectId,
+ UPnPDirContent &dirbuf,
+ Error &error) const
+{
+ unsigned offset = 0, total = -1, count;
+
+ do {
+ if (!readDirSlice(handle, objectId, offset, m_rdreqcnt, dirbuf,
+ count, total, error))
+ return false;
+
+ offset += count;
+ } while (count > 0 && offset < total);
+
+ return true;
+}
+
+bool
+ContentDirectoryService::search(UpnpClient_Handle hdl,
+ const char *objectId,
+ const char *ss,
+ UPnPDirContent &dirbuf,
+ Error &error) const
+{
+ unsigned offset = 0, total = -1, count;
+
+ do {
+ char ofbuf[100];
+ sprintf(ofbuf, "%d", offset);
+
+ IXML_Document *request =
+ MakeActionHelper("Search", m_serviceType.c_str(),
+ "ContainerID", objectId,
+ "SearchCriteria", ss,
+ "Filter", "*",
+ "SortCriteria", "",
+ "StartingIndex", ofbuf,
+ "RequestedCount", "0"); // Setting a value here gets twonky into fits
+ if (request == 0) {
+ error.Set(upnp_domain, "UpnpMakeAction() failed");
+ return false;
+ }
+
+ IXML_Document *response;
+ auto code = UpnpSendAction(hdl, m_actionURL.c_str(),
+ m_serviceType.c_str(),
+ 0 /*devUDN*/, request, &response);
+ ixmlDocument_free(request);
+ if (code != UPNP_E_SUCCESS) {
+ error.Format(upnp_domain, code,
+ "UpnpSendAction() failed: %s",
+ UpnpGetErrorMessage(code));
+ return false;
+ }
+
+ const char *value =
+ ixmlwrap::getFirstElementValue(response, "NumberReturned");
+ count = value != nullptr
+ ? ParseUnsigned(value)
+ : 0;
+
+ offset += count;
+
+ value = ixmlwrap::getFirstElementValue(response, "TotalMatches");
+ if (value != nullptr)
+ total = ParseUnsigned(value);
+
+ bool success = ReadResultTag(dirbuf, response, error);
+ ixmlDocument_free(response);
+ if (!success)
+ return false;
+ } while (count > 0 && offset < total);
+
+ return true;
+}
+
+bool
+ContentDirectoryService::getMetadata(UpnpClient_Handle hdl,
+ const char *objectId,
+ UPnPDirContent &dirbuf,
+ Error &error) const
+{
+ // Create request
+ IXML_Document *request =
+ MakeActionHelper("Browse", m_serviceType.c_str(),
+ "ObjectID", objectId,
+ "BrowseFlag", "BrowseMetadata",
+ "Filter", "*",
+ "SortCriteria", "",
+ "StartingIndex", "0",
+ "RequestedCount", "1");
+ if (request == nullptr) {
+ error.Set(upnp_domain, "UpnpMakeAction() failed");
+ return false;
+ }
+
+ IXML_Document *response;
+ auto code = UpnpSendAction(hdl, m_actionURL.c_str(),
+ m_serviceType.c_str(),
+ 0 /*devUDN*/, request, &response);
+ ixmlDocument_free(request);
+ if (code != UPNP_E_SUCCESS) {
+ error.Format(upnp_domain, code,
+ "UpnpSendAction() failed: %s",
+ UpnpGetErrorMessage(code));
+ return false;
+ }
+
+ bool success = ReadResultTag(dirbuf, response, error);
+ ixmlDocument_free(response);
+ return success;
+}
diff --git a/src/db/plugins/upnp/Directory.cxx b/src/db/plugins/upnp/Directory.cxx
new file mode 100644
index 000000000..e43cd48a6
--- /dev/null
+++ b/src/db/plugins/upnp/Directory.cxx
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Directory.hxx"
+#include "lib/upnp/Util.hxx"
+#include "lib/expat/ExpatParser.hxx"
+#include "Tags.hxx"
+#include "tag/TagBuilder.hxx"
+#include "tag/TagTable.hxx"
+#include "util/NumberParser.hxx"
+
+#include <algorithm>
+#include <string>
+
+#include <string.h>
+
+UPnPDirContent::~UPnPDirContent()
+{
+ /* this destructor exists here just so it won't get inlined */
+}
+
+gcc_pure gcc_nonnull_all
+static bool
+CompareStringLiteral(const char *literal, const char *value, size_t length)
+{
+ return length == strlen(literal) &&
+ memcmp(literal, value, length) == 0;
+}
+
+gcc_pure
+static UPnPDirObject::ItemClass
+ParseItemClass(const char *name, size_t length)
+{
+ if (CompareStringLiteral("object.item.audioItem.musicTrack",
+ name, length))
+ return UPnPDirObject::ItemClass::MUSIC;
+ else if (CompareStringLiteral("object.item.playlistItem",
+ name, length))
+ return UPnPDirObject::ItemClass::PLAYLIST;
+ else
+ return UPnPDirObject::ItemClass::UNKNOWN;
+}
+
+gcc_pure
+static int
+ParseDuration(const char *duration)
+{
+ char *endptr;
+
+ unsigned result = ParseUnsigned(duration, &endptr);
+ if (endptr == duration || *endptr != ':')
+ return 0;
+
+ result *= 60;
+ duration = endptr + 1;
+ result += ParseUnsigned(duration, &endptr);
+ if (endptr == duration || *endptr != ':')
+ return 0;
+
+ result *= 60;
+ duration = endptr + 1;
+ result += ParseUnsigned(duration, &endptr);
+ if (endptr == duration || *endptr != 0)
+ return 0;
+
+ return result;
+}
+
+/**
+ * Transform titles to turn '/' into '_' to make them acceptable path
+ * elements. There is a very slight risk of collision in doing
+ * this. Twonky returns directory names (titles) like 'Artist/Album'.
+ */
+gcc_pure
+static std::string
+titleToPathElt(std::string &&s)
+{
+ std::replace(s.begin(), s.end(), '/', '_');
+ return s;
+}
+
+/**
+ * An XML parser which builds directory contents from DIDL lite input.
+ */
+class UPnPDirParser final : public CommonExpatParser {
+ UPnPDirContent &m_dir;
+
+ enum {
+ NONE,
+ RES,
+ CLASS,
+ } state;
+
+ /**
+ * If not equal to #TAG_NUM_OF_ITEM_TYPES, then we're
+ * currently reading an element containing a tag value. The
+ * value is being constructed in #value.
+ */
+ TagType tag_type;
+
+ /**
+ * The text inside the current element.
+ */
+ std::string value;
+
+ UPnPDirObject m_tobj;
+ TagBuilder tag;
+
+public:
+ UPnPDirParser(UPnPDirContent& dir)
+ :m_dir(dir),
+ state(NONE),
+ tag_type(TAG_NUM_OF_ITEM_TYPES)
+ {
+ }
+
+protected:
+ virtual void StartElement(const XML_Char *name, const XML_Char **attrs)
+ {
+ if (m_tobj.type != UPnPDirObject::Type::UNKNOWN &&
+ tag_type == TAG_NUM_OF_ITEM_TYPES) {
+ tag_type = tag_table_lookup(upnp_tags, name);
+ if (tag_type != TAG_NUM_OF_ITEM_TYPES)
+ return;
+ } else {
+ assert(tag_type == TAG_NUM_OF_ITEM_TYPES);
+ }
+
+ switch (name[0]) {
+ case 'c':
+ if (!strcmp(name, "container")) {
+ m_tobj.clear();
+ m_tobj.type = UPnPDirObject::Type::CONTAINER;
+
+ const char *id = GetAttribute(attrs, "id");
+ if (id != nullptr)
+ m_tobj.m_id = id;
+
+ const char *pid = GetAttribute(attrs, "parentID");
+ if (pid != nullptr)
+ m_tobj.m_pid = pid;
+ }
+ break;
+
+ case 'i':
+ if (!strcmp(name, "item")) {
+ m_tobj.clear();
+ m_tobj.type = UPnPDirObject::Type::ITEM;
+
+ const char *id = GetAttribute(attrs, "id");
+ if (id != nullptr)
+ m_tobj.m_id = id;
+
+ const char *pid = GetAttribute(attrs, "parentID");
+ if (pid != nullptr)
+ m_tobj.m_pid = pid;
+ }
+ break;
+
+ case 'r':
+ if (!strcmp(name, "res")) {
+ // <res protocolInfo="http-get:*:audio/mpeg:*" size="5171496"
+ // bitrate="24576" duration="00:03:35" sampleFrequency="44100"
+ // nrAudioChannels="2">
+
+ const char *duration =
+ GetAttribute(attrs, "duration");
+ if (duration != nullptr)
+ tag.SetTime(ParseDuration(duration));
+
+ state = RES;
+ }
+
+ break;
+
+ case 'u':
+ if (strcmp(name, "upnp:class") == 0)
+ state = CLASS;
+ }
+ }
+
+ bool checkobjok() {
+ if (m_tobj.m_id.empty() || m_tobj.m_pid.empty() ||
+ m_tobj.name.empty() ||
+ (m_tobj.type == UPnPDirObject::Type::ITEM &&
+ m_tobj.item_class == UPnPDirObject::ItemClass::UNKNOWN))
+ return false;
+
+ return true;
+ }
+
+ virtual void EndElement(const XML_Char *name)
+ {
+ if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
+ assert(m_tobj.type != UPnPDirObject::Type::UNKNOWN);
+
+ tag.AddItem(tag_type, value.c_str());
+
+ if (tag_type == TAG_TITLE)
+ m_tobj.name = titleToPathElt(std::move(value));
+
+ value.clear();
+ tag_type = TAG_NUM_OF_ITEM_TYPES;
+ return;
+ }
+
+ if ((!strcmp(name, "container") || !strcmp(name, "item")) &&
+ checkobjok()) {
+ tag.Commit(m_tobj.tag);
+ m_dir.objects.emplace_back(std::move(m_tobj));
+ }
+
+ state = NONE;
+ }
+
+ virtual void CharacterData(const XML_Char *s, int len)
+ {
+ if (tag_type != TAG_NUM_OF_ITEM_TYPES) {
+ assert(m_tobj.type != UPnPDirObject::Type::UNKNOWN);
+
+ value.append(s, len);
+ return;
+ }
+
+ switch (state) {
+ case NONE:
+ break;
+
+ case RES:
+ m_tobj.url.assign(s, len);
+ break;
+
+ case CLASS:
+ m_tobj.item_class = ParseItemClass(s, len);
+ break;
+ }
+ }
+};
+
+bool
+UPnPDirContent::parse(const char *input, Error &error)
+{
+ UPnPDirParser parser(*this);
+ return parser.Parse(input, strlen(input), true, error);
+}
diff --git a/src/db/plugins/upnp/Directory.hxx b/src/db/plugins/upnp/Directory.hxx
new file mode 100644
index 000000000..433979900
--- /dev/null
+++ b/src/db/plugins/upnp/Directory.hxx
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_UPNP_DIRECTORY_HXX
+#define MPD_UPNP_DIRECTORY_HXX
+
+#include "Object.hxx"
+#include "Compiler.h"
+
+#include <string>
+#include <vector>
+
+class Error;
+
+/**
+ * Image of a MediaServer Directory Service container (directory),
+ * possibly containing items and subordinate containers.
+ */
+class UPnPDirContent {
+public:
+ std::vector<UPnPDirObject> objects;
+
+ ~UPnPDirContent();
+
+ gcc_pure
+ UPnPDirObject *FindObject(const char *name) {
+ for (auto &o : objects)
+ if (o.name == name)
+ return &o;
+
+ return nullptr;
+ }
+
+ /**
+ * Parse from DIDL-Lite XML data.
+ *
+ * Normally only used by ContentDirectoryService::readDir()
+ * This is cumulative: in general, the XML data is obtained in
+ * several documents corresponding to (offset,count) slices of the
+ * directory (container). parse() can be called repeatedly with
+ * the successive XML documents and will accumulate entries in the item
+ * and container vectors. This makes more sense if the different
+ * chunks are from the same container, but given that UPnP Ids are
+ * actually global, nothing really bad will happen if you mix
+ * up...
+ */
+ bool parse(const char *didltext, Error &error);
+};
+
+#endif /* _UPNPDIRCONTENT_H_X_INCLUDED_ */
diff --git a/src/db/plugins/upnp/Object.cxx b/src/db/plugins/upnp/Object.cxx
new file mode 100644
index 000000000..703fb0be4
--- /dev/null
+++ b/src/db/plugins/upnp/Object.cxx
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "Object.hxx"
+
+UPnPDirObject::~UPnPDirObject()
+{
+ /* this destructor exists here just so it won't get inlined */
+}
diff --git a/src/db/plugins/upnp/Object.hxx b/src/db/plugins/upnp/Object.hxx
new file mode 100644
index 000000000..16a66c774
--- /dev/null
+++ b/src/db/plugins/upnp/Object.hxx
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_UPNP_OBJECT_HXX
+#define MPD_UPNP_OBJECT_HXX
+
+#include "tag/Tag.hxx"
+
+#include <string>
+
+/**
+ * UpnP Media Server directory entry, converted from XML data.
+ *
+ * This is a dumb data holder class, a struct with helpers.
+ */
+class UPnPDirObject {
+public:
+ enum class Type {
+ UNKNOWN,
+ ITEM,
+ CONTAINER,
+ };
+
+ // There are actually several kinds of containers:
+ // object.container.storageFolder, object.container.person,
+ // object.container.playlistContainer etc., but they all seem to
+ // behave the same as far as we're concerned. Otoh, musicTrack
+ // items are special to us, and so should playlists, but I've not
+ // seen one of the latter yet (servers seem to use containers for
+ // playlists).
+ enum class ItemClass {
+ UNKNOWN,
+ MUSIC,
+ PLAYLIST,
+ };
+
+ std::string m_id; // ObjectId
+ std::string m_pid; // Parent ObjectId
+ std::string url;
+
+ /**
+ * A copy of "dc:title" sanitized as a file name.
+ */
+ std::string name;
+
+ Type type;
+ ItemClass item_class;
+
+ Tag tag;
+
+ UPnPDirObject() = default;
+ UPnPDirObject(UPnPDirObject &&) = default;
+
+ ~UPnPDirObject();
+
+ UPnPDirObject &operator=(UPnPDirObject &&) = default;
+
+ void clear()
+ {
+ m_id.clear();
+ m_pid.clear();
+ url.clear();
+ type = Type::UNKNOWN;
+ item_class = ItemClass::UNKNOWN;
+ tag.Clear();
+ }
+};
+
+#endif /* _UPNPDIRCONTENT_H_X_INCLUDED_ */
diff --git a/src/db/plugins/upnp/Tags.cxx b/src/db/plugins/upnp/Tags.cxx
new file mode 100644
index 000000000..fd65df4d0
--- /dev/null
+++ b/src/db/plugins/upnp/Tags.cxx
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Tags.hxx"
+#include "tag/TagTable.hxx"
+
+const struct tag_table upnp_tags[] = {
+ { "upnp:artist", TAG_ARTIST },
+ { "upnp:album", TAG_ALBUM },
+ { "upnp:originalTrackNumber", TAG_TRACK },
+ { "upnp:genre", TAG_GENRE },
+ { "dc:title", TAG_TITLE },
+
+ /* sentinel */
+ { nullptr, TAG_NUM_OF_ITEM_TYPES }
+};
diff --git a/src/db/plugins/upnp/Tags.hxx b/src/db/plugins/upnp/Tags.hxx
new file mode 100644
index 000000000..ec6d18478
--- /dev/null
+++ b/src/db/plugins/upnp/Tags.hxx
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_UPNP_TAGS_HXX
+#define MPD_UPNP_TAGS_HXX
+
+/**
+ * Map UPnP property names to MPD tags.
+ */
+extern const struct tag_table upnp_tags[];
+
+#endif
diff --git a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
new file mode 100644
index 000000000..66951f402
--- /dev/null
+++ b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
@@ -0,0 +1,786 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "UpnpDatabasePlugin.hxx"
+#include "Directory.hxx"
+#include "Tags.hxx"
+#include "lib/upnp/Domain.hxx"
+#include "lib/upnp/ClientInit.hxx"
+#include "lib/upnp/Discovery.hxx"
+#include "lib/upnp/ContentDirectoryService.hxx"
+#include "lib/upnp/Util.hxx"
+#include "db/Interface.hxx"
+#include "db/DatabasePlugin.hxx"
+#include "db/Selection.hxx"
+#include "db/DatabaseError.hxx"
+#include "db/LightDirectory.hxx"
+#include "db/LightSong.hxx"
+#include "db/Stats.hxx"
+#include "config/ConfigData.hxx"
+#include "tag/TagBuilder.hxx"
+#include "tag/TagTable.hxx"
+#include "util/Error.hxx"
+#include "util/Domain.hxx"
+#include "fs/Traits.hxx"
+#include "Log.hxx"
+#include "SongFilter.hxx"
+
+#include <string>
+#include <vector>
+#include <set>
+
+#include <assert.h>
+#include <string.h>
+
+static const char *const rootid = "0";
+
+class UpnpSong : public LightSong {
+ std::string uri2, real_uri2;
+
+ Tag tag2;
+
+public:
+ UpnpSong(UPnPDirObject &&object, std::string &&_uri)
+ :uri2(std::move(_uri)),
+ real_uri2(std::move(object.url)),
+ tag2(std::move(object.tag)) {
+ directory = nullptr;
+ uri = uri2.c_str();
+ real_uri = real_uri2.c_str();
+ tag = &tag2;
+ mtime = 0;
+ start_ms = end_ms = 0;
+ }
+};
+
+class UpnpDatabase : public Database {
+ UpnpClient_Handle handle;
+ UPnPDeviceDirectory *discovery;
+
+public:
+ UpnpDatabase():Database(upnp_db_plugin) {}
+
+ static Database *Create(EventLoop &loop, DatabaseListener &listener,
+ const config_param &param,
+ Error &error);
+
+ virtual bool Open(Error &error) override;
+ virtual void Close() override;
+ virtual const LightSong *GetSong(const char *uri_utf8,
+ Error &error) const override;
+ virtual void ReturnSong(const LightSong *song) const;
+
+ virtual bool Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const override;
+
+ virtual bool VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type, uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const override;
+
+ virtual bool GetStats(const DatabaseSelection &selection,
+ DatabaseStats &stats,
+ Error &error) const override;
+ virtual time_t GetUpdateStamp() const {return 0;}
+
+protected:
+ bool Configure(const config_param &param, Error &error);
+
+private:
+ bool VisitServer(const ContentDirectoryService &server,
+ const std::list<std::string> &vpath,
+ const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const;
+
+ /**
+ * Run an UPnP search according to MPD parameters, and
+ * visit_song the results.
+ */
+ bool SearchSongs(const ContentDirectoryService &server,
+ const char *objid,
+ const DatabaseSelection &selection,
+ VisitSong visit_song,
+ Error &error) const;
+
+ bool SearchSongs(const ContentDirectoryService &server,
+ const char *objid,
+ const DatabaseSelection &selection,
+ UPnPDirContent& dirbuf,
+ Error &error) const;
+
+ bool Namei(const ContentDirectoryService &server,
+ const std::list<std::string> &vpath,
+ UPnPDirObject &dirent,
+ Error &error) const;
+
+ /**
+ * Take server and objid, return metadata.
+ */
+ bool ReadNode(const ContentDirectoryService &server,
+ const char *objid, UPnPDirObject& dirent,
+ Error &error) const;
+
+ /**
+ * Get the path for an object Id. This works much like pwd,
+ * except easier cause our inodes have a parent id. Not used
+ * any more actually (see comments in SearchSongs).
+ */
+ bool BuildPath(const ContentDirectoryService &server,
+ const UPnPDirObject& dirent, std::string &idpath,
+ Error &error) const;
+};
+
+Database *
+UpnpDatabase::Create(gcc_unused EventLoop &loop,
+ gcc_unused DatabaseListener &listener,
+ const config_param &param, Error &error)
+{
+ UpnpDatabase *db = new UpnpDatabase();
+ if (!db->Configure(param, error)) {
+ delete db;
+ return nullptr;
+ }
+
+ /* libupnp loses its ability to receive multicast messages
+ apparently due to daemonization; using the LazyDatabase
+ wrapper works around this problem */
+ return db;
+}
+
+inline bool
+UpnpDatabase::Configure(const config_param &, Error &)
+{
+ return true;
+}
+
+bool
+UpnpDatabase::Open(Error &error)
+{
+ if (!UpnpClientGlobalInit(handle, error))
+ return false;
+
+ discovery = new UPnPDeviceDirectory(handle);
+ if (!discovery->Start(error)) {
+ delete discovery;
+ UpnpClientGlobalFinish();
+ return false;
+ }
+
+ return true;
+}
+
+void
+UpnpDatabase::Close()
+{
+ delete discovery;
+ UpnpClientGlobalFinish();
+}
+
+void
+UpnpDatabase::ReturnSong(const LightSong *_song) const
+{
+ assert(_song != nullptr);
+
+ UpnpSong *song = (UpnpSong *)const_cast<LightSong *>(_song);
+ delete song;
+}
+
+// Get song info by path. We can receive either the id path, or the titles
+// one
+const LightSong *
+UpnpDatabase::GetSong(const char *uri, Error &error) const
+{
+ auto vpath = stringToTokens(uri, "/", true);
+ if (vpath.size() < 2) {
+ error.Format(db_domain, DB_NOT_FOUND, "No such song: %s", uri);
+ return nullptr;
+ }
+
+ ContentDirectoryService server;
+ if (!discovery->getServer(vpath.front().c_str(), server, error))
+ return nullptr;
+
+ vpath.pop_front();
+
+ UPnPDirObject dirent;
+ if (vpath.front() != rootid) {
+ if (!Namei(server, vpath, dirent, error))
+ return nullptr;
+ } else {
+ if (!ReadNode(server, vpath.back().c_str(), dirent,
+ error))
+ return nullptr;
+ }
+
+ return new UpnpSong(std::move(dirent), uri);
+}
+
+/**
+ * Double-quote a string, adding internal backslash escaping.
+ */
+static void
+dquote(std::string &out, const char *in)
+{
+ out.push_back('"');
+
+ for (; *in != 0; ++in) {
+ switch(*in) {
+ case '\\':
+ case '"':
+ out.push_back('\\');
+ break;
+ }
+
+ out.push_back(*in);
+ }
+
+ out.push_back('"');
+}
+
+// Run an UPnP search, according to MPD parameters. Return results as
+// UPnP items
+bool
+UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
+ const char *objid,
+ const DatabaseSelection &selection,
+ UPnPDirContent &dirbuf,
+ Error &error) const
+{
+ const SongFilter *filter = selection.filter;
+ if (selection.filter == nullptr)
+ return true;
+
+ std::list<std::string> searchcaps;
+ if (!server.getSearchCapabilities(handle, searchcaps, error))
+ return false;
+
+ if (searchcaps.empty())
+ return true;
+
+ std::string cond;
+ for (const auto &item : filter->GetItems()) {
+ switch (auto tag = item.GetTag()) {
+ case LOCATE_TAG_ANY_TYPE:
+ {
+ if (!cond.empty()) {
+ cond += " and ";
+ }
+ cond += '(';
+ bool first(true);
+ for (const auto& cap : searchcaps) {
+ if (first)
+ first = false;
+ else
+ cond += " or ";
+ cond += cap;
+ if (item.GetFoldCase()) {
+ cond += " contains ";
+ } else {
+ cond += " = ";
+ }
+ dquote(cond, item.GetValue().c_str());
+ }
+ cond += ')';
+ }
+ break;
+
+ default:
+ /* Unhandled conditions like
+ LOCATE_TAG_BASE_TYPE or
+ LOCATE_TAG_FILE_TYPE won't have a
+ corresponding upnp prop, so they will be
+ skipped */
+ if (tag == TAG_ALBUM_ARTIST)
+ tag = TAG_ARTIST;
+
+ // TODO: support LOCATE_TAG_ANY_TYPE etc.
+ const char *name = tag_table_lookup(upnp_tags,
+ TagType(tag));
+ if (name == nullptr)
+ continue;
+
+ if (!cond.empty()) {
+ cond += " and ";
+ }
+ cond += name;
+
+ /* FoldCase doubles up as contains/equal
+ switch. UpNP search is supposed to be
+ case-insensitive, but at least some servers
+ have the same convention as mpd (e.g.:
+ minidlna) */
+ if (item.GetFoldCase()) {
+ cond += " contains ";
+ } else {
+ cond += " = ";
+ }
+ dquote(cond, item.GetValue().c_str());
+ }
+ }
+
+ return server.search(handle,
+ objid, cond.c_str(), dirbuf,
+ error);
+}
+
+static bool
+visitSong(const UPnPDirObject &meta, const char *path,
+ const DatabaseSelection &selection,
+ VisitSong visit_song, Error& error)
+{
+ if (!visit_song)
+ return true;
+
+ LightSong song;
+ song.directory = nullptr;
+ song.uri = path;
+ song.real_uri = meta.url.c_str();
+ song.tag = &meta.tag;
+ song.mtime = 0;
+ song.start_ms = song.end_ms = 0;
+
+ return !selection.Match(song) || visit_song(song, error);
+}
+
+/**
+ * Build synthetic path based on object id for search results. The use
+ * of "rootid" is arbitrary, any name that is not likely to be a top
+ * directory name would fit.
+ */
+static std::string
+songPath(const std::string &servername,
+ const std::string &objid)
+{
+ return servername + "/" + rootid + "/" + objid;
+}
+
+bool
+UpnpDatabase::SearchSongs(const ContentDirectoryService &server,
+ const char *objid,
+ const DatabaseSelection &selection,
+ VisitSong visit_song,
+ Error &error) const
+{
+ UPnPDirContent dirbuf;
+ if (!visit_song)
+ return true;
+ if (!SearchSongs(server, objid, selection, dirbuf, error))
+ return false;
+
+ for (auto &dirent : dirbuf.objects) {
+ if (dirent.type != UPnPDirObject::Type::ITEM ||
+ dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
+ continue;
+
+ // We get song ids as the result of the UPnP search. But our
+ // client expects paths (e.g. we get 1$4$3788 from minidlna,
+ // but we need to translate to /Music/All_Music/Satisfaction).
+ // We can do this in two ways:
+ // - Rebuild a normal path using BuildPath() which is a kind of pwd
+ // - Build a bogus path based on the song id.
+ // The first method is nice because the returned paths are pretty, but
+ // it has two big problems:
+ // - The song paths are ambiguous: e.g. minidlna returns all search
+ // results as being from the "All Music" directory, which can
+ // contain several songs with the same title (but different objids)
+ // - The performance of BuildPath() is atrocious on very big
+ // directories, even causing timeouts in clients. And of
+ // course, 'All Music' is very big.
+ // So we return synthetic and ugly paths based on the object id,
+ // which we later have to detect.
+ const std::string path = songPath(server.getFriendlyName(),
+ dirent.m_id);
+ if (!visitSong(std::move(dirent), path.c_str(),
+ selection, visit_song,
+ error))
+ return false;
+ }
+
+ return true;
+}
+
+bool
+UpnpDatabase::ReadNode(const ContentDirectoryService &server,
+ const char *objid, UPnPDirObject &dirent,
+ Error &error) const
+{
+ UPnPDirContent dirbuf;
+ if (!server.getMetadata(handle, objid, dirbuf, error))
+ return false;
+
+ if (dirbuf.objects.size() == 1) {
+ dirent = std::move(dirbuf.objects.front());
+ } else {
+ error.Format(upnp_domain, "Bad resource");
+ return false;
+ }
+
+ return true;
+}
+
+bool
+UpnpDatabase::BuildPath(const ContentDirectoryService &server,
+ const UPnPDirObject& idirent,
+ std::string &path,
+ Error &error) const
+{
+ const char *pid = idirent.m_id.c_str();
+ path.clear();
+ UPnPDirObject dirent;
+ while (strcmp(pid, rootid) != 0) {
+ if (!ReadNode(server, pid, dirent, error))
+ return false;
+ pid = dirent.m_pid.c_str();
+
+ if (path.empty())
+ path = dirent.name;
+ else
+ path = PathTraitsUTF8::Build(dirent.name.c_str(),
+ path.c_str());
+ }
+
+ path = PathTraitsUTF8::Build(server.getFriendlyName(),
+ path.c_str());
+ return true;
+}
+
+// Take server and internal title pathname and return objid and metadata.
+bool
+UpnpDatabase::Namei(const ContentDirectoryService &server,
+ const std::list<std::string> &vpath,
+ UPnPDirObject &odirent,
+ Error &error) const
+{
+ if (vpath.empty()) {
+ // looking for root info
+ if (!ReadNode(server, rootid, odirent, error))
+ return false;
+
+ return true;
+ }
+
+ std::string objid(rootid);
+
+ // Walk the path elements, read each directory and try to find the next one
+ for (auto i = vpath.begin(), last = std::prev(vpath.end());; ++i) {
+ UPnPDirContent dirbuf;
+ if (!server.readDir(handle, objid.c_str(), dirbuf, error))
+ return false;
+
+ // Look for the name in the sub-container list
+ UPnPDirObject *child = dirbuf.FindObject(i->c_str());
+ if (child == nullptr) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "No such object");
+ return false;
+ }
+
+ if (i == last) {
+ odirent = std::move(*child);
+ return true;
+ }
+
+ if (child->type != UPnPDirObject::Type::CONTAINER) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "Not a container");
+ return false;
+ }
+
+ objid = std::move(child->m_id);
+ }
+}
+
+static bool
+VisitItem(const UPnPDirObject &object, const char *uri,
+ const DatabaseSelection &selection,
+ VisitSong visit_song, VisitPlaylist visit_playlist,
+ Error &error)
+{
+ assert(object.type == UPnPDirObject::Type::ITEM);
+
+ switch (object.item_class) {
+ case UPnPDirObject::ItemClass::MUSIC:
+ return !visit_song ||
+ visitSong(object, uri,
+ selection, visit_song, error);
+
+ case UPnPDirObject::ItemClass::PLAYLIST:
+ if (visit_playlist) {
+ /* Note: I've yet to see a
+ playlist item (playlists
+ seem to be usually handled
+ as containers, so I'll
+ decide what to do when I
+ see one... */
+ }
+
+ return true;
+
+ case UPnPDirObject::ItemClass::UNKNOWN:
+ return true;
+ }
+
+ assert(false);
+ gcc_unreachable();
+}
+
+static bool
+VisitObject(const UPnPDirObject &object, const char *uri,
+ const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error)
+{
+ switch (object.type) {
+ case UPnPDirObject::Type::UNKNOWN:
+ assert(false);
+ gcc_unreachable();
+
+ case UPnPDirObject::Type::CONTAINER:
+ return !visit_directory ||
+ visit_directory(LightDirectory(uri, 0), error);
+
+ case UPnPDirObject::Type::ITEM:
+ return VisitItem(object, uri, selection,
+ visit_song, visit_playlist,
+ error);
+ }
+
+ assert(false);
+ gcc_unreachable();
+}
+
+// vpath is a parsed and writeable version of selection.uri. There is
+// really just one path parameter.
+bool
+UpnpDatabase::VisitServer(const ContentDirectoryService &server,
+ const std::list<std::string> &vpath,
+ const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ /* If the path begins with rootid, we know that this is a
+ song, not a directory (because that's how we set things
+ up). Just visit it. Note that the choice of rootid is
+ arbitrary, any value not likely to be the name of a top
+ directory would be ok. */
+ /* !Note: this *can't* be handled by Namei further down,
+ because the path is not valid for traversal. Besides, it's
+ just faster to access the target node directly */
+ if (!vpath.empty() && vpath.front() == rootid) {
+ switch (vpath.size()) {
+ case 1:
+ return true;
+
+ case 2:
+ break;
+
+ default:
+ error.Format(db_domain, DB_NOT_FOUND,
+ "Not found");
+ return false;
+ }
+
+ if (visit_song) {
+ UPnPDirObject dirent;
+ if (!ReadNode(server, vpath.back().c_str(), dirent,
+ error))
+ return false;
+
+ if (dirent.type != UPnPDirObject::Type::ITEM ||
+ dirent.item_class != UPnPDirObject::ItemClass::MUSIC) {
+ error.Format(db_domain, DB_NOT_FOUND,
+ "Not found");
+ return false;
+ }
+
+ std::string path = songPath(server.getFriendlyName(),
+ dirent.m_id);
+ if (!visitSong(std::move(dirent), path.c_str(),
+ selection,
+ visit_song, error))
+ return false;
+ }
+ return true;
+ }
+
+ // Translate the target path into an object id and the associated metadata.
+ UPnPDirObject tdirent;
+ if (!Namei(server, vpath, tdirent, error))
+ return false;
+
+ /* If recursive is set, this is a search... No use sending it
+ if the filter is empty. In this case, we implement limited
+ recursion (1-deep) here, which will handle the "add dir"
+ case. */
+ if (selection.recursive && selection.filter)
+ return SearchSongs(server, tdirent.m_id.c_str(), selection,
+ visit_song, error);
+
+ const char *const base_uri = selection.uri.empty()
+ ? server.getFriendlyName()
+ : selection.uri.c_str();
+
+ if (tdirent.type == UPnPDirObject::Type::ITEM) {
+ return VisitItem(tdirent, base_uri,
+ selection,
+ visit_song, visit_playlist,
+ error);
+ }
+
+ /* Target was a a container. Visit it. We could read slices
+ and loop here, but it's not useful as mpd will only return
+ data to the client when we're done anyway. */
+ UPnPDirContent dirbuf;
+ if (!server.readDir(handle, tdirent.m_id.c_str(), dirbuf,
+ error))
+ return false;
+
+ for (auto &dirent : dirbuf.objects) {
+ const std::string uri = PathTraitsUTF8::Build(base_uri,
+ dirent.name.c_str());
+ if (!VisitObject(dirent, uri.c_str(),
+ selection,
+ visit_directory,
+ visit_song, visit_playlist,
+ error))
+ return false;
+ }
+
+ return true;
+}
+
+// Deal with the possibly multiple servers, call VisitServer if needed.
+bool
+UpnpDatabase::Visit(const DatabaseSelection &selection,
+ VisitDirectory visit_directory,
+ VisitSong visit_song,
+ VisitPlaylist visit_playlist,
+ Error &error) const
+{
+ auto vpath = stringToTokens(selection.uri, "/", true);
+ if (vpath.empty()) {
+ std::vector<ContentDirectoryService> servers;
+ if (!discovery->getDirServices(servers, error))
+ return false;
+
+ for (const auto &server : servers) {
+ if (visit_directory) {
+ const LightDirectory d(server.getFriendlyName(), 0);
+ if (!visit_directory(d, error))
+ return false;
+ }
+
+ if (selection.recursive &&
+ !VisitServer(server, vpath, selection,
+ visit_directory, visit_song, visit_playlist,
+ error))
+ return false;
+ }
+
+ return true;
+ }
+
+ // We do have a path: the first element selects the server
+ std::string servername(std::move(vpath.front()));
+ vpath.pop_front();
+
+ ContentDirectoryService server;
+ if (!discovery->getServer(servername.c_str(), server, error))
+ return false;
+
+ return VisitServer(server, vpath, selection,
+ visit_directory, visit_song, visit_playlist, error);
+}
+
+bool
+UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag, gcc_unused uint32_t group_mask,
+ VisitTag visit_tag,
+ Error &error) const
+{
+ // TODO: use group_mask
+
+ if (!visit_tag)
+ return true;
+
+ std::vector<ContentDirectoryService> servers;
+ if (!discovery->getDirServices(servers, error))
+ return false;
+
+ std::set<std::string> values;
+ for (auto& server : servers) {
+ UPnPDirContent dirbuf;
+ if (!SearchSongs(server, rootid, selection, dirbuf, error))
+ return false;
+
+ for (const auto &dirent : dirbuf.objects) {
+ if (dirent.type != UPnPDirObject::Type::ITEM ||
+ dirent.item_class != UPnPDirObject::ItemClass::MUSIC)
+ continue;
+
+ const char *value = dirent.tag.GetValue(tag);
+ if (value != nullptr) {
+#if defined(__clang__) || GCC_CHECK_VERSION(4,8)
+ values.emplace(value);
+#else
+ values.insert(value);
+#endif
+ }
+ }
+ }
+
+ for (const auto& value : values) {
+ TagBuilder builder;
+ builder.AddItem(tag, value.c_str());
+ if (!visit_tag(builder.Commit(), error))
+ return false;
+ }
+
+ return true;
+}
+
+bool
+UpnpDatabase::GetStats(const DatabaseSelection &,
+ DatabaseStats &stats, Error &) const
+{
+ /* Note: this gets called before the daemonizing so we can't
+ reallyopen this would be a problem if we had real stats */
+ stats.song_count = 0;
+ stats.total_duration = 0;
+ stats.artist_count = 0;
+ stats.album_count = 0;
+ return true;
+}
+
+const DatabasePlugin upnp_db_plugin = {
+ "upnp",
+ 0,
+ UpnpDatabase::Create,
+};
diff --git a/src/db/plugins/upnp/UpnpDatabasePlugin.hxx b/src/db/plugins/upnp/UpnpDatabasePlugin.hxx
new file mode 100644
index 000000000..0228405cd
--- /dev/null
+++ b/src/db/plugins/upnp/UpnpDatabasePlugin.hxx
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_UPNP_DATABASE_PLUGIN_HXX
+#define MPD_UPNP_DATABASE_PLUGIN_HXX
+
+struct DatabasePlugin;
+
+extern const DatabasePlugin upnp_db_plugin;
+
+#endif