aboutsummaryrefslogtreecommitdiffstats
path: root/src/db/UpnpDatabasePlugin.cxx
diff options
context:
space:
mode:
authorMax Kellermann <max@duempel.org>2014-01-24 16:18:50 +0100
committerMax Kellermann <max@duempel.org>2014-01-24 16:38:44 +0100
commit9d34fc394ce30a28ec0e43f2ad7172b8de8b3be6 (patch)
tree4b58951bc81b17f16140c7f743a163bd837827bf /src/db/UpnpDatabasePlugin.cxx
parent973c9872f930d73a8ddc98e4802b242aea9f0dba (diff)
downloadmpd-9d34fc394ce30a28ec0e43f2ad7172b8de8b3be6.tar.gz
mpd-9d34fc394ce30a28ec0e43f2ad7172b8de8b3be6.tar.xz
mpd-9d34fc394ce30a28ec0e43f2ad7172b8de8b3be6.zip
Database*: move to db/
Diffstat (limited to 'src/db/UpnpDatabasePlugin.cxx')
-rw-r--r--src/db/UpnpDatabasePlugin.cxx785
1 files changed, 0 insertions, 785 deletions
diff --git a/src/db/UpnpDatabasePlugin.cxx b/src/db/UpnpDatabasePlugin.cxx
deleted file mode 100644
index 46084061f..000000000
--- a/src/db/UpnpDatabasePlugin.cxx
+++ /dev/null
@@ -1,785 +0,0 @@
-/*
- * 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 "upnp/Domain.hxx"
-#include "upnp/upnpplib.hxx"
-#include "upnp/Discovery.hxx"
-#include "upnp/ContentDirectoryService.hxx"
-#include "upnp/Directory.hxx"
-#include "upnp/Tags.hxx"
-#include "upnp/Util.hxx"
-#include "DatabasePlugin.hxx"
-#include "DatabaseSelection.hxx"
-#include "DatabaseError.hxx"
-#include "LightDirectory.hxx"
-#include "LightSong.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 {
- LibUPnP *m_lib;
- UPnPDeviceDirectory *m_superdir;
-
-public:
- 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,
- VisitString visit_string,
- 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)
-{
- m_lib = new LibUPnP();
- if (!m_lib->ok()) {
- error.Set(m_lib->GetInitError());
- delete m_lib;
- return false;
- }
-
- m_superdir = new UPnPDeviceDirectory(m_lib);
- if (!m_superdir->Start(error)) {
- delete m_superdir;
- delete m_lib;
- return false;
- }
-
- // Wait for device answers. This should be consistent with the value set
- // in the lib (currently 2)
- sleep(2);
- return true;
-}
-
-void
-UpnpDatabase::Close()
-{
- delete m_superdir;
- delete m_lib;
-}
-
-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 (!m_superdir->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(m_lib->getclh(), 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(m_lib->getclh(),
- 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(m_lib->getclh(), 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;
- }
-
- const UpnpClient_Handle handle = m_lib->getclh();
-
- 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(m_lib->getclh(), 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 (!m_superdir->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 (!m_superdir->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,
- VisitString visit_string,
- Error &error) const
-{
- if (!visit_string)
- return true;
-
- std::vector<ContentDirectoryService> servers;
- if (!m_superdir->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)
- if (!visit_string(value.c_str(), 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",
- UpnpDatabase::Create,
-};