From 8e5e97bfeda23ab296683882ceb997098b48f567 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 5 Apr 2019 12:38:49 +0200 Subject: [PATCH] command: add command "getfingerprint" A first use case for our libchromaprint integration added by commit 30e22b753b06660d888eb42a8c74d950d044d6dd --- doc/protocol.rst | 11 + meson.build | 8 + src/command/AllCommands.cxx | 4 + src/command/FingerprintCommands.cxx | 358 ++++++++++++++++++++++++++ src/command/FingerprintCommands.hxx | 32 +++ src/lib/chromaprint/DecoderClient.hxx | 7 + 6 files changed, 420 insertions(+) create mode 100644 src/command/FingerprintCommands.cxx create mode 100644 src/command/FingerprintCommands.hxx diff --git a/doc/protocol.rst b/doc/protocol.rst index 92b63dcfe..e1a882f9d 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -824,6 +824,17 @@ The music database don't this group tag. It exists only if at least one such song is found. +:command:`getfingerprint {URI}` + + Calculate the song's audio fingerprint. Example (abbreviated fingerprint):: + + getfingerprint "foo/bar.ogg" + chromaprint: AQACcEmSREmWJJmkIT_6CCf64... + OK + + This command is only available if MPD was built with + :file:`libchromaprint` (``-Dchromaprint=enabled``). + .. _command_find: :command:`find {FILTER} [sort {TYPE}] [window {START:END}]` diff --git a/meson.build b/meson.build index 3aecf9310..19ac06ecf 100644 --- a/meson.build +++ b/meson.build @@ -356,6 +356,13 @@ if sqlite_dep.found() ] endif +if chromaprint_dep.found() + sources += [ + 'src/command/FingerprintCommands.cxx', + 'src/lib/chromaprint/DecoderClient.cxx', + ] +endif + basic = static_library( 'basic', 'src/ReplayGainInfo.cxx', @@ -444,6 +451,7 @@ mpd = build_target( sqlite_dep, zeroconf_dep, more_deps, + chromaprint_dep, ], link_args: link_args, install: not is_android and not is_haiku, diff --git a/src/command/AllCommands.cxx b/src/command/AllCommands.cxx index dc77538bc..b0b7f57e9 100644 --- a/src/command/AllCommands.cxx +++ b/src/command/AllCommands.cxx @@ -33,6 +33,7 @@ #include "NeighborCommands.hxx" #include "ClientCommands.hxx" #include "PartitionCommands.hxx" +#include "FingerprintCommands.hxx" #include "OtherCommands.hxx" #include "Permission.hxx" #include "tag/Type.h" @@ -106,6 +107,9 @@ static constexpr struct command commands[] = { #ifdef ENABLE_DATABASE { "find", PERMISSION_READ, 1, -1, handle_find }, { "findadd", PERMISSION_ADD, 1, -1, handle_findadd}, +#endif +#ifdef ENABLE_CHROMAPRINT + { "getfingerprint", PERMISSION_READ, 1, 1, handle_getfingerprint }, #endif { "idle", PERMISSION_READ, 0, -1, handle_idle }, { "kill", PERMISSION_ADMIN, -1, -1, handle_kill }, diff --git a/src/command/FingerprintCommands.cxx b/src/command/FingerprintCommands.cxx new file mode 100644 index 000000000..3deacd8ec --- /dev/null +++ b/src/command/FingerprintCommands.cxx @@ -0,0 +1,358 @@ +/* + * 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 "FingerprintCommands.hxx" +#include "Request.hxx" +#include "LocateUri.hxx" +#include "lib/chromaprint/DecoderClient.hxx" +#include "decoder/DecoderAPI.hxx" +#include "decoder/DecoderList.hxx" +#include "storage/StorageInterface.hxx" +#include "client/Client.hxx" +#include "client/Response.hxx" +#include "client/ThreadBackgroundCommand.hxx" +#include "input/InputStream.hxx" +#include "input/LocalOpen.hxx" +#include "input/Handler.hxx" +#include "thread/Mutex.hxx" +#include "thread/Cond.hxx" +#include "system/Error.hxx" +#include "util/MimeType.hxx" +#include "util/UriUtil.hxx" + +class GetChromaprintCommand final + : public ThreadBackgroundCommand, ChromaprintDecoderClient, InputStreamHandler +{ + Mutex mutex; + Cond cond; + + const std::string uri; + const AllocatedPath path; + + bool cancel = false; + +public: + GetChromaprintCommand(Client &_client, std::string &&_uri, + AllocatedPath &&_path) noexcept + :ThreadBackgroundCommand(_client), + uri(std::move(_uri)), path(std::move(_path)) + { + } + +protected: + void Run() override; + + void SendResponse(Response &r) noexcept override { + r.Format("chromaprint: %s\n", + GetFingerprint().c_str()); + } + + void CancelThread() noexcept override { + const std::lock_guard lock(mutex); + cancel = true; + cond.signal(); + } + +private: + void DecodeStream(InputStream &is, const DecoderPlugin &plugin); + bool DecodeStream(InputStream &is, const char *suffix, + const DecoderPlugin &plugin); + void DecodeStream(InputStream &is); + bool DecodeContainer(const char *suffix, const DecoderPlugin &plugin); + bool DecodeContainer(const char *suffix); + bool DecodeFile(const char *suffix, InputStream &is, + const DecoderPlugin &plugin); + void DecodeFile(); + + /* virtual methods from class DecoderClient */ + InputStreamPtr OpenUri(const char *uri) override; + size_t Read(InputStream &is, void *buffer, size_t length) override; + + /* virtual methods from class InputStreamHandler */ + void OnInputStreamReady() noexcept override { + cond.signal(); + } + + void OnInputStreamAvailable() noexcept override { + cond.signal(); + } +}; + +inline void +GetChromaprintCommand::DecodeStream(InputStream &input_stream, + const DecoderPlugin &plugin) +{ + assert(plugin.stream_decode != nullptr); + assert(input_stream.IsReady()); + + if (cancel) + throw StopDecoder(); + + /* rewind the stream, so each plugin gets a fresh start */ + try { + input_stream.Rewind(); + } catch (...) { + } + + const ScopeUnlock unlock(mutex); + plugin.StreamDecode(*this, input_stream); +} + +gcc_pure +static bool +decoder_check_plugin_mime(const DecoderPlugin &plugin, + const InputStream &is) noexcept +{ + assert(plugin.stream_decode != nullptr); + + const char *mime_type = is.GetMimeType(); + return mime_type != nullptr && + plugin.SupportsMimeType(GetMimeTypeBase(mime_type).c_str()); +} + +gcc_pure +static bool +decoder_check_plugin_suffix(const DecoderPlugin &plugin, + const char *suffix) noexcept +{ + assert(plugin.stream_decode != nullptr); + + return suffix != nullptr && plugin.SupportsSuffix(suffix); +} + +gcc_pure +static bool +decoder_check_plugin(const DecoderPlugin &plugin, const InputStream &is, + const char *suffix) noexcept +{ + return plugin.stream_decode != nullptr && + (decoder_check_plugin_mime(plugin, is) || + decoder_check_plugin_suffix(plugin, suffix)); +} + +inline bool +GetChromaprintCommand::DecodeStream(InputStream &is, + const char *suffix, + const DecoderPlugin &plugin) +{ + if (!decoder_check_plugin(plugin, is, suffix)) + return false; + + ChromaprintDecoderClient::Reset(); + + DecodeStream(is, plugin); + return true; +} + +inline void +GetChromaprintCommand::DecodeStream(InputStream &is) +{ + UriSuffixBuffer suffix_buffer; + const char *const suffix = uri_get_suffix(uri.c_str(), suffix_buffer); + + decoder_plugins_try([this, &is, suffix](const DecoderPlugin &plugin){ + return DecodeStream(is, suffix, plugin); + }); +} + +inline bool +GetChromaprintCommand::DecodeContainer(const char *suffix, + const DecoderPlugin &plugin) +{ + if (plugin.container_scan == nullptr || + plugin.file_decode == nullptr || + !plugin.SupportsSuffix(suffix)) + return false; + + ChromaprintDecoderClient::Reset(); + + plugin.FileDecode(*this, path); + return IsReady(); +} + +inline bool +GetChromaprintCommand::DecodeContainer(const char *suffix) +{ + return decoder_plugins_try([this, suffix](const DecoderPlugin &plugin){ + return DecodeContainer(suffix, plugin); + }); +} + +inline bool +GetChromaprintCommand::DecodeFile(const char *suffix, InputStream &is, + const DecoderPlugin &plugin) +{ + if (!plugin.SupportsSuffix(suffix)) + return false; + + { + const std::lock_guard protect(mutex); + if (cancel) + throw StopDecoder(); + } + + ChromaprintDecoderClient::Reset(); + + if (plugin.file_decode != nullptr) { + plugin.FileDecode(*this, path); + return IsReady(); + } else if (plugin.stream_decode != nullptr) { + plugin.StreamDecode(*this, is); + return IsReady(); + } else + return false; +} + +inline void +GetChromaprintCommand::DecodeFile() +{ + const char *suffix = uri_get_suffix(uri.c_str()); + if (suffix == nullptr) + return; + + InputStreamPtr input_stream; + + try { + input_stream = OpenLocalInputStream(path, mutex); + } catch (const std::system_error &e) { + if (IsPathNotFound(e) && + /* ENOTDIR means this may be a path inside a + "container" file */ + DecodeContainer(suffix)) + return; + + throw; + } + + assert(input_stream); + + auto &is = *input_stream; + decoder_plugins_try([this, suffix, &is](const DecoderPlugin &plugin){ + return DecodeFile(suffix, is, plugin); + }); +} + +void +GetChromaprintCommand::Run() +try { + if (!path.IsNull()) + DecodeFile(); + else + DecodeStream(*OpenUri(uri.c_str())); + + ChromaprintDecoderClient::Finish(); +} catch (StopDecoder) { +} + +InputStreamPtr +GetChromaprintCommand::OpenUri(const char *uri2) +{ + if (cancel) + throw StopDecoder(); + + auto is = InputStream::Open(uri2, mutex); + is->SetHandler(this); + + const std::lock_guard lock(mutex); + while (true) { + if (cancel) + throw StopDecoder(); + + is->Update(); + if (is->IsReady()) { + is->Check(); + return is; + } + + cond.wait(mutex); + } +} + +size_t +GetChromaprintCommand::Read(InputStream &is, void *buffer, size_t length) +{ + /* overriding ChromaprintDecoderClient's implementation to + make it cancellable */ + + if (length == 0) + return 0; + + std::lock_guard lock(mutex); + + while (true) { + if (cancel) + return 0; + + if (is.IsAvailable()) + break; + + cond.wait(mutex); + } + + return is.Read(buffer, length); +} + +CommandResult +handle_getfingerprint(Client &client, Request args, Response &) +{ + const char *_uri = args.front(); + + auto lu = LocateUri(_uri, &client +#ifdef ENABLE_DATABASE + , nullptr +#endif + ); + + std::string uri = lu.canonical_uri; + + switch (lu.type) { + case LocatedUri::Type::ABSOLUTE: + break; + + case LocatedUri::Type::PATH: + break; + + case LocatedUri::Type::RELATIVE: +#ifdef ENABLE_DATABASE + { + const auto *storage = client.GetStorage(); + if (storage == nullptr) + throw ProtocolError(ACK_ERROR_NO_EXIST, "No database"); + + lu.path = storage->MapFS(lu.canonical_uri); + if (lu.path.IsNull()) { + uri = storage->MapUTF8(lu.canonical_uri); + if (uri_has_scheme(uri.c_str())) + throw ProtocolError(ACK_ERROR_NO_EXIST, "No such song"); + } + } +#else + throw ProtocolError(ACK_ERROR_NO_EXIST, "No database"); +#endif + } + + + auto cmd = std::make_unique(client, + std::move(uri), + std::move(lu.path)); + cmd->Start(); + client.SetBackgroundCommand(std::move(cmd)); + return CommandResult::BACKGROUND; +} diff --git a/src/command/FingerprintCommands.hxx b/src/command/FingerprintCommands.hxx new file mode 100644 index 000000000..5428730a1 --- /dev/null +++ b/src/command/FingerprintCommands.hxx @@ -0,0 +1,32 @@ +/* + * Copyright 2003-2019 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_FINGERPRINT_COMMANDS_HXX +#define MPD_FINGERPRINT_COMMANDS_HXX + +#include "CommandResult.hxx" + +class Client; +class Request; +class Response; + +CommandResult +handle_getfingerprint(Client &client, Request request, Response &response); + +#endif diff --git a/src/lib/chromaprint/DecoderClient.hxx b/src/lib/chromaprint/DecoderClient.hxx index c7dc021f3..b85b9a806 100644 --- a/src/lib/chromaprint/DecoderClient.hxx +++ b/src/lib/chromaprint/DecoderClient.hxx @@ -45,6 +45,13 @@ public: ChromaprintDecoderClient(); ~ChromaprintDecoderClient() noexcept; + bool IsReady() const noexcept { + return ready; + } + + void Reset() noexcept { + } + void Finish(); std::string GetFingerprint() const {