diff --git a/Makefile.am b/Makefile.am index cb08b832a..7fb6dc15f 100644 --- a/Makefile.am +++ b/Makefile.am @@ -183,6 +183,12 @@ libmpd_a_SOURCES = \ src/SongFilter.cxx src/SongFilter.hxx \ src/PlaylistFile.cxx src/PlaylistFile.hxx +if ENABLE_CURL +libmpd_a_SOURCES += \ + src/RemoteTagCache.cxx src/RemoteTagCache.hxx \ + src/RemoteTagCacheHandler.hxx +endif + if ANDROID else libmpd_a_SOURCES += \ diff --git a/src/Instance.cxx b/src/Instance.cxx index f87a53be1..2c27b7938 100644 --- a/src/Instance.cxx +++ b/src/Instance.cxx @@ -23,6 +23,11 @@ #include "Idle.hxx" #include "Stats.hxx" +#ifdef ENABLE_CURL +#include "RemoteTagCache.hxx" +#include "util/UriUtil.hxx" +#endif + #ifdef ENABLE_DATABASE #include "db/DatabaseError.hxx" @@ -114,3 +119,31 @@ Instance::LostNeighbor(gcc_unused const NeighborInfo &info) noexcept } #endif + +#ifdef ENABLE_CURL + +void +Instance::LookupRemoteTag(const char *uri) noexcept +{ + if (!uri_has_scheme(uri)) + return; + + if (!remote_tag_cache) + remote_tag_cache = std::make_unique(event_loop, + *this); + + remote_tag_cache->Lookup(uri); +} + +void +Instance::OnRemoteTag(const char *uri, const Tag &tag) noexcept +{ + if (!tag.IsDefined()) + /* boring */ + return; + + for (auto &partition : partitions) + partition.TagModified(uri, tag); +} + +#endif diff --git a/src/Instance.hxx b/src/Instance.hxx index 9dcb2f788..bec707219 100644 --- a/src/Instance.hxx +++ b/src/Instance.hxx @@ -26,6 +26,10 @@ #include "event/MaskMonitor.hxx" #include "Compiler.h" +#ifdef ENABLE_CURL +#include "RemoteTagCacheHandler.hxx" +#endif + #ifdef ENABLE_NEIGHBOR_PLUGINS #include "neighbor/Listener.hxx" class NeighborGlue; @@ -38,11 +42,13 @@ class Storage; class UpdateService; #endif +#include #include class ClientList; struct Partition; class StateFile; +class RemoteTagCache; /** * A utility class which, when used as the first base class, ensures @@ -66,6 +72,9 @@ struct Instance final #ifdef ENABLE_NEIGHBOR_PLUGINS public NeighborListener #endif +#ifdef ENABLE_CURL + , public RemoteTagCacheHandler +#endif { EventThread io_thread; @@ -87,6 +96,10 @@ struct Instance final UpdateService *update = nullptr; #endif +#ifdef ENABLE_CURL + std::unique_ptr remote_tag_cache; +#endif + ClientList *client_list; std::list partitions; @@ -139,6 +152,14 @@ struct Instance final void FinishShutdownUpdate() noexcept; void ShutdownDatabase() noexcept; +#ifdef ENABLE_CURL + void LookupRemoteTag(const char *uri) noexcept; +#else + void LookupRemoteTag(const char *) noexcept { + /* no-op */ + } +#endif + private: #ifdef ENABLE_DATABASE void OnDatabaseModified() override; @@ -151,6 +172,11 @@ private: void LostNeighbor(const NeighborInfo &info) noexcept override; #endif +#ifdef ENABLE_CURL + /* virtual methods from class RemoteTagCacheHandler */ + void OnRemoteTag(const char *uri, const Tag &tag) noexcept override; +#endif + /* callback for #idle_monitor */ void OnIdle(unsigned mask); }; diff --git a/src/Partition.cxx b/src/Partition.cxx index 6fb24dcf6..cb603247f 100644 --- a/src/Partition.cxx +++ b/src/Partition.cxx @@ -97,6 +97,12 @@ Partition::TagModified() playlist.TagModified(std::move(*song)); } +void +Partition::TagModified(const char *uri, const Tag &tag) noexcept +{ + playlist.TagModified(uri, tag); +} + void Partition::SyncWithPlayer() { diff --git a/src/Partition.hxx b/src/Partition.hxx index e41a39b58..94056522d 100644 --- a/src/Partition.hxx +++ b/src/Partition.hxx @@ -226,6 +226,12 @@ struct Partition final : QueueListener, PlayerListener, MixerListener { */ void TagModified(); + /** + * The tag of the given song has been modified. Propagate the + * change to all instances of this song in the queue. + */ + void TagModified(const char *uri, const Tag &tag) noexcept; + /** * Synchronize the player with the play queue. */ diff --git a/src/RemoteTagCache.cxx b/src/RemoteTagCache.cxx new file mode 100644 index 000000000..ec0c5d0ba --- /dev/null +++ b/src/RemoteTagCache.cxx @@ -0,0 +1,140 @@ +/* + * Copyright 2003-2018 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 "RemoteTagCache.hxx" +#include "RemoteTagCacheHandler.hxx" +#include "input/ScanTags.hxx" +#include "util/DeleteDisposer.hxx" +#include "Log.hxx" + +RemoteTagCache::RemoteTagCache(EventLoop &event_loop, + RemoteTagCacheHandler &_handler) noexcept + :handler(_handler), + defer_invoke_handler(event_loop, BIND_THIS_METHOD(InvokeHandlers)), + map(typename KeyMap::bucket_traits(&buckets.front(), buckets.size())) +{ +} + +RemoteTagCache::~RemoteTagCache() noexcept +{ + map.clear_and_dispose(DeleteDisposer()); +} + +void +RemoteTagCache::Lookup(const std::string &uri) noexcept +{ + std::unique_lock lock(mutex); + + KeyMap::insert_commit_data hint; + auto result = map.insert_check(uri, Item::Hash(), Item::Equal(), hint); + if (result.second) { + auto *item = new Item(*this, uri); + map.insert_commit(*item, hint); + waiting_list.push_back(*item); + lock.unlock(); + + try { + item->scanner = InputScanTags(uri.c_str(), *item); + if (!item->scanner) { + /* unsupported */ + lock.lock(); + ItemResolved(*item); + return; + } + + item->scanner->Start(); + } catch (...) { + FormatError(std::current_exception(), + "Failed to scan tags of '%s'", + uri.c_str()); + + item->scanner.reset(); + + lock.lock(); + ItemResolved(*item); + return; + } + } else if (result.first->scanner) { + /* already scanning this one - no-op */ + } else { + /* already finished: re-invoke the handler */ + + auto &item = *result.first; + + idle_list.erase(waiting_list.iterator_to(item)); + invoke_list.push_back(item); + + ScheduleInvokeHandlers(); + } +} + +void +RemoteTagCache::ItemResolved(Item &item) noexcept +{ + waiting_list.erase(waiting_list.iterator_to(item)); + invoke_list.push_back(item); + + ScheduleInvokeHandlers(); +} + +void +RemoteTagCache::InvokeHandlers() noexcept +{ + const std::lock_guard lock(mutex); + + while (!invoke_list.empty()) { + auto &item = invoke_list.front(); + invoke_list.pop_front(); + idle_list.push_back(item); + + const ScopeUnlock unlock(mutex); + handler.OnRemoteTag(item.uri.c_str(), item.tag); + } + + /* evict items if there are too many */ + while (map.size() > MAX_SIZE && !idle_list.empty()) { + auto *item = &idle_list.front(); + idle_list.pop_front(); + map.erase(map.iterator_to(*item)); + delete item; + } +} + +void +RemoteTagCache::Item::OnRemoteTag(Tag &&_tag) noexcept +{ + tag = std::move(_tag); + + scanner.reset(); + + const std::lock_guard lock(parent.mutex); + parent.ItemResolved(*this); +} + +void +RemoteTagCache::Item::OnRemoteTagError(std::exception_ptr e) noexcept +{ + FormatError(e, "Failed to scan tags of '%s'", uri.c_str()); + + scanner.reset(); + + const std::lock_guard lock(parent.mutex); + parent.ItemResolved(*this); +} diff --git a/src/RemoteTagCache.hxx b/src/RemoteTagCache.hxx new file mode 100644 index 000000000..7e138138e --- /dev/null +++ b/src/RemoteTagCache.hxx @@ -0,0 +1,144 @@ +/* + * Copyright 2003-2018 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_REMOTE_TAG_CACHE_HXX +#define MPD_REMOTE_TAG_CACHE_HXX + +#include "check.h" +#include "input/RemoteTagScanner.hxx" +#include "tag/Tag.hxx" +#include "event/DeferEvent.hxx" +#include "thread/Mutex.hxx" + +#include +#include + +#include + +class RemoteTagCacheHandler; + +/** + * A cache for tags received via #RemoteTagScanner. + */ +class RemoteTagCache final { + static constexpr size_t MAX_SIZE = 4096; + + RemoteTagCacheHandler &handler; + + DeferEvent defer_invoke_handler; + + Mutex mutex; + + struct Item final + : public boost::intrusive::unordered_set_base_hook>, + public boost::intrusive::list_base_hook>, + RemoteTagHandler + { + RemoteTagCache &parent; + + const std::string uri; + + std::unique_ptr scanner; + + Tag tag; + + template + Item(RemoteTagCache &_parent, U &&_uri) noexcept + :parent(_parent), uri(std::forward(_uri)) {} + + /* virtual methods from RemoteTagHandler */ + void OnRemoteTag(Tag &&tag) noexcept override; + void OnRemoteTagError(std::exception_ptr e) noexcept override; + + struct Hash : std::hash { + using std::hash::operator(); + + gcc_pure + std::size_t operator()(const Item &item) const noexcept { + return std::hash::operator()(item.uri); + } + }; + + struct Equal { + gcc_pure + bool operator()(const Item &a, + const Item &b) const noexcept { + return a.uri == b.uri; + } + + gcc_pure + bool operator()(const std::string &a, + const Item &b) const noexcept { + return a == b.uri; + } + }; + }; + + typedef boost::intrusive::list> ItemList; + + /** + * These items have been resolved completely (successful or + * failed). All callbacks have been invoked. The oldest + * comes first in the list, and is the first one to be evicted + * if the cache is full. + */ + ItemList idle_list; + + /** + * A #RemoteTagScanner instances is currently busy on fetching + * information, and we're waiting for our #RemoteTagHandler + * methods to be invoked. + */ + ItemList waiting_list; + + /** + * These items have just been resolved, and the + * #RemoteTagCacheHandler is about to be invoked. After that, + * they will be moved to the #idle_list. + */ + ItemList invoke_list; + + typedef boost::intrusive::unordered_set, + boost::intrusive::equal, + boost::intrusive::constant_time_size> KeyMap; + + std::array buckets; + + KeyMap map; + +public: + RemoteTagCache(EventLoop &event_loop, + RemoteTagCacheHandler &_handler) noexcept; + ~RemoteTagCache() noexcept; + + void Lookup(const std::string &uri) noexcept; + +private: + void InvokeHandlers() noexcept; + + void ScheduleInvokeHandlers() noexcept { + defer_invoke_handler.Schedule(); + } + + void ItemResolved(Item &item) noexcept; +}; + +#endif diff --git a/src/RemoteTagCacheHandler.hxx b/src/RemoteTagCacheHandler.hxx new file mode 100644 index 000000000..e9a482088 --- /dev/null +++ b/src/RemoteTagCacheHandler.hxx @@ -0,0 +1,30 @@ +/* + * Copyright 2003-2018 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_REMOTE_TAG_CACHE_HANDLER_HXX +#define MPD_REMOTE_TAG_CACHE_HANDLER_HXX + +struct Tag; + +class RemoteTagCacheHandler { +public: + virtual void OnRemoteTag(const char *uri, const Tag &tag) noexcept = 0; +}; + +#endif diff --git a/src/command/QueueCommands.cxx b/src/command/QueueCommands.cxx index a696d24ce..3457842c7 100644 --- a/src/command/QueueCommands.cxx +++ b/src/command/QueueCommands.cxx @@ -32,6 +32,7 @@ #include "client/Client.hxx" #include "client/Response.hxx" #include "Partition.hxx" +#include "Instance.hxx" #include "BulkEdit.hxx" #include "util/ConstBuffer.hxx" #include "util/StringAPI.hxx" @@ -87,6 +88,10 @@ handle_add(Client &client, Request args, Response &r) ); switch (located_uri.type) { case LocatedUri::Type::ABSOLUTE: + AddUri(client, located_uri); + client.GetInstance().LookupRemoteTag(located_uri.canonical_uri); + return CommandResult::OK; + case LocatedUri::Type::PATH: AddUri(client, located_uri); return CommandResult::OK; @@ -107,6 +112,7 @@ handle_addid(Client &client, Request args, Response &r) auto &partition = client.GetPartition(); const SongLoader loader(client); unsigned added_id = partition.AppendURI(loader, uri); + partition.instance.LookupRemoteTag(uri); if (args.size == 2) { unsigned to = args.ParseUnsigned(1); diff --git a/src/queue/Playlist.cxx b/src/queue/Playlist.cxx index 0b68f0298..aaed7190a 100644 --- a/src/queue/Playlist.cxx +++ b/src/queue/Playlist.cxx @@ -43,6 +43,24 @@ playlist::TagModified(DetachedSong &&song) OnModified(); } +void +playlist::TagModified(const char *uri, const Tag &tag) noexcept +{ + bool modified = false; + + for (unsigned i = 0; i < queue.length; ++i) { + auto &song = *queue.items[i].song; + if (song.IsURI(uri)) { + song.SetTag(tag); + queue.ModifyAtPosition(i); + modified = true; + } + } + + if (modified) + OnModified(); +} + inline void playlist::QueueSongOrder(PlayerControl &pc, unsigned order) diff --git a/src/queue/Playlist.hxx b/src/queue/Playlist.hxx index d2b7937d3..499481360 100644 --- a/src/queue/Playlist.hxx +++ b/src/queue/Playlist.hxx @@ -23,6 +23,7 @@ #include "queue/Queue.hxx" enum TagType : uint8_t; +struct Tag; struct PlayerControl; class DetachedSong; class Database; @@ -192,6 +193,7 @@ public: * the song matches. */ void TagModified(DetachedSong &&song); + void TagModified(const char *uri, const Tag &tag) noexcept; #ifdef ENABLE_DATABASE /**