aboutsummaryrefslogtreecommitdiffstats
path: root/src/db/plugins/simple
diff options
context:
space:
mode:
authorMax Kellermann <max@duempel.org>2014-02-26 09:17:41 +0100
committerMax Kellermann <max@duempel.org>2014-02-26 09:17:41 +0100
commit4d73e4d605a8abecff28b7e8c015d252b25954a9 (patch)
tree3b563de109eef41acf1cd3c2bd84cdee9b465f8b /src/db/plugins/simple
parentd86cc5bf42bc22e1903f0d1fada5282e7ec9ac89 (diff)
downloadmpd-4d73e4d605a8abecff28b7e8c015d252b25954a9.tar.gz
mpd-4d73e4d605a8abecff28b7e8c015d252b25954a9.tar.xz
mpd-4d73e4d605a8abecff28b7e8c015d252b25954a9.zip
db/simple: create dedicated directory
Diffstat (limited to 'src/db/plugins/simple')
-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.cxx291
-rw-r--r--src/db/plugins/simple/Directory.hxx239
-rw-r--r--src/db/plugins/simple/DirectorySave.cxx207
-rw-r--r--src/db/plugins/simple/DirectorySave.hxx35
-rw-r--r--src/db/plugins/simple/SimpleDatabasePlugin.cxx331
-rw-r--r--src/db/plugins/simple/SimpleDatabasePlugin.hxx114
-rw-r--r--src/db/plugins/simple/Song.cxx111
-rw-r--r--src/db/plugins/simple/Song.hxx113
-rw-r--r--src/db/plugins/simple/SongSort.cxx113
-rw-r--r--src/db/plugins/simple/SongSort.hxx28
12 files changed, 1776 insertions, 0 deletions
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..98262bc52
--- /dev/null
+++ b/src/db/plugins/simple/Directory.cxx
@@ -0,0 +1,291 @@
+/*
+ * 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 "db/LightDirectory.hxx"
+#include "db/LightSong.hxx"
+#include "db/Uri.hxx"
+#include "db/DatabaseLock.hxx"
+#include "SongFilter.hxx"
+#include "lib/icu/Collate.hxx"
+#include "fs/Traits.hxx"
+#include "util/Alloc.hxx"
+#include "util/Error.hxx"
+
+extern "C" {
+#include "util/list_sort.h"
+}
+
+#include <assert.h>
+#include <string.h>
+#include <stdlib.h>
+
+Directory::Directory(std::string &&_path_utf8, Directory *_parent)
+ :parent(_parent),
+ mtime(0), have_stat(false),
+ path(std::move(_path_utf8))
+{
+ INIT_LIST_HEAD(&children);
+ INIT_LIST_HEAD(&songs);
+}
+
+Directory::~Directory()
+{
+ Song *song, *ns;
+ directory_for_each_song_safe(song, ns, *this)
+ song->Free();
+
+ Directory *child, *n;
+ directory_for_each_child_safe(child, n, *this)
+ delete child;
+}
+
+void
+Directory::Delete()
+{
+ assert(holding_db_lock());
+ assert(parent != nullptr);
+
+ list_del(&siblings);
+ delete this;
+}
+
+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);
+ list_add_tail(&child->siblings, &children);
+ return child;
+}
+
+const Directory *
+Directory::FindChild(const char *name) const
+{
+ assert(holding_db_lock());
+
+ const Directory *child;
+ directory_for_each_child(child, *this)
+ if (strcmp(child->GetName(), name) == 0)
+ return child;
+
+ return nullptr;
+}
+
+void
+Directory::PruneEmpty()
+{
+ assert(holding_db_lock());
+
+ Directory *child, *n;
+ directory_for_each_child_safe(child, n, *this) {
+ child->PruneEmpty();
+
+ if (child->IsEmpty())
+ child->Delete();
+ }
+}
+
+Directory *
+Directory::LookupDirectory(const char *uri)
+{
+ assert(holding_db_lock());
+ assert(uri != nullptr);
+
+ if (isRootDirectory(uri))
+ return this;
+
+ char *duplicated = xstrdup(uri), *name = duplicated;
+
+ Directory *d = this;
+ while (1) {
+ char *slash = strchr(name, '/');
+ if (slash == name) {
+ d = nullptr;
+ break;
+ }
+
+ if (slash != nullptr)
+ *slash = '\0';
+
+ d = d->FindChild(name);
+ if (d == nullptr || slash == nullptr)
+ break;
+
+ name = slash + 1;
+ }
+
+ free(duplicated);
+
+ return d;
+}
+
+void
+Directory::AddSong(Song *song)
+{
+ assert(holding_db_lock());
+ assert(song != nullptr);
+ assert(song->parent == this);
+
+ list_add_tail(&song->siblings, &songs);
+}
+
+void
+Directory::RemoveSong(Song *song)
+{
+ assert(holding_db_lock());
+ assert(song != nullptr);
+ assert(song->parent == this);
+
+ list_del(&song->siblings);
+}
+
+const Song *
+Directory::FindSong(const char *name_utf8) const
+{
+ assert(holding_db_lock());
+ assert(name_utf8 != nullptr);
+
+ Song *song;
+ directory_for_each_song(song, *this) {
+ assert(song->parent == this);
+
+ if (strcmp(song->uri, name_utf8) == 0)
+ return song;
+ }
+
+ return nullptr;
+}
+
+Song *
+Directory::LookupSong(const char *uri)
+{
+ char *duplicated, *base;
+
+ assert(holding_db_lock());
+ assert(uri != nullptr);
+
+ duplicated = xstrdup(uri);
+ base = strrchr(duplicated, '/');
+
+ Directory *d = this;
+ if (base != nullptr) {
+ *base++ = 0;
+ d = d->LookupDirectory(duplicated);
+ if (d == nullptr) {
+ free(duplicated);
+ return nullptr;
+ }
+ } else
+ base = duplicated;
+
+ Song *song = d->FindSong(base);
+ assert(song == nullptr || song->parent == d);
+
+ free(duplicated);
+ return song;
+
+}
+
+static int
+directory_cmp(gcc_unused void *priv,
+ struct list_head *_a, struct list_head *_b)
+{
+ const Directory *a = (const Directory *)_a;
+ const Directory *b = (const Directory *)_b;
+
+ return IcuCollate(a->path.c_str(), b->path.c_str());
+}
+
+void
+Directory::Sort()
+{
+ assert(holding_db_lock());
+
+ list_sort(nullptr, &children, directory_cmp);
+ song_list_sort(&songs);
+
+ Directory *child;
+ directory_for_each_child(child, *this)
+ 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 (visit_song) {
+ Song *song;
+ directory_for_each_song(song, *this) {
+ 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;
+ }
+
+ Directory *child;
+ directory_for_each_child(child, *this) {
+ 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..6c60b7ebf
--- /dev/null
+++ b/src/db/plugins/simple/Directory.hxx
@@ -0,0 +1,239 @@
+/*
+ * 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 "util/list.h"
+#include "Compiler.h"
+#include "db/Visitor.hxx"
+#include "db/PlaylistVector.hxx"
+
+#include <string>
+
+static constexpr unsigned DEVICE_INARCHIVE = -1;
+static constexpr unsigned DEVICE_CONTAINER = -2;
+
+#define directory_for_each_child(pos, directory) \
+ list_for_each_entry(pos, &(directory).children, siblings)
+
+#define directory_for_each_child_safe(pos, n, directory) \
+ list_for_each_entry_safe(pos, n, &(directory).children, siblings)
+
+#define directory_for_each_song(pos, directory) \
+ list_for_each_entry(pos, &(directory).songs, siblings)
+
+#define directory_for_each_song_safe(pos, n, directory) \
+ list_for_each_entry_safe(pos, n, &(directory).songs, siblings)
+
+struct Song;
+struct db_visitor;
+class SongFilter;
+class Error;
+
+struct 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.
+ */
+ struct list_head siblings;
+
+ /**
+ * 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.
+ */
+ struct list_head 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.
+ */
+ struct list_head songs;
+
+ PlaylistVector playlists;
+
+ Directory *parent;
+ time_t mtime;
+ unsigned inode, device;
+ bool have_stat; /* not needed if ino_t == dev_t == 0 is impossible */
+
+ std::string path;
+
+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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Looks up a directory by its relative URI.
+ *
+ * @param uri the relative URI
+ * @return the Directory, or nullptr if none was found
+ */
+ gcc_pure
+ Directory *LookupDirectory(const char *uri);
+
+ gcc_pure
+ bool IsEmpty() const {
+ return list_empty(&children) &&
+ list_empty(&songs) &&
+ 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;
+ }
+
+ /**
+ * 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));
+ }
+
+ /**
+ * Looks up a song by its relative URI.
+ *
+ * Caller must lock the #db_mutex.
+ *
+ * @param uri the relative URI
+ * @return the song, or nullptr if none was found
+ */
+ gcc_pure
+ Song *LookupSong(const char *uri);
+
+ /**
+ * 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..6cc5df6cb
--- /dev/null
+++ b/src/db/plugins/simple/DirectorySave.cxx
@@ -0,0 +1,207 @@
+/*
+ * 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());
+ }
+
+ Directory *cur;
+ directory_for_each_child(cur, directory) {
+ fprintf(fp, DIRECTORY_DIR "%s\n", cur->GetName());
+
+ directory_save(fp, *cur);
+
+ if (ferror(fp))
+ return;
+ }
+
+ Song *song;
+ directory_for_each_song(song, directory)
+ 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/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
new file mode 100644
index 000000000..9911caa66
--- /dev/null
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
@@ -0,0 +1,331 @@
+/*
+ * 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 "db/DatabasePlugin.hxx"
+#include "db/Selection.hxx"
+#include "db/Helpers.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/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()) {}
+
+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();
+
+ 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)
+{
+ 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(borrowed_song_count == 0);
+
+ delete root;
+}
+
+const LightSong *
+SimpleDatabase::GetSong(const char *uri, Error &error) const
+{
+ assert(root != nullptr);
+ assert(borrowed_song_count == 0);
+
+ db_lock();
+ const Song *song = root->LookupSong(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 == &light_song);
+
+#ifndef NDEBUG
+ 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;
+
+ const Directory *directory = root->LookupDirectory(selection.uri.c_str());
+ if (directory == nullptr) {
+ if (visit_song) {
+ Song *song = root->LookupSong(selection.uri.c_str());
+ 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;
+ }
+
+ if (selection.recursive && visit_directory &&
+ !visit_directory(directory->Export(), error))
+ return false;
+
+ return directory->Walk(selection.recursive, selection.filter,
+ visit_directory, visit_song, visit_playlist,
+ error);
+}
+
+bool
+SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
+ TagType tag_type,
+ VisitString visit_string,
+ Error &error) const
+{
+ return ::VisitUniqueTags(*this, selection, tag_type, visit_string,
+ 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;
+}
+
+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..40d870460
--- /dev/null
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
@@ -0,0 +1,114 @@
+/*
+ * 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 SimpleDatabase : public Database {
+ AllocatedPath path;
+ std::string path_utf8;
+
+ Directory *root;
+
+ time_t mtime;
+
+ /**
+ * A buffer for GetSong().
+ */
+ mutable LightSong light_song;
+
+#ifndef NDEBUG
+ mutable unsigned borrowed_song_count;
+#endif
+
+ SimpleDatabase();
+
+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;
+ }
+
+ /* 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,
+ VisitString visit_string,
+ 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);
+};
+
+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..75fce20e9
--- /dev/null
+++ b/src/db/plugins/simple/Song.hxx
@@ -0,0 +1,113 @@
+/*
+ * 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 "util/list.h"
+#include "tag/Tag.hxx"
+#include "Compiler.h"
+
+#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 {
+ /**
+ * 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.
+ */
+ struct list_head 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;
+};
+
+#endif
diff --git a/src/db/plugins/simple/SongSort.cxx b/src/db/plugins/simple/SongSort.cxx
new file mode 100644
index 000000000..c5752f568
--- /dev/null
+++ b/src/db/plugins/simple/SongSort.cxx
@@ -0,0 +1,113 @@
+/*
+ * 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"
+
+extern "C" {
+#include "util/list_sort.h"
+}
+
+#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 */
+static int
+song_cmp(gcc_unused void *priv, struct list_head *_a, struct list_head *_b)
+{
+ const Song *a = (const Song *)_a;
+ const Song *b = (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;
+
+ /* then sort by disc */
+ ret = compare_tag_item(a->tag, b->tag, TAG_DISC);
+ if (ret != 0)
+ return ret;
+
+ /* then by track number */
+ ret = compare_tag_item(a->tag, b->tag, TAG_TRACK);
+ if (ret != 0)
+ return ret;
+
+ /* still no difference? compare file name */
+ return IcuCollate(a->uri, b->uri);
+}
+
+void
+song_list_sort(struct list_head *songs)
+{
+ list_sort(nullptr, songs, song_cmp);
+}
diff --git a/src/db/plugins/simple/SongSort.hxx b/src/db/plugins/simple/SongSort.hxx
new file mode 100644
index 000000000..28b903532
--- /dev/null
+++ b/src/db/plugins/simple/SongSort.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_SONG_SORT_HXX
+#define MPD_SONG_SORT_HXX
+
+struct list_head;
+
+void
+song_list_sort(list_head *songs);
+
+#endif