From 88bc3a9271873dfb89c00b73766b6575299db934 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 26 Jan 2018 18:50:13 +0100 Subject: [PATCH] input/qobuz: implement InputPlugin::scan_tags() --- Makefile.am | 1 + src/input/plugins/QobuzClient.cxx | 19 ++ src/input/plugins/QobuzClient.hxx | 3 + src/input/plugins/QobuzInputPlugin.cxx | 15 ++ src/input/plugins/QobuzTagScanner.cxx | 292 +++++++++++++++++++++++++ src/input/plugins/QobuzTagScanner.hxx | 60 +++++ 6 files changed, 390 insertions(+) create mode 100644 src/input/plugins/QobuzTagScanner.cxx create mode 100644 src/input/plugins/QobuzTagScanner.hxx diff --git a/Makefile.am b/Makefile.am index cb6a4cc71..3beb8bd71 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1392,6 +1392,7 @@ libinput_a_SOURCES += \ src/input/plugins/QobuzErrorParser.cxx src/input/plugins/QobuzErrorParser.hxx \ src/input/plugins/QobuzLoginRequest.cxx src/input/plugins/QobuzLoginRequest.hxx \ src/input/plugins/QobuzTrackRequest.cxx src/input/plugins/QobuzTrackRequest.hxx \ + src/input/plugins/QobuzTagScanner.cxx src/input/plugins/QobuzTagScanner.hxx \ src/input/plugins/QobuzInputPlugin.cxx src/input/plugins/QobuzInputPlugin.hxx INPUT_LIBS += $(YAJL_LIBS) $(LIBGCRYPT_LIBS) endif diff --git a/src/input/plugins/QobuzClient.cxx b/src/input/plugins/QobuzClient.cxx index 693afaf11..419f24682 100644 --- a/src/input/plugins/QobuzClient.cxx +++ b/src/input/plugins/QobuzClient.cxx @@ -164,6 +164,25 @@ QobuzClient::InvokeHandlers() noexcept } } +std::string +QobuzClient::MakeUrl(const char *object, const char *method, + const std::multimap &query) const noexcept +{ + assert(!query.empty()); + + std::string uri(base_url); + uri += object; + uri.push_back('/'); + uri += method; + + QueryStringBuilder q; + for (const auto &i : query) + q(uri, i.first.c_str(), i.second.c_str()); + + q(uri, "app_id", app_id); + return uri; +} + std::string QobuzClient::MakeSignedUrl(const char *object, const char *method, const std::multimap &query) const noexcept diff --git a/src/input/plugins/QobuzClient.hxx b/src/input/plugins/QobuzClient.hxx index 5999adb00..4edcc2c5d 100644 --- a/src/input/plugins/QobuzClient.hxx +++ b/src/input/plugins/QobuzClient.hxx @@ -96,6 +96,9 @@ public: */ QobuzSession GetSession() const; + std::string MakeUrl(const char *object, const char *method, + const std::multimap &query) const noexcept; + std::string MakeSignedUrl(const char *object, const char *method, const std::multimap &query) const noexcept; diff --git a/src/input/plugins/QobuzInputPlugin.cxx b/src/input/plugins/QobuzInputPlugin.cxx index 0b7ed2c03..e5c4f6d64 100644 --- a/src/input/plugins/QobuzInputPlugin.cxx +++ b/src/input/plugins/QobuzInputPlugin.cxx @@ -21,6 +21,7 @@ #include "QobuzInputPlugin.hxx" #include "QobuzClient.hxx" #include "QobuzTrackRequest.hxx" +#include "QobuzTagScanner.hxx" #include "CurlInputPlugin.hxx" #include "PluginUnavailable.hxx" #include "input/ProxyInputStream.hxx" @@ -192,9 +193,23 @@ OpenQobuzInput(const char *uri, Mutex &mutex, Cond &cond) return std::make_unique(uri, track_id, mutex, cond); } +static std::unique_ptr +ScanQobuzTags(const char *uri, RemoteTagHandler &handler) +{ + assert(qobuz_client != nullptr); + + const char *track_id = ExtractQobuzTrackId(uri); + if (track_id == nullptr) + return nullptr; + + return std::make_unique(*qobuz_client, track_id, + handler); +} + const InputPlugin qobuz_input_plugin = { "qobuz", InitQobuzInput, FinishQobuzInput, OpenQobuzInput, + ScanQobuzTags, }; diff --git a/src/input/plugins/QobuzTagScanner.cxx b/src/input/plugins/QobuzTagScanner.cxx new file mode 100644 index 000000000..ce01829b4 --- /dev/null +++ b/src/input/plugins/QobuzTagScanner.cxx @@ -0,0 +1,292 @@ +/* + * 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 "QobuzTagScanner.hxx" +#include "QobuzErrorParser.hxx" +#include "QobuzClient.hxx" +#include "lib/yajl/Callbacks.hxx" +#include "tag/Builder.hxx" +#include "tag/Tag.hxx" + +using Wrapper = Yajl::CallbacksWrapper; +static constexpr yajl_callbacks parse_callbacks = { + nullptr, + nullptr, + Wrapper::Integer, + nullptr, + nullptr, + Wrapper::String, + Wrapper::StartMap, + Wrapper::MapKey, + Wrapper::EndMap, + nullptr, + nullptr, +}; + +class QobuzTagScanner::ResponseParser final : public YajlResponseParser { + enum class State { + NONE, + COMPOSER, + COMPOSER_NAME, + DURATION, + TITLE, + ALBUM, + ALBUM_TITLE, + ALBUM_ARTIST, + ALBUM_ARTIST_NAME, + PERFORMER, + PERFORMER_NAME, + } state = State::NONE; + + unsigned map_depth = 0; + + TagBuilder tag; + +public: + explicit ResponseParser() noexcept + :YajlResponseParser(&parse_callbacks, nullptr, this) {} + + Tag GetTag() { + return tag.Commit(); + } + + /* yajl callbacks */ + bool Integer(long long value) noexcept; + bool String(StringView value) noexcept; + bool StartMap() noexcept; + bool MapKey(StringView value) noexcept; + bool EndMap() noexcept; +}; + +static std::string +MakeTrackUrl(QobuzClient &client, const char *track_id) +{ + return client.MakeUrl("track", "get", + { + {"track_id", track_id}, + }); +} + +QobuzTagScanner::QobuzTagScanner(QobuzClient &client, + const char *track_id, + RemoteTagHandler &_handler) + :request(client.GetCurl(), + MakeTrackUrl(client, track_id).c_str(), + *this), + handler(_handler) +{ +} + +QobuzTagScanner::~QobuzTagScanner() noexcept +{ + request.StopIndirect(); +} + +std::unique_ptr +QobuzTagScanner::MakeParser(unsigned status, + std::multimap &&headers) +{ + if (status != 200) + return std::make_unique(status, headers); + + auto i = headers.find("content-type"); + if (i == headers.end() || i->second.find("/json") == i->second.npos) + throw std::runtime_error("Not a JSON response from Qobuz"); + + return std::make_unique(); +} + +void +QobuzTagScanner::FinishParser(std::unique_ptr p) +{ + assert(dynamic_cast(p.get()) != nullptr); + auto &rp = (ResponseParser &)*p; + handler.OnRemoteTag(rp.GetTag()); +} + +void +QobuzTagScanner::OnError(std::exception_ptr e) noexcept +{ + handler.OnRemoteTagError(e); +} + +inline bool +QobuzTagScanner::ResponseParser::Integer(long long value) noexcept +{ + switch (state) { + case State::DURATION: + if (value > 0) + tag.SetDuration(SignedSongTime::FromS((unsigned)value)); + break; + + default: + break; + } + + return true; +} + +inline bool +QobuzTagScanner::ResponseParser::String(StringView value) noexcept +{ + switch (state) { + case State::TITLE: + if (map_depth == 1) + tag.AddItem(TAG_TITLE, value); + break; + + case State::COMPOSER_NAME: + if (map_depth == 2) + tag.AddItem(TAG_COMPOSER, value); + break; + + case State::ALBUM_TITLE: + if (map_depth == 2) + tag.AddItem(TAG_ALBUM, value); + break; + + case State::ALBUM_ARTIST_NAME: + if (map_depth == 3) + tag.AddItem(TAG_ALBUM_ARTIST, value); + break; + + case State::PERFORMER_NAME: + if (map_depth == 2) + tag.AddItem(TAG_PERFORMER, value); + break; + + default: + break; + } + + return true; +} + +inline bool +QobuzTagScanner::ResponseParser::StartMap() noexcept +{ + ++map_depth; + return true; +} + +inline bool +QobuzTagScanner::ResponseParser::MapKey(StringView value) noexcept +{ + switch (map_depth) { + case 1: + if (value.Equals("composer")) + state = State::COMPOSER; + else if (value.Equals("duration")) + state = State::DURATION; + else if (value.Equals("title")) + state = State::TITLE; + else if (value.Equals("album")) + state = State::ALBUM; + else if (value.Equals("performer")) + state = State::PERFORMER; + else + state = State::NONE; + break; + + case 2: + switch (state) { + case State::NONE: + case State::DURATION: + case State::TITLE: + break; + + case State::COMPOSER: + case State::COMPOSER_NAME: + if (value.Equals("name")) + state = State::COMPOSER_NAME; + else + state = State::COMPOSER; + break; + + case State::ALBUM: + case State::ALBUM_TITLE: + case State::ALBUM_ARTIST: + case State::ALBUM_ARTIST_NAME: + if (value.Equals("title")) + state = State::ALBUM_TITLE; + else if (value.Equals("artist")) + state = State::ALBUM_ARTIST; + else + state = State::ALBUM; + break; + + case State::PERFORMER: + case State::PERFORMER_NAME: + if (value.Equals("name")) + state = State::PERFORMER_NAME; + else + state = State::PERFORMER; + break; + + default: + break; + } + break; + + case 3: + switch (state) { + case State::ALBUM_ARTIST: + case State::ALBUM_ARTIST_NAME: + if (value.Equals("name")) + state = State::ALBUM_ARTIST_NAME; + else + state = State::ALBUM_ARTIST; + break; + + default: + break; + } + break; + } + + return true; +} + +inline bool +QobuzTagScanner::ResponseParser::EndMap() noexcept +{ + switch (map_depth) { + case 2: + state = State::NONE; + break; + + case 3: + switch (state) { + case State::ALBUM_TITLE: + case State::ALBUM_ARTIST: + case State::ALBUM_ARTIST_NAME: + state = State::ALBUM; + break; + + default: + break; + } + break; + } + + --map_depth; + + return true; +} diff --git a/src/input/plugins/QobuzTagScanner.hxx b/src/input/plugins/QobuzTagScanner.hxx new file mode 100644 index 000000000..eae715da1 --- /dev/null +++ b/src/input/plugins/QobuzTagScanner.hxx @@ -0,0 +1,60 @@ +/* + * 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 QOBUZ_TAG_SCANNER +#define QOBUZ_TAG_SCANNER + +#include "check.h" +#include "lib/curl/Delegate.hxx" +#include "lib/curl/Request.hxx" +#include "input/RemoteTagScanner.hxx" + +class QobuzClient; + +class QobuzTagScanner final + : public RemoteTagScanner, DelegateCurlResponseHandler +{ + CurlRequest request; + + RemoteTagHandler &handler; + +public: + class ResponseParser; + + QobuzTagScanner(QobuzClient &client, + const char *track_id, + RemoteTagHandler &_handler); + + ~QobuzTagScanner() noexcept override; + + void Start() noexcept override { + request.StartIndirect(); + } + +private: + /* virtual methods from DelegateCurlResponseHandler */ + std::unique_ptr MakeParser(unsigned status, + std::multimap &&headers) override; + void FinishParser(std::unique_ptr p) override; + + /* virtual methods from CurlResponseHandler */ + void OnError(std::exception_ptr e) noexcept override; +}; + +#endif