/* * 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 "PlaylistVector.hxx" #include "Directory.hxx" #include "Song.hxx" #include "ConfigData.hxx" #include "tag/TagBuilder.hxx" #include "tag/TagTable.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" #include "Log.hxx" #include "SongFilter.hxx" #include #include #include #include #include static const char *const rootid = "0"; class UpnpDatabase : public Database { LibUPnP *m_lib; UPnPDeviceDirectory *m_superdir; Directory *m_root; public: static Database *Create(EventLoop &loop, DatabaseListener &listener, const config_param ¶m, Error &error); virtual bool Open(Error &error) override; virtual void Close() override; virtual Song *GetSong(const char *uri_utf8, Error &error) const override; virtual void ReturnSong(Song *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 ¶m, Error &error); private: bool VisitServer(ContentDirectoryService &server, const std::vector &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(ContentDirectoryService &server, const char *objid, const DatabaseSelection &selection, VisitSong visit_song, Error &error) const; bool SearchSongs(ContentDirectoryService &server, const char *objid, const DatabaseSelection &selection, UPnPDirContent& dirbuf, Error &error) const; bool Namei(ContentDirectoryService &server, const std::vector &vpath, std::string &oobjid, UPnPDirObject &dirent, Error &error) const; /** * Take server and objid, return metadata. */ bool ReadNode(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(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 ¶m, 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; } 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->ok()) { error.Set(m_superdir->GetError()); delete m_superdir; delete m_lib; return false; } m_root = Directory::NewRoot(); // 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_root; delete m_superdir; delete m_lib; } void UpnpDatabase::ReturnSong(Song *song) const { assert(song != nullptr); song->Free(); } // If uri is empty, we use the object's url instead. This happens // when the target of a Visit() is a song, which only happens when // "add"ing AFAIK. Visit() calls us with a null uri so that the url // appropriate for fetching is used instead. static Song * upnpItemToSong(const UPnPDirObject &dirent, const char *uri) { if (*uri == 0) uri = dirent.url.c_str(); Song *s = Song::NewFile(uri, nullptr); s->tag = new Tag(dirent.tag); return s; } // Get song info by path. We can receive either the id path, or the titles // one Song * UpnpDatabase::GetSong(const char *uri, Error &error) const { if (!m_superdir->ok()) { error.Set(upnp_domain, "UpnpDatabase::GetSong() superdir is sick"); return nullptr; } Song *song = nullptr; auto vpath = stringToTokens(uri, "/", true); if (vpath.size() >= 2) { ContentDirectoryService server; if (!m_superdir->getServer(vpath[0].c_str(), server)) { error.Set(upnp_domain, "server not found"); return nullptr; } vpath.erase(vpath.begin()); UPnPDirObject dirent; if (vpath[0].compare(rootid)) { std::string objid; if (!Namei(server, vpath, objid, dirent, error)) return nullptr; } else { if (!ReadNode(server, vpath.back().c_str(), dirent, error)) return nullptr; } song = upnpItemToSong(dirent, ""); } if (song == nullptr) error.Format(db_domain, DB_NOT_FOUND, "No such song: %s", uri); return song; } /** * Double-quote a string, adding internal backslash escaping. */ static void dquote(std::string &out, const char *in) { out.append(1, '"'); for (; *in != 0; ++in) { switch(*in) { case '\\': case '"': out.append(1, '\\'); out.append(1, *in); break; default: out.append(1, *in); } } out.append(1, '"'); } // Run an UPnP search, according to MPD parameters. Return results as // UPnP items bool UpnpDatabase::SearchSongs(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::set 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; Song *s = upnpItemToSong(meta, path); if (!selection.Match(*s)) return true; bool success = visit_song(*s, error); s->Free(); return success; } /** * 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 const std::string songPath(const std::string &servername, const std::string &objid) { return servername + "/" + rootid + "/" + objid; } bool UpnpDatabase::SearchSongs(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 (const 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. std::string path = songPath(server.getFriendlyName(), dirent.m_id); //BuildPath(server, dirent, path); if (!visitSong(dirent, path.c_str(), selection, visit_song, error)) return false; } return true; } bool UpnpDatabase::ReadNode(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[0]); } else { error.Format(upnp_domain, "Bad resource"); return false; } return true; } bool UpnpDatabase::BuildPath(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(); path = dirent.name + (path.empty()? "" : "/" + path); } path = std::string(server.getFriendlyName()) + "/" + path; return true; } // Take server and internal title pathname and return objid and metadata. bool UpnpDatabase::Namei(ContentDirectoryService &server, const std::vector &vpath, std::string &oobjid, UPnPDirObject &odirent, Error &error) const { oobjid.clear(); std::string objid(rootid); if (vpath.empty()) { // looking for root info if (!ReadNode(server, rootid, odirent, error)) return false; oobjid = rootid; return true; } const UpnpClient_Handle handle = m_lib->getclh(); // Walk the path elements, read each directory and try to find the next one for (unsigned int i = 0; i < vpath.size(); 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(vpath[i].c_str()); if (child == nullptr) break; switch (child->type) { case UPnPDirObject::Type::UNKNOWN: assert(false); gcc_unreachable(); case UPnPDirObject::Type::CONTAINER: objid = child->m_id; // Next readdir target if (i == vpath.size() - 1) { // The last element in the path was found and it's // a container, we're done oobjid = objid; odirent = std::move(*child); return true; } break; case UPnPDirObject::Type::ITEM: // If this is the last path elt, we found the target, // else it does not exist if (i == vpath.size() - 1) { oobjid = objid; odirent = std::move(*child); return true; } else { error.Format(db_domain, DB_NOT_FOUND, "No such object"); return false; } } } error.Format(db_domain, DB_NOT_FOUND, "No such object"); return false; } // vpath is a parsed and writeable version of selection.uri. There is // really just one path parameter. bool UpnpDatabase::VisitServer(ContentDirectoryService &server, const std::vector &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[0].compare(rootid)) { if (visit_song) { UPnPDirObject dirent; if (!ReadNode(server, vpath.back().c_str(), dirent, error)) return false; if (!visitSong(dirent, "", selection, visit_song, error)) return false; } return true; } // Translate the target path into an object id and the associated metadata. std::string objid; UPnPDirObject tdirent; if (!Namei(server, vpath, objid, tdirent, error)) return false; if (objid.empty()) // Not found, not a fatal error return true; /* 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, objid.c_str(), selection, visit_song, error); if (tdirent.type == UPnPDirObject::Type::ITEM) { // Target is a song. Not too sure we ever get there actually, maybe // this is always catched by the special uri test above. switch (tdirent.item_class) { case UPnPDirObject::ItemClass::MUSIC: if (visit_song) return visitSong(tdirent, "", selection, visit_song, error); break; 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... */ } break; case UPnPDirObject::ItemClass::UNKNOWN: break; } return true; } /* 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(), objid.c_str(), dirbuf, error)) return false; for (const auto &dirent : dirbuf.objects) { switch (dirent.type) { case UPnPDirObject::Type::UNKNOWN: assert(false); gcc_unreachable(); case UPnPDirObject::Type::CONTAINER: if (visit_directory) { Directory d((selection.uri + "/" + dirent.name).c_str(), m_root); if (!visit_directory(d, error)) return false; } break; case UPnPDirObject::Type::ITEM: switch (dirent.item_class) { case UPnPDirObject::ItemClass::MUSIC: if (visit_song) { /* We identify songs by giving them a special path. The Id is enough to fetch them from the server anyway. */ std::string p; if (!selection.recursive) p = selection.uri + "/" + dirent.name; if (!visitSong(dirent, p.c_str(), selection, visit_song, error)) return false; } break; 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... */ } break; case UPnPDirObject::ItemClass::UNKNOWN: break; } } } 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 { std::vector servers; if (!m_superdir->ok() || !m_superdir->getDirServices(servers)) { error.Set(upnp_domain, "UpnpDatabase::Visit() superdir is sick"); return false; } auto vpath = stringToTokens(selection.uri, "/", true); if (vpath.empty()) { if (!selection.recursive) { // If the path is empty and recursive is not set, synthetize a // pseudo-directory from the list of servers. if (visit_directory) { for (auto& server : servers) { Directory d(server.getFriendlyName(), m_root); if (!visit_directory(d, error)) return false; } } } else { // Recursive is set: visit each server for (auto& server : servers) { if (!VisitServer(server, std::vector(), 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(vpath[0]); vpath.erase(vpath.begin()); ContentDirectoryService *server = 0; for (auto& dir : servers) { if (!servername.compare(dir.getFriendlyName())) { server = &dir; break; } } if (server == 0) { FormatDebug(db_domain, "UpnpDatabase::Visit: server %s not found\n", vpath[0].c_str()); return true; } 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 servers; if (!m_superdir->ok() || !m_superdir->getDirServices(servers)) { error.Set(upnp_domain, "UpnpDatabase::Visit() superdir is sick"); return false; } std::set 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, };