/* * Copyright (C) 2003-2013 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 "DatabasePlugin.hxx" #include "DatabaseSelection.hxx" #include "DatabaseError.hxx" #include "Directory.hxx" #include "Song.hxx" #include "SongFilter.hxx" #include "Compiler.h" #include "ConfigData.hxx" #include "tag/TagBuilder.hxx" #include "util/Error.hxx" #include "util/Domain.hxx" #include "protocol/Ack.hxx" #include #include #include #include class ProxyDatabase : public Database { std::string host; unsigned port; struct mpd_connection *connection; Directory *root; /* this is mutable because GetStats() must be "const" */ mutable time_t update_stamp; 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 override { return update_stamp; } private: bool Configure(const config_param ¶m, Error &error); bool Connect(Error &error); bool CheckConnection(Error &error); bool EnsureConnected(Error &error); void Disconnect(); }; 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 } }; 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(gcc_unused EventLoop &loop, gcc_unused DatabaseListener &listener, const config_param ¶m, Error &error) { ProxyDatabase *db = new ProxyDatabase(); if (!db->Configure(param, error)) { delete db; db = nullptr; } return db; } bool ProxyDatabase::Configure(const config_param ¶m, 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; root = Directory::NewRoot(); update_stamp = 0; return true; } void ProxyDatabase::Close() { delete root; 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)) { if (connection != nullptr) Disconnect(); return false; } return true; } bool ProxyDatabase::CheckConnection(Error &error) { assert(connection != nullptr); if (!mpd_connection_clear_error(connection)) { Disconnect(); return Connect(error); } return true; } bool ProxyDatabase::EnsureConnected(Error &error) { return connection != nullptr ? CheckConnection(error) : Connect(error); } void ProxyDatabase::Disconnect() { assert(connection != nullptr); mpd_connection_free(connection); connection = nullptr; } static Song * Convert(const struct mpd_song *song); Song * ProxyDatabase::GetSong(const char *uri, Error &error) const { // TODO: eliminate the const_cast if (!const_cast(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); Song *song2 = song != nullptr ? Convert(song) : nullptr; if (song != nullptr) mpd_song_free(song); if (!mpd_response_finish(connection)) { if (song2 != nullptr) song2->Free(); CheckError(connection, error); return nullptr; } if (song2 == nullptr) error.Format(db_domain, DB_NOT_FOUND, "No such song: %s", uri); return song2; } void ProxyDatabase::ReturnSong(Song *song) const { assert(song != nullptr); assert(song->parent == nullptr); song->Free(); } static bool Visit(struct mpd_connection *connection, Directory &root, 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, Directory &root, 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 (visit_directory && !visit_directory(Directory(path, &root), error)) return false; if (recursive && !Visit(connection, root, path, recursive, filter, visit_directory, visit_song, visit_playlist, error)) return false; return true; } 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); } } static Song * Convert(const struct mpd_song *song) { Song *s = Song::NewFile(mpd_song_get_uri(song), nullptr); s->mtime = mpd_song_get_last_modified(song); s->start_ms = mpd_song_get_start(song) * 1000; s->end_ms = mpd_song_get_end(song) * 1000; TagBuilder tag; tag.SetTime(mpd_song_get_duration(song)); for (const auto *i = &tag_table[0]; i->d != TAG_NUM_OF_ITEM_TYPES; ++i) Copy(tag, i->d, song, i->s); s->tag = tag.CommitNew(); return s; } gcc_pure static bool Match(const SongFilter *filter, const Song &song) { return filter == nullptr || filter->Match(song); } static bool Visit(const SongFilter *filter, const struct mpd_song *song, VisitSong visit_song, Error &error) { if (!visit_song) return true; Song *s = Convert(song); bool success = !Match(filter, *s) || visit_song(*s, error); s->Free(); return success; } static bool Visit(const struct mpd_playlist *playlist, Directory &root, 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, 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 ReceiveEntities(struct mpd_connection *connection) { std::list 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, Directory &root, 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 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, root, 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), root, 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) { Song *song2 = Convert(song); mpd_song_free(song); result = !Match(selection.filter, *song2) || visit_song(*song2, error); song2->Free(); } mpd_response_finish(connection); return result && CheckError(connection, error); } 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(this)->EnsureConnected(error)) return nullptr; if (!visit_directory && !visit_playlist && selection.recursive) /* 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, *root, selection.uri.c_str(), selection.recursive, selection.filter, visit_directory, visit_song, visit_playlist, error); } bool ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection, TagType tag_type, VisitString visit_string, Error &error) const { // TODO: eliminate the const_cast if (!const_cast(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); 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) { result = visit_string(pair->value, 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(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", ProxyDatabase::Create, };