diff --git a/NEWS b/NEWS index 831a7aad4..f10c7c053 100644 --- a/NEWS +++ b/NEWS @@ -16,6 +16,19 @@ ver 0.23 (not yet released) - new tags "ComposerSort", "Ensemble", "Movement", "MovementNumber", and "Location" * new build-time dependency: libfmt +ver 0.22.10 (not yet released) +* protocol + - support "albumart" for virtual tracks in CUE sheets +* database + - simple: fix crash bug +* input + - curl: fix crash bug after stream with Icy metadata was closed by peer + - tidal: remove defunct unmaintained plugin +* tags + - fix crash caused by bug in TagBuilder and a few potential reference leaks +* output + - oss: fix channel order of multi-channel files + ver 0.22.9 (2021/06/23) * database - simple: load all .mpdignore files of all parent directories diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 6d55e5481..4cb565e82 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="58" + android:versionName="0.22.10"> diff --git a/doc/plugins.rst b/doc/plugins.rst index 7d7213fcc..ced0dd8cc 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -302,37 +302,6 @@ in the form ``qobuz://track/ID``, e.g.: * - **format_id N** - The `Qobuz format identifier `_, i.e. a number which chooses the format and quality to be requested from Qobuz. The default is "5" (320 kbit/s MP3). -tidal ------ - -Play songs from the commercial streaming service `Tidal -`_. It plays URLs in the form ``tidal://track/ID``, -e.g.: - -.. warning:: - - This plugin is currently defunct because Tidal has changed the - protocol and decided not to share documentation. - -.. code-block:: none - - mpc add tidal://track/59727857 - -.. list-table:: - :widths: 20 80 - :header-rows: 1 - - * - Setting - - Description - * - **token TOKEN** - - The Tidal application token. Since Tidal is unwilling to assign a token to MPD, this needs to be reverse-engineered from another (approved) Tidal client. - * - **username USERNAME** - - The Tidal user name. - * - **password PASSWORD** - - The Tidal password. - * - **audioquality Q** - - The Tidal "audioquality" parameter. Possible values: HI_RES, LOSSLESS, HIGH, LOW. Default is HIGH. - .. _decoder_plugins: Decoder plugins diff --git a/doc/user.rst b/doc/user.rst index 2acfbc0dc..2f1fb55f1 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -629,7 +629,8 @@ By default, all clients are unauthenticated and have a full set of permissions. * - **control** - Allows all other player and playlist manipulations. * - **admin** - - Allows database updates and allows shutting down :program:`MPD`. + - Allows manipulating outputs, stickers and partitions, + mounting/unmounting storage and shutting down :program:`MPD`. :code:`local_permissions` may be used to assign other permissions to clients connecting on a local socket. diff --git a/meson_options.txt b/meson_options.txt index 3f0b6f71f..523821bf2 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -107,7 +107,6 @@ option('smbclient', type: 'feature', value: 'disabled', description: 'SMB suppor option('qobuz', type: 'feature', description: 'Qobuz client') option('soundcloud', type: 'feature', description: 'SoundCloud client') -option('tidal', type: 'feature', description: 'Tidal client') # # Archive plugins diff --git a/src/command/FileCommands.cxx b/src/command/FileCommands.cxx index 0ccf494db..b5ad01f52 100644 --- a/src/command/FileCommands.cxx +++ b/src/command/FileCommands.cxx @@ -25,11 +25,15 @@ #include "client/Response.hxx" #include "util/CharUtil.hxx" #include "util/OffsetPointer.hxx" +#include "util/ScopeExit.hxx" +#include "util/StringCompare.hxx" #include "util/StringView.hxx" #include "util/UriExtract.hxx" #include "tag/Handler.hxx" #include "tag/Generic.hxx" #include "TagAny.hxx" +#include "db/Interface.hxx" +#include "song/LightSong.hxx" #include "storage/StorageInterface.hxx" #include "fs/AllocatedPath.hxx" #include "fs/FileInfo.hxx" @@ -174,10 +178,9 @@ find_stream_art(std::string_view directory, Mutex &mutex) } static CommandResult -read_stream_art(Response &r, const char *uri, size_t offset) +read_stream_art(Response &r, const std::string_view art_directory, + size_t offset) { - const auto art_directory = PathTraitsUTF8::GetParent(uri); - // TODO: eliminate this const_cast auto &client = const_cast(r.GetClient()); @@ -226,6 +229,41 @@ read_stream_art(Response &r, const char *uri, size_t offset) } #ifdef ENABLE_DATABASE + +/** + * Attempt to locate the "real" directory where the given song is + * stored. This attempts to resolve "virtual" directories/songs, + * e.g. expanded CUE sheet contents. + */ +[[gnu::pure]] +static std::string_view +RealDirectoryOfSong(Client &client, const char *song_uri, + std::string_view directory_uri) noexcept +try { + const auto *db = client.GetDatabase(); + if (db == nullptr) + return directory_uri; + + const auto *song = db->GetSong(song_uri); + if (song == nullptr) + return directory_uri; + + AtScopeExit(db, song) { db->ReturnSong(song); }; + + const char *real_uri = song->real_uri; + + /* this is a simplification which is just enough for CUE + sheets (but may be incomplete): for each "../", go one + level up */ + while ((real_uri = StringAfterPrefix(real_uri, "../")) != nullptr) + directory_uri = PathTraitsUTF8::GetParent(directory_uri); + + return directory_uri; +} catch (...) { + /* ignore all exceptions from Database::GetSong() */ + return directory_uri; +} + static CommandResult read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset) { @@ -235,7 +273,13 @@ read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset) return CommandResult::ERROR; } std::string uri2 = storage->MapUTF8(uri); - return read_stream_art(r, uri2.c_str(), offset); + + std::string_view directory_uri = + RealDirectoryOfSong(client, + uri, + PathTraitsUTF8::GetParent(uri2.c_str())); + + return read_stream_art(r, directory_uri, offset); } #endif @@ -256,7 +300,10 @@ handle_album_art(Client &client, Request args, Response &r) switch (located_uri.type) { case LocatedUri::Type::ABSOLUTE: case LocatedUri::Type::PATH: - return read_stream_art(r, located_uri.canonical_uri, offset); + return read_stream_art(r, + PathTraitsUTF8::GetParent(located_uri.canonical_uri), + offset); + case LocatedUri::Type::RELATIVE: #ifdef ENABLE_DATABASE return read_db_art(client, r, located_uri.canonical_uri, offset); diff --git a/src/db/plugins/simple/ExportedSong.hxx b/src/db/plugins/simple/ExportedSong.hxx index 9a2d54a85..31f3946f2 100644 --- a/src/db/plugins/simple/ExportedSong.hxx +++ b/src/db/plugins/simple/ExportedSong.hxx @@ -53,7 +53,7 @@ public: moved-from instance also owned the Tag which its LightSong::tag field refers to */ - OwnsTag() ? tag_buffer : src.tag), + src.OwnsTag() ? tag_buffer : src.tag), tag_buffer(std::move(src.tag_buffer)) {} ExportedSong &operator=(ExportedSong &&) = delete; diff --git a/src/fs/Traits.cxx b/src/fs/Traits.cxx index f3598fdc1..35d7fa1fd 100644 --- a/src/fs/Traits.cxx +++ b/src/fs/Traits.cxx @@ -84,6 +84,22 @@ GetParentPathImpl(typename Traits::const_pointer p) noexcept return {p, size_t(sep - p)}; } +template +typename Traits::string_view +GetParentPathImpl(typename Traits::string_view p) noexcept +{ + auto sep = Traits::FindLastSeparator(p); + if (sep == nullptr) + return Traits::CURRENT_DIRECTORY; + if (sep == p.data()) + return p.substr(0, 1); +#ifdef _WIN32 + if (Traits::IsDrive(p) && sep == p.data() + 2) + return p.substr(0, 3); +#endif + return p.substr(0, sep - p.data()); +} + template typename Traits::const_pointer RelativePathImpl(typename Traits::string_view base, @@ -166,6 +182,12 @@ PathTraitsFS::GetParent(PathTraitsFS::const_pointer p) noexcept return GetParentPathImpl(p); } +PathTraitsFS::string_view +PathTraitsFS::GetParent(string_view p) noexcept +{ + return GetParentPathImpl(p); +} + PathTraitsFS::const_pointer PathTraitsFS::Relative(string_view base, const_pointer other) noexcept { @@ -210,6 +232,12 @@ PathTraitsUTF8::GetParent(const_pointer p) noexcept return GetParentPathImpl(p); } +PathTraitsUTF8::string_view +PathTraitsUTF8::GetParent(string_view p) noexcept +{ + return GetParentPathImpl(p); +} + PathTraitsUTF8::const_pointer PathTraitsUTF8::Relative(string_view base, const_pointer other) noexcept { diff --git a/src/fs/Traits.hxx b/src/fs/Traits.hxx index 9f22e2f7a..a5495a29f 100644 --- a/src/fs/Traits.hxx +++ b/src/fs/Traits.hxx @@ -88,6 +88,18 @@ struct PathTraitsFS { #endif } + [[gnu::pure]] + static const_pointer FindLastSeparator(string_view p) noexcept { +#ifdef _WIN32 + const_pointer pos = p.data() + p.size(); + while (p.data() != pos && !IsSeparator(*pos)) + --pos; + return IsSeparator(*pos) ? pos : nullptr; +#else + return StringFindLast(p.data(), SEPARATOR, p.size()); +#endif + } + gcc_pure static const_pointer GetFilenameSuffix(const_pointer filename) noexcept { const_pointer dot = StringFindLast(filename, '.'); @@ -106,6 +118,10 @@ struct PathTraitsFS { static constexpr bool IsDrive(const_pointer p) noexcept { return IsAlphaASCII(p[0]) && p[1] == ':'; } + + static constexpr bool IsDrive(string_view p) noexcept { + return p.size() >= 2 && IsAlphaASCII(p[0]) && p[1] == ':'; + } #endif gcc_pure gcc_nonnull_all @@ -153,6 +169,9 @@ struct PathTraitsFS { gcc_pure gcc_nonnull_all static string_view GetParent(const_pointer p) noexcept; + [[gnu::pure]] + static string_view GetParent(string_view p) noexcept; + /** * Determine the relative part of the given path to this * object, not including the directory separator. Returns an @@ -212,6 +231,11 @@ struct PathTraitsUTF8 { return std::strrchr(p, SEPARATOR); } + [[gnu::pure]] + static const_pointer FindLastSeparator(string_view p) noexcept { + return StringFindLast(p.data(), SEPARATOR, p.size()); + } + gcc_pure static const_pointer GetFilenameSuffix(const_pointer filename) noexcept { const_pointer dot = StringFindLast(filename, '.'); @@ -230,6 +254,10 @@ struct PathTraitsUTF8 { static constexpr bool IsDrive(const_pointer p) noexcept { return IsAlphaASCII(p[0]) && p[1] == ':'; } + + static constexpr bool IsDrive(string_view p) noexcept { + return p.size() >= 2 && IsAlphaASCII(p[0]) && p[1] == ':'; + } #endif gcc_pure gcc_nonnull_all @@ -277,6 +305,9 @@ struct PathTraitsUTF8 { gcc_pure gcc_nonnull_all static string_view GetParent(const_pointer p) noexcept; + [[gnu::pure]] + static string_view GetParent(string_view p) noexcept; + /** * Determine the relative part of the given path to this * object, not including the directory separator. Returns an diff --git a/src/input/IcyInputStream.cxx b/src/input/IcyInputStream.cxx index f24399e85..46ebeaca4 100644 --- a/src/input/IcyInputStream.cxx +++ b/src/input/IcyInputStream.cxx @@ -104,8 +104,11 @@ IcyInputStream::Read(std::unique_lock &lock, while (true) { size_t nbytes = ProxyInputStream::Read(lock, ptr, read_size); - if (nbytes == 0) + if (nbytes == 0) { + assert(IsEOF()); + offset = override_offset; return 0; + } size_t result = parser->ParseInPlace(ptr, nbytes); if (result > 0) { diff --git a/src/input/InputStream.cxx b/src/input/InputStream.cxx index 5af7d681b..322138914 100644 --- a/src/input/InputStream.cxx +++ b/src/input/InputStream.cxx @@ -57,7 +57,6 @@ static bool ExpensiveSeeking(const char *uri) noexcept { return StringStartsWithCaseASCII(uri, "http://") || - StringStartsWithCaseASCII(uri, "tidal://") || StringStartsWithCaseASCII(uri, "qobuz://") || StringStartsWithCaseASCII(uri, "https://"); } diff --git a/src/input/Registry.cxx b/src/input/Registry.cxx index 73980b327..1c39f20a8 100644 --- a/src/input/Registry.cxx +++ b/src/input/Registry.cxx @@ -20,7 +20,6 @@ #include "Registry.hxx" #include "InputPlugin.hxx" #include "input/Features.h" -#include "plugins/TidalInputPlugin.hxx" #include "plugins/QobuzInputPlugin.hxx" #include "config.h" @@ -56,9 +55,6 @@ constexpr const InputPlugin *input_plugins[] = { #ifdef ENABLE_ALSA &input_plugin_alsa, #endif -#ifdef ENABLE_TIDAL - &tidal_input_plugin, -#endif #ifdef ENABLE_QOBUZ &qobuz_input_plugin, #endif diff --git a/src/input/plugins/TidalError.hxx b/src/input/plugins/TidalError.hxx deleted file mode 100644 index 036f12998..000000000 --- a/src/input/plugins/TidalError.hxx +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2003-2021 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 TIDAL_ERROR_HXX -#define TIDAL_ERROR_HXX - -#include - -/** - * An error condition reported by the server. - * - * See http://developer.tidal.com/technical/errors/ for details (login - * required). - */ -class TidalError : public std::runtime_error { - /** - * The HTTP status code. - */ - unsigned status; - - /** - * The Tidal-specific "subStatus". 0 if none was found in the - * JSON response. - */ - unsigned sub_status; - -public: - template - TidalError(unsigned _status, unsigned _sub_status, W &&_what) noexcept - :std::runtime_error(std::forward(_what)), - status(_status), sub_status(_sub_status) {} - - unsigned GetStatus() const noexcept { - return status; - } - - unsigned GetSubStatus() const noexcept { - return sub_status; - } - - bool IsInvalidSession() const noexcept { - return sub_status == 6001 || sub_status == 6002; - } -}; - -#endif diff --git a/src/input/plugins/TidalErrorParser.cxx b/src/input/plugins/TidalErrorParser.cxx deleted file mode 100644 index 8049113d4..000000000 --- a/src/input/plugins/TidalErrorParser.cxx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalErrorParser.hxx" -#include "TidalError.hxx" -#include "lib/yajl/Callbacks.hxx" -#include "util/RuntimeError.hxx" - -using Wrapper = Yajl::CallbacksWrapper; -static constexpr yajl_callbacks tidal_error_parser_callbacks = { - nullptr, - nullptr, - Wrapper::Integer, - nullptr, - nullptr, - Wrapper::String, - nullptr, - Wrapper::MapKey, - Wrapper::EndMap, - nullptr, - nullptr, -}; - -TidalErrorParser::TidalErrorParser(unsigned _status, - const std::multimap &headers) - :YajlResponseParser(&tidal_error_parser_callbacks, nullptr, this), - status(_status) -{ - auto i = headers.find("content-type"); - if (i == headers.end() || i->second.find("/json") == i->second.npos) - throw FormatRuntimeError("Status %u from Tidal", status); -} - -void -TidalErrorParser::OnEnd() -{ - YajlResponseParser::OnEnd(); - - char what[1024]; - - if (!message.empty()) - snprintf(what, sizeof(what), "Error from Tidal: %s", - message.c_str()); - else - snprintf(what, sizeof(what), "Status %u from Tidal", status); - - throw TidalError(status, sub_status, what); -} - -inline bool -TidalErrorParser::Integer(long long value) noexcept -{ - switch (state) { - case State::NONE: - case State::USER_MESSAGE: - break; - - case State::SUB_STATUS: - sub_status = value; - break; - } - - return true; -} - -inline bool -TidalErrorParser::String(StringView value) noexcept -{ - switch (state) { - case State::NONE: - case State::SUB_STATUS: - break; - - case State::USER_MESSAGE: - message.assign(value.data, value.size); - break; - } - - return true; -} - -inline bool -TidalErrorParser::MapKey(StringView value) noexcept -{ - if (value.Equals("userMessage")) - state = State::USER_MESSAGE; - else if (value.Equals("subStatus")) - state = State::SUB_STATUS; - else - state = State::NONE; - - return true; -} - -inline bool -TidalErrorParser::EndMap() noexcept -{ - state = State::NONE; - - return true; -} diff --git a/src/input/plugins/TidalErrorParser.hxx b/src/input/plugins/TidalErrorParser.hxx deleted file mode 100644 index 6291d130f..000000000 --- a/src/input/plugins/TidalErrorParser.hxx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2003-2021 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 TIDAL_ERROR_PARSER_HXX -#define TIDAL_ERROR_PARSER_HXX - -#include "lib/yajl/ResponseParser.hxx" - -#include -#include - -template struct ConstBuffer; -struct StringView; - -/** - * Parse an error JSON response and throw a #TidalError upon - * completion. - */ -class TidalErrorParser final : public YajlResponseParser { - const unsigned status; - - enum class State { - NONE, - USER_MESSAGE, - SUB_STATUS, - } state = State::NONE; - - unsigned sub_status = 0; - - std::string message; - -public: - /** - * May throw if there is a formal error in the response - * headers. - */ - TidalErrorParser(unsigned status, - const std::multimap &headers); - -protected: - /* virtual methods from CurlResponseParser */ - [[noreturn]] - void OnEnd() override; - -public: - /* yajl callbacks */ - bool Integer(long long value) noexcept; - bool String(StringView value) noexcept; - bool MapKey(StringView value) noexcept; - bool EndMap() noexcept; -}; - -#endif diff --git a/src/input/plugins/TidalInputPlugin.cxx b/src/input/plugins/TidalInputPlugin.cxx deleted file mode 100644 index 4a5a1e514..000000000 --- a/src/input/plugins/TidalInputPlugin.cxx +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalInputPlugin.hxx" -#include "TidalSessionManager.hxx" -#include "TidalTrackRequest.hxx" -#include "TidalTagScanner.hxx" -#include "TidalError.hxx" -#include "CurlInputPlugin.hxx" -#include "PluginUnavailable.hxx" -#include "input/ProxyInputStream.hxx" -#include "input/FailingInputStream.hxx" -#include "input/InputPlugin.hxx" -#include "lib/fmt/ExceptionFormatter.hxx" -#include "config/Block.hxx" -#include "thread/Mutex.hxx" -#include "util/Domain.hxx" -#include "util/StringCompare.hxx" -#include "Log.hxx" - -#include -#include - -static constexpr Domain tidal_domain("tidal"); - -static TidalSessionManager *tidal_session; -static const char *tidal_audioquality; - -class TidalInputStream final - : public ProxyInputStream, TidalSessionHandler, TidalTrackHandler { - - const std::string track_id; - - std::unique_ptr track_request; - - std::exception_ptr error; - - /** - * Retry to login if TidalError::IsInvalidSession() returns - * true? - */ - bool retry_login = true; - -public: - TidalInputStream(const char *_uri, const char *_track_id, - Mutex &_mutex) noexcept - :ProxyInputStream(_uri, _mutex), - track_id(_track_id) - { - tidal_session->AddLoginHandler(*this); - } - - ~TidalInputStream() override { - tidal_session->RemoveLoginHandler(*this); - } - - TidalInputStream(const TidalInputStream &) = delete; - TidalInputStream &operator=(const TidalInputStream &) = delete; - - /* virtual methods from InputStream */ - - void Check() override { - if (error) - std::rethrow_exception(error); - } - -private: - void Failed(const std::exception_ptr& e) { - SetInput(std::make_unique(GetURI(), e, - mutex)); - } - - /* virtual methods from TidalSessionHandler */ - void OnTidalSession() noexcept override; - - /* virtual methods from TidalTrackHandler */ - void OnTidalTrackSuccess(std::string url) noexcept override; - void OnTidalTrackError(std::exception_ptr error) noexcept override; -}; - -void -TidalInputStream::OnTidalSession() noexcept -{ - const std::lock_guard protect(mutex); - - try { - TidalTrackHandler &h = *this; - track_request = std::make_unique(tidal_session->GetCurl(), - tidal_session->GetBaseUrl(), - tidal_session->GetToken(), - tidal_session->GetSession().c_str(), - track_id.c_str(), - tidal_audioquality, - h); - track_request->Start(); - } catch (...) { - Failed(std::current_exception()); - } -} - -void -TidalInputStream::OnTidalTrackSuccess(std::string url) noexcept -{ - FmtDebug(tidal_domain, "Tidal track '{}' resolves to {}", - track_id, url); - - const std::lock_guard protect(mutex); - - track_request.reset(); - - try { - SetInput(OpenCurlInputStream(url.c_str(), {}, - mutex)); - } catch (...) { - Failed(std::current_exception()); - } -} - -gcc_pure -static bool -IsInvalidSession(std::exception_ptr e) noexcept -{ - try { - std::rethrow_exception(std::move(e)); - } catch (const TidalError &te) { - return te.IsInvalidSession(); - } catch (...) { - return false; - } -} - -void -TidalInputStream::OnTidalTrackError(std::exception_ptr e) noexcept -{ - const std::lock_guard protect(mutex); - - if (retry_login && IsInvalidSession(e)) { - /* the session has expired - obtain a new session id - by logging in again */ - - FmtInfo(tidal_domain, - "Session expired ('{}'), retrying to log in", e); - - retry_login = false; - tidal_session->AddLoginHandler(*this); - return; - } - - Failed(e); -} - -static void -InitTidalInput(EventLoop &event_loop, const ConfigBlock &block) -{ - const char *base_url = block.GetBlockValue("base_url", - "https://api.tidal.com/v1"); - - const char *token = block.GetBlockValue("token"); - if (token == nullptr) - throw PluginUnconfigured("No Tidal application token configured"); - - const char *username = block.GetBlockValue("username"); - if (username == nullptr) - throw PluginUnconfigured("No Tidal username configured"); - - const char *password = block.GetBlockValue("password"); - if (password == nullptr) - throw PluginUnconfigured("No Tidal password configured"); - - LogWarning(tidal_domain, - "The Tidal input plugin is deprecated because Tidal has changed the protocol and doesn't share documentation"); - - tidal_audioquality = block.GetBlockValue("audioquality", "HIGH"); - - tidal_session = new TidalSessionManager(event_loop, base_url, token, - username, password); -} - -static void -FinishTidalInput() noexcept -{ - delete tidal_session; -} - -gcc_pure -static const char * -ExtractTidalTrackId(const char *uri) -{ - const char *track_id = StringAfterPrefix(uri, "tidal://track/"); - if (track_id == nullptr) { - track_id = StringAfterPrefix(uri, "https://listen.tidal.com/track/"); - if (track_id == nullptr) - return nullptr; - } - - if (*track_id == 0) - return nullptr; - - return track_id; -} - -static InputStreamPtr -OpenTidalInput(const char *uri, Mutex &mutex) -{ - assert(tidal_session != nullptr); - - const char *track_id = ExtractTidalTrackId(uri); - if (track_id == nullptr) - return nullptr; - - // TODO: validate track_id - - return std::make_unique(uri, track_id, mutex); -} - -static std::unique_ptr -ScanTidalTags(const char *uri, RemoteTagHandler &handler) -{ - assert(tidal_session != nullptr); - - const char *track_id = ExtractTidalTrackId(uri); - if (track_id == nullptr) - return nullptr; - - return std::make_unique(tidal_session->GetCurl(), - tidal_session->GetBaseUrl(), - tidal_session->GetToken(), - track_id, handler); -} - -static constexpr const char *tidal_prefixes[] = { - "tidal://", - nullptr -}; - -const InputPlugin tidal_input_plugin = { - "tidal", - tidal_prefixes, - InitTidalInput, - FinishTidalInput, - OpenTidalInput, - nullptr, - ScanTidalTags, -}; diff --git a/src/input/plugins/TidalInputPlugin.hxx b/src/input/plugins/TidalInputPlugin.hxx deleted file mode 100644 index ba88bf68b..000000000 --- a/src/input/plugins/TidalInputPlugin.hxx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2003-2021 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 INPUT_TIDAL_HXX -#define INPUT_TIDAL_HXX - -extern const struct InputPlugin tidal_input_plugin; - -#endif diff --git a/src/input/plugins/TidalLoginRequest.cxx b/src/input/plugins/TidalLoginRequest.cxx deleted file mode 100644 index ae7133734..000000000 --- a/src/input/plugins/TidalLoginRequest.cxx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalLoginRequest.hxx" -#include "TidalErrorParser.hxx" -#include "lib/curl/Form.hxx" -#include "lib/yajl/Callbacks.hxx" -#include "lib/yajl/ResponseParser.hxx" - -#include - -using Wrapper = Yajl::CallbacksWrapper; -static constexpr yajl_callbacks parse_callbacks = { - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - Wrapper::String, - nullptr, - Wrapper::MapKey, - Wrapper::EndMap, - nullptr, - nullptr, -}; - -class TidalLoginRequest::ResponseParser final : public YajlResponseParser { - enum class State { - NONE, - SESSION_ID, - } state = State::NONE; - - std::string session; - -public: - explicit ResponseParser() noexcept - :YajlResponseParser(&parse_callbacks, nullptr, this) {} - - std::string &&GetSession() { - if (session.empty()) - throw std::runtime_error("No sessionId in login response"); - - return std::move(session); - } - - /* yajl callbacks */ - bool String(StringView value) noexcept; - bool MapKey(StringView value) noexcept; - bool EndMap() noexcept; -}; - -static std::string -MakeLoginUrl(const char *base_url) -{ - return std::string(base_url) + "/login/username"; -} - -TidalLoginRequest::TidalLoginRequest(CurlGlobal &curl, - const char *base_url, const char *token, - const char *username, const char *password, - TidalLoginHandler &_handler) - :request(curl, MakeLoginUrl(base_url).c_str(), *this), - handler(_handler) -{ - request_headers.Append((std::string("X-Tidal-Token:") - + token).c_str()); - request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get()); - - request.SetOption(CURLOPT_COPYPOSTFIELDS, - EncodeForm(request.Get(), - {{"username", username}, {"password", password}}).c_str()); -} - -TidalLoginRequest::~TidalLoginRequest() noexcept -{ - request.StopIndirect(); -} - -std::unique_ptr -TidalLoginRequest::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 Tidal"); - - return std::make_unique(); -} - -void -TidalLoginRequest::FinishParser(std::unique_ptr p) -{ - assert(dynamic_cast(p.get()) != nullptr); - auto &rp = (ResponseParser &)*p; - handler.OnTidalLoginSuccess(rp.GetSession()); -} - -void -TidalLoginRequest::OnError(std::exception_ptr e) noexcept -{ - handler.OnTidalLoginError(e); -} - -inline bool -TidalLoginRequest::ResponseParser::String(StringView value) noexcept -{ - switch (state) { - case State::NONE: - break; - - case State::SESSION_ID: - session.assign(value.data, value.size); - break; - } - - return true; -} - -inline bool -TidalLoginRequest::ResponseParser::MapKey(StringView value) noexcept -{ - if (value.Equals("sessionId")) - state = State::SESSION_ID; - else - state = State::NONE; - - return true; -} - -inline bool -TidalLoginRequest::ResponseParser::EndMap() noexcept -{ - state = State::NONE; - - return true; -} diff --git a/src/input/plugins/TidalLoginRequest.hxx b/src/input/plugins/TidalLoginRequest.hxx deleted file mode 100644 index b830b2f2c..000000000 --- a/src/input/plugins/TidalLoginRequest.hxx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2003-2021 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 TIDAL_LOGIN_REQUEST_HXX -#define TIDAL_LOGIN_REQUEST_HXX - -#include "lib/curl/Delegate.hxx" -#include "lib/curl/Slist.hxx" -#include "lib/curl/Request.hxx" - -/** - * Callback class for #TidalLoginRequest. - * - * Its methods must be thread-safe. - */ -class TidalLoginHandler { -public: - virtual void OnTidalLoginSuccess(std::string session) noexcept = 0; - virtual void OnTidalLoginError(std::exception_ptr error) noexcept = 0; -}; - -/** - * An asynchronous Tidal "login/username" request. - * - * After construction, call Start() to initiate the request. - */ -class TidalLoginRequest final : DelegateCurlResponseHandler { - CurlSlist request_headers; - - CurlRequest request; - - TidalLoginHandler &handler; - -public: - class ResponseParser; - - TidalLoginRequest(CurlGlobal &curl, - const char *base_url, const char *token, - const char *username, const char *password, - TidalLoginHandler &_handler); - - ~TidalLoginRequest() noexcept; - - void Start() { - 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 diff --git a/src/input/plugins/TidalSessionManager.cxx b/src/input/plugins/TidalSessionManager.cxx deleted file mode 100644 index 5ee971b9a..000000000 --- a/src/input/plugins/TidalSessionManager.cxx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalSessionManager.hxx" -#include "util/Domain.hxx" - -#include "Log.hxx" - -static constexpr Domain tidal_domain("tidal"); - -TidalSessionManager::TidalSessionManager(EventLoop &event_loop, - const char *_base_url, const char *_token, - const char *_username, - const char *_password) - :base_url(_base_url), token(_token), - username(_username), password(_password), - curl(event_loop), - defer_invoke_handlers(event_loop, - BIND_THIS_METHOD(InvokeHandlers)) -{ -} - -TidalSessionManager::~TidalSessionManager() noexcept -{ - assert(handlers.empty()); -} - -void -TidalSessionManager::AddLoginHandler(TidalSessionHandler &h) noexcept -{ - const std::lock_guard protect(mutex); - assert(!h.is_linked()); - - const bool was_empty = handlers.empty(); - handlers.push_front(h); - - if (!was_empty || login_request) - return; - - if (session.empty()) { - // TODO: throttle login attempts? - - LogDebug(tidal_domain, "Sending login request"); - - std::string login_uri(base_url); - login_uri += "/login/username"; - - try { - TidalLoginHandler &handler = *this; - login_request = - std::make_unique(*curl, base_url, - token, - username, password, - handler); - login_request->Start(); - } catch (...) { - error = std::current_exception(); - ScheduleInvokeHandlers(); - return; - } - } else - ScheduleInvokeHandlers(); -} - -void -TidalSessionManager::OnTidalLoginSuccess(std::string _session) noexcept -{ - FmtDebug(tidal_domain, "Login successful, session={}", _session); - - { - const std::lock_guard protect(mutex); - login_request.reset(); - session = std::move(_session); - } - - ScheduleInvokeHandlers(); -} - -void -TidalSessionManager::OnTidalLoginError(std::exception_ptr e) noexcept -{ - { - const std::lock_guard protect(mutex); - login_request.reset(); - error = e; - } - - ScheduleInvokeHandlers(); -} - -void -TidalSessionManager::InvokeHandlers() noexcept -{ - const std::lock_guard protect(mutex); - while (!handlers.empty()) { - auto &h = handlers.front(); - handlers.pop_front(); - - const ScopeUnlock unlock(mutex); - h.OnTidalSession(); - } -} diff --git a/src/input/plugins/TidalSessionManager.hxx b/src/input/plugins/TidalSessionManager.hxx deleted file mode 100644 index aee38102e..000000000 --- a/src/input/plugins/TidalSessionManager.hxx +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2003-2021 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 TIDAL_SESSION_MANAGER_HXX -#define TIDAL_SESSION_MANAGER_HXX - -#include "TidalLoginRequest.hxx" -#include "lib/curl/Init.hxx" -#include "thread/Mutex.hxx" -#include "event/DeferEvent.hxx" -#include "util/IntrusiveList.hxx" - -#include -#include - -/** - * Callback class for #TidalSessionManager. - * - * Its methods must be thread-safe. - */ -class TidalSessionHandler - : public SafeLinkIntrusiveListHook -{ -public: - /** - * TidalSessionHandler::AddLoginHandler() has completed - * (successful or failed). This method may now call - * #TidalSessionHandler::GetSession(). - */ - virtual void OnTidalSession() noexcept = 0; -}; - -class TidalSessionManager final : TidalLoginHandler { - /** - * The Tidal API base URL. - */ - const char *const base_url; - - /** - * The configured Tidal application token. - */ - const char *const token; - - /** - * The configured Tidal user name. - */ - const char *const username; - - /** - * The configured Tidal password. - */ - const char *const password; - - CurlInit curl; - - DeferEvent defer_invoke_handlers; - - /** - * Protects #session, #error and #handlers. - */ - mutable Mutex mutex; - - std::exception_ptr error; - - /** - * The current Tidal session id, empty if none. - */ - std::string session; - - using LoginHandlerList = IntrusiveList; - - LoginHandlerList handlers; - - std::unique_ptr login_request; - -public: - TidalSessionManager(EventLoop &event_loop, - const char *_base_url, const char *_token, - const char *_username, - const char *_password); - - ~TidalSessionManager() noexcept; - - auto &GetEventLoop() const noexcept { - return defer_invoke_handlers.GetEventLoop(); - } - - CurlGlobal &GetCurl() noexcept { - return *curl; - } - - const char *GetBaseUrl() const noexcept { - return base_url; - } - - /** - * Ask the object to call back once the login to Tidal has - * completed. If no session exists currently, then one is - * created. Since the callback may occur in another thread, - * the it may have been completed already before this method - * returns. - */ - void AddLoginHandler(TidalSessionHandler &h) noexcept; - - void RemoveLoginHandler(TidalSessionHandler &h) noexcept { - const std::lock_guard protect(mutex); - if (h.is_linked()) - h.unlink(); - } - - const char *GetToken() const noexcept { - return token; - } - - /** - * Get the Tidal session id, or rethrows an exception if an - * error has occurred while logging in. - */ - std::string GetSession() const { - const std::lock_guard protect(mutex); - - if (error) - std::rethrow_exception(error); - - if (session.empty()) - throw std::runtime_error("No session"); - - return session; - } - -private: - void InvokeHandlers() noexcept; - - void ScheduleInvokeHandlers() noexcept { - defer_invoke_handlers.Schedule(); - } - - /* virtual methods from TidalLoginHandler */ - void OnTidalLoginSuccess(std::string session) noexcept override; - void OnTidalLoginError(std::exception_ptr error) noexcept override; -}; - -#endif diff --git a/src/input/plugins/TidalTagScanner.cxx b/src/input/plugins/TidalTagScanner.cxx deleted file mode 100644 index 4b345dc35..000000000 --- a/src/input/plugins/TidalTagScanner.cxx +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalTagScanner.hxx" -#include "TidalErrorParser.hxx" -#include "lib/yajl/Callbacks.hxx" -#include "tag/Builder.hxx" -#include "tag/Tag.hxx" - -#include - -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 TidalTagScanner::ResponseParser final : public YajlResponseParser { - enum class State { - NONE, - TITLE, - DURATION, - ARTIST, - ARTIST_NAME, - ALBUM, - ALBUM_TITLE, - } 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(const char *base_url, const char *track_id) -{ - return std::string(base_url) - + "/tracks/" - + track_id - // TODO: configurable countryCode? - + "?countryCode=US"; -} - -TidalTagScanner::TidalTagScanner(CurlGlobal &curl, - const char *base_url, const char *token, - const char *track_id, - RemoteTagHandler &_handler) - :request(curl, MakeTrackUrl(base_url, track_id).c_str(), *this), - handler(_handler) -{ - request_headers.Append((std::string("X-Tidal-Token:") - + token).c_str()); - request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get()); -} - -TidalTagScanner::~TidalTagScanner() noexcept -{ - request.StopIndirect(); -} - -std::unique_ptr -TidalTagScanner::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 Tidal"); - - return std::make_unique(); -} - -void -TidalTagScanner::FinishParser(std::unique_ptr p) -{ - assert(dynamic_cast(p.get()) != nullptr); - auto &rp = (ResponseParser &)*p; - handler.OnRemoteTag(rp.GetTag()); -} - -void -TidalTagScanner::OnError(std::exception_ptr e) noexcept -{ - handler.OnRemoteTagError(e); -} - -inline bool -TidalTagScanner::ResponseParser::Integer(long long value) noexcept -{ - switch (state) { - case State::NONE: - case State::TITLE: - case State::ARTIST: - case State::ARTIST_NAME: - case State::ALBUM: - case State::ALBUM_TITLE: - break; - - case State::DURATION: - if (map_depth == 1 && value > 0) - tag.SetDuration(SignedSongTime::FromS((unsigned)value)); - break; - } - - return true; -} - -inline bool -TidalTagScanner::ResponseParser::String(StringView value) noexcept -{ - switch (state) { - case State::NONE: - case State::DURATION: - case State::ARTIST: - case State::ALBUM: - break; - - case State::TITLE: - if (map_depth == 1) - tag.AddItem(TAG_TITLE, value); - break; - - case State::ARTIST_NAME: - if (map_depth == 2) - tag.AddItem(TAG_ARTIST, value); - break; - - case State::ALBUM_TITLE: - if (map_depth == 2) - tag.AddItem(TAG_ALBUM, value); - break; - } - - return true; -} - -inline bool -TidalTagScanner::ResponseParser::StartMap() noexcept -{ - ++map_depth; - return true; -} - -inline bool -TidalTagScanner::ResponseParser::MapKey(StringView value) noexcept -{ - switch (map_depth) { - case 1: - if (value.Equals("title")) - state = State::TITLE; - else if (value.Equals("duration")) - state = State::DURATION; - else if (value.Equals("artist")) - state = State::ARTIST; - else if (value.Equals("album")) - state = State::ALBUM; - else - state = State::NONE; - break; - - case 2: - switch (state) { - case State::NONE: - case State::TITLE: - case State::DURATION: - break; - - case State::ARTIST: - case State::ARTIST_NAME: - if (value.Equals("name")) - state = State::ARTIST_NAME; - else - state = State::ARTIST; - break; - - case State::ALBUM: - case State::ALBUM_TITLE: - if (value.Equals("title")) - state = State::ALBUM_TITLE; - else - state = State::ALBUM; - break; - } - break; - } - - return true; -} - -inline bool -TidalTagScanner::ResponseParser::EndMap() noexcept -{ - switch (map_depth) { - case 2: - state = State::NONE; - break; - } - - --map_depth; - - return true; -} diff --git a/src/input/plugins/TidalTagScanner.hxx b/src/input/plugins/TidalTagScanner.hxx deleted file mode 100644 index b7c8cd3bc..000000000 --- a/src/input/plugins/TidalTagScanner.hxx +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2003-2021 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 TIDAL_TAG_SCANNER_HXX -#define TIDAL_TAG_SCANNER_HXX - -#include "lib/curl/Delegate.hxx" -#include "lib/curl/Slist.hxx" -#include "lib/curl/Request.hxx" -#include "input/RemoteTagScanner.hxx" - -class TidalTagScanner final - : public RemoteTagScanner, DelegateCurlResponseHandler -{ - CurlSlist request_headers; - - CurlRequest request; - - RemoteTagHandler &handler; - -public: - class ResponseParser; - - TidalTagScanner(CurlGlobal &curl, - const char *base_url, const char *token, - const char *track_id, - RemoteTagHandler &_handler); - - ~TidalTagScanner() noexcept override; - - void Start() 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 diff --git a/src/input/plugins/TidalTrackRequest.cxx b/src/input/plugins/TidalTrackRequest.cxx deleted file mode 100644 index bced95ef9..000000000 --- a/src/input/plugins/TidalTrackRequest.cxx +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2003-2021 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 "TidalTrackRequest.hxx" -#include "TidalErrorParser.hxx" -#include "lib/yajl/Callbacks.hxx" - -#include - -using Wrapper = Yajl::CallbacksWrapper; -static constexpr yajl_callbacks parse_callbacks = { - nullptr, - nullptr, - nullptr, - nullptr, - nullptr, - Wrapper::String, - nullptr, - Wrapper::MapKey, - Wrapper::EndMap, - nullptr, - nullptr, -}; - -class TidalTrackRequest::ResponseParser final : public YajlResponseParser { - enum class State { - NONE, - URLS, - } state = State::NONE; - - std::string url; - -public: - explicit ResponseParser() noexcept - :YajlResponseParser(&parse_callbacks, nullptr, this) {} - - std::string &&GetUrl() { - if (url.empty()) - throw std::runtime_error("No url in track response"); - - return std::move(url); - } - - /* yajl callbacks */ - bool String(StringView value) noexcept; - bool MapKey(StringView value) noexcept; - bool EndMap() noexcept; -}; - -static std::string -MakeTrackUrl(const char *base_url, const char *track_id, - const char *audioquality) noexcept -{ - return std::string(base_url) - + "/tracks/" - + track_id - + "/urlpostpaywall?assetpresentation=FULL&audioquality=" - + audioquality + "&urlusagemode=STREAM"; -} - -TidalTrackRequest::TidalTrackRequest(CurlGlobal &curl, - const char *base_url, const char *token, - const char *session, - const char *track_id, - const char *audioquality, - TidalTrackHandler &_handler) - :request(curl, MakeTrackUrl(base_url, track_id, audioquality).c_str(), - *this), - handler(_handler) -{ - request_headers.Append((std::string("X-Tidal-Token:") - + token).c_str()); - request_headers.Append((std::string("X-Tidal-SessionId:") - + session).c_str()); - request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get()); -} - -TidalTrackRequest::~TidalTrackRequest() noexcept -{ - request.StopIndirect(); -} - -std::unique_ptr -TidalTrackRequest::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 Tidal"); - - return std::make_unique(); -} - -void -TidalTrackRequest::FinishParser(std::unique_ptr p) -{ - assert(dynamic_cast(p.get()) != nullptr); - auto &rp = (ResponseParser &)*p; - handler.OnTidalTrackSuccess(rp.GetUrl()); -} - -void -TidalTrackRequest::OnError(std::exception_ptr e) noexcept -{ - handler.OnTidalTrackError(e); -} - -inline bool -TidalTrackRequest::ResponseParser::String(StringView value) noexcept -{ - switch (state) { - case State::NONE: - break; - - case State::URLS: - if (url.empty()) - url.assign(value.data, value.size); - break; - } - - return true; -} - -inline bool -TidalTrackRequest::ResponseParser::MapKey(StringView value) noexcept -{ - if (value.Equals("urls")) - state = State::URLS; - else - state = State::NONE; - - return true; -} - -inline bool -TidalTrackRequest::ResponseParser::EndMap() noexcept -{ - state = State::NONE; - - return true; -} diff --git a/src/input/plugins/TidalTrackRequest.hxx b/src/input/plugins/TidalTrackRequest.hxx deleted file mode 100644 index 44f3dad6b..000000000 --- a/src/input/plugins/TidalTrackRequest.hxx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2003-2021 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 TIDAL_TRACK_REQUEST_HXX -#define TIDAL_TRACK_REQUEST_HXX - -#include "lib/curl/Delegate.hxx" -#include "lib/curl/Slist.hxx" -#include "lib/curl/Request.hxx" - -/** - * Callback class for #TidalTrackRequest. - * - * Its methods must be thread-safe. - */ -class TidalTrackHandler { -public: - virtual void OnTidalTrackSuccess(std::string url) noexcept = 0; - virtual void OnTidalTrackError(std::exception_ptr error) noexcept = 0; -}; - -/** - * An asynchronous request for the streaming URL of a Tidal track. - * - * After construction, call Start() to initiate the request. - */ -class TidalTrackRequest final : DelegateCurlResponseHandler { - CurlSlist request_headers; - - CurlRequest request; - - TidalTrackHandler &handler; - -public: - class ResponseParser; - - TidalTrackRequest(CurlGlobal &curl, - const char *base_url, const char *token, - const char *session, - const char *track_id, - const char *audioquality, - TidalTrackHandler &_handler); - - ~TidalTrackRequest() noexcept; - - void Start() { - 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 diff --git a/src/input/plugins/meson.build b/src/input/plugins/meson.build index 73223654f..9802e942e 100644 --- a/src/input/plugins/meson.build +++ b/src/input/plugins/meson.build @@ -63,27 +63,6 @@ if enable_qobuz ] endif -tidal_feature = get_option('tidal') -if tidal_feature.disabled() - enable_tidal = false -else - enable_tidal = curl_dep.found() and yajl_dep.found() - if not enable_tidal and tidal_feature.enabled() - error('Tidal requires CURL and libyajl') - endif -endif -input_features.set('ENABLE_TIDAL', enable_tidal) -if enable_tidal - input_plugins_sources += [ - 'TidalErrorParser.cxx', - 'TidalLoginRequest.cxx', - 'TidalSessionManager.cxx', - 'TidalTrackRequest.cxx', - 'TidalTagScanner.cxx', - 'TidalInputPlugin.cxx', - ] -endif - input_plugins = static_library( 'input_plugins', input_plugins_sources, diff --git a/src/output/plugins/OssOutputPlugin.cxx b/src/output/plugins/OssOutputPlugin.cxx index 4f65d95db..43905b9f6 100644 --- a/src/output/plugins/OssOutputPlugin.cxx +++ b/src/output/plugins/OssOutputPlugin.cxx @@ -20,11 +20,13 @@ #include "OssOutputPlugin.hxx" #include "../OutputAPI.hxx" #include "mixer/MixerList.hxx" +#include "pcm/Export.hxx" #include "io/UniqueFileDescriptor.hxx" #include "system/Error.hxx" #include "util/ConstBuffer.hxx" #include "util/Domain.hxx" #include "util/ByteOrder.hxx" +#include "util/Manual.hxx" #include "Log.hxx" #include @@ -53,15 +55,8 @@ #undef AFMT_S24_NE #endif -#ifdef AFMT_S24_PACKED -#include "pcm/Export.hxx" -#include "util/Manual.hxx" -#endif - class OssOutput final : AudioOutput { -#ifdef AFMT_S24_PACKED Manual pcm_export; -#endif FileDescriptor fd = FileDescriptor::Undefined(); const char *device; @@ -78,11 +73,7 @@ class OssOutput final : AudioOutput { */ int oss_format; -#ifdef AFMT_S24_PACKED static constexpr unsigned oss_flags = FLAG_ENABLE_DISABLE; -#else - static constexpr unsigned oss_flags = 0; -#endif public: explicit OssOutput(const char *_device=nullptr) @@ -92,7 +83,6 @@ public: static AudioOutput *Create(EventLoop &event_loop, const ConfigBlock &block); -#ifdef AFMT_S24_PACKED void Enable() override { pcm_export.Construct(); } @@ -100,7 +90,6 @@ public: void Disable() noexcept override { pcm_export.Destruct(); } -#endif void Open(AudioFormat &audio_format) override; @@ -428,11 +417,8 @@ sample_format_from_oss(int format) noexcept static bool oss_probe_sample_format(FileDescriptor fd, SampleFormat sample_format, SampleFormat *sample_format_r, - int *oss_format_r -#ifdef AFMT_S24_PACKED - , PcmExport &pcm_export -#endif - ) + int *oss_format_r, + PcmExport &pcm_export) { int oss_format = sample_format_to_oss(sample_format); if (oss_format == AFMT_QUERY) @@ -464,15 +450,15 @@ oss_probe_sample_format(FileDescriptor fd, SampleFormat sample_format, *sample_format_r = sample_format; *oss_format_r = oss_format; -#ifdef AFMT_S24_PACKED PcmExport::Params params; params.alsa_channel_order = true; +#ifdef AFMT_S24_PACKED params.pack24 = oss_format == AFMT_S24_PACKED; params.reverse_endian = oss_format == AFMT_S24_PACKED && !IsLittleEndian(); +#endif pcm_export.Open(sample_format, 0, params); -#endif return true; } @@ -483,19 +469,13 @@ oss_probe_sample_format(FileDescriptor fd, SampleFormat sample_format, */ static void oss_setup_sample_format(FileDescriptor fd, AudioFormat &audio_format, - int *oss_format_r -#ifdef AFMT_S24_PACKED - , PcmExport &pcm_export -#endif - ) + int *oss_format_r, + PcmExport &pcm_export) { SampleFormat mpd_format; if (oss_probe_sample_format(fd, audio_format.format, - &mpd_format, oss_format_r -#ifdef AFMT_S24_PACKED - , pcm_export -#endif - )) { + &mpd_format, oss_format_r, + pcm_export)) { audio_format.format = mpd_format; return; } @@ -518,11 +498,8 @@ oss_setup_sample_format(FileDescriptor fd, AudioFormat &audio_format, continue; if (oss_probe_sample_format(fd, mpd_format, - &mpd_format, oss_format_r -#ifdef AFMT_S24_PACKED - , pcm_export -#endif - )) { + &mpd_format, oss_format_r, + pcm_export)) { audio_format.format = mpd_format; return; } @@ -536,11 +513,7 @@ OssOutput::Setup(AudioFormat &_audio_format) { oss_setup_channels(fd, _audio_format); oss_setup_sample_rate(fd, _audio_format); - oss_setup_sample_format(fd, _audio_format, &oss_format -#ifdef AFMT_S24_PACKED - , pcm_export -#endif - ); + oss_setup_sample_format(fd, _audio_format, &oss_format, pcm_export); } /** @@ -595,9 +568,7 @@ OssOutput::Cancel() noexcept DoClose(); } -#ifdef AFMT_S24_PACKED pcm_export->Reset(); -#endif } size_t @@ -611,23 +582,17 @@ OssOutput::Play(const void *chunk, size_t size) if (!fd.IsDefined()) Reopen(); -#ifdef AFMT_S24_PACKED const auto e = pcm_export->Export({chunk, size}); if (e.empty()) return size; chunk = e.data; size = e.size; -#endif while (true) { ret = fd.Write(chunk, size); - if (ret > 0) { -#ifdef AFMT_S24_PACKED - ret = pcm_export->CalcInputSize(ret); -#endif - return ret; - } + if (ret > 0) + return pcm_export->CalcInputSize(ret); if (ret < 0 && errno != EINTR) throw FormatErrno("Write error on %s", device); diff --git a/src/tag/Builder.cxx b/src/tag/Builder.cxx index 5a60a6e78..b2887c903 100644 --- a/src/tag/Builder.cxx +++ b/src/tag/Builder.cxx @@ -36,10 +36,12 @@ TagBuilder::TagBuilder(const Tag &other) noexcept { items.reserve(other.num_items); - const std::lock_guard protect(tag_pool_lock); - - for (unsigned i = 0, n = other.num_items; i != n; ++i) - items.push_back(tag_pool_dup_item(other.items[i])); + const std::size_t n = other.num_items; + if (n > 0) { + const std::lock_guard protect(tag_pool_lock); + for (std::size_t i = 0; i != n; ++i) + items.push_back(tag_pool_dup_item(other.items[i])); + } } TagBuilder::TagBuilder(Tag &&other) noexcept @@ -63,12 +65,17 @@ TagBuilder::operator=(const TagBuilder &other) noexcept /* copy all attributes */ duration = other.duration; has_playlist = other.has_playlist; - items = other.items; - /* increment the tag pool refcounters */ - const std::lock_guard protect(tag_pool_lock); - for (auto i : items) - tag_pool_dup_item(i); + RemoveAll(); + + if (!other.items.empty()) { + items = other.items; + + /* increment the tag pool refcounters */ + const std::lock_guard protect(tag_pool_lock); + for (auto &i : items) + i = tag_pool_dup_item(i); + } return *this; } @@ -76,9 +83,14 @@ TagBuilder::operator=(const TagBuilder &other) noexcept TagBuilder & TagBuilder::operator=(TagBuilder &&other) noexcept { + using std::swap; + duration = other.duration; has_playlist = other.has_playlist; - items = std::move(other.items); + + /* swap the two TagItem lists so we don't need to touch the + tag pool just yet */ + swap(items, other.items); return *this; } @@ -92,7 +104,7 @@ TagBuilder::operator=(Tag &&other) noexcept /* move all TagItem pointers from the Tag object; we don't need to contact the tag pool, because all we do is move references */ - items.clear(); + RemoveAll(); items.reserve(other.num_items); std::copy_n(other.items, other.num_items, std::back_inserter(items)); @@ -174,11 +186,14 @@ TagBuilder::Complement(const Tag &other) noexcept items.reserve(items.size() + other.num_items); - const std::lock_guard protect(tag_pool_lock); - for (unsigned i = 0, n = other.num_items; i != n; ++i) { - TagItem *item = other.items[i]; - if (!present[item->type]) - items.push_back(tag_pool_dup_item(item)); + const std::size_t n = other.num_items; + if (n > 0) { + const std::lock_guard protect(tag_pool_lock); + for (std::size_t i = 0; i != n; ++i) { + TagItem *item = other.items[i]; + if (!present[item->type]) + items.push_back(tag_pool_dup_item(item)); + } } } @@ -238,6 +253,11 @@ TagBuilder::AddEmptyItem(TagType type) noexcept void TagBuilder::RemoveAll() noexcept { + if (items.empty()) + /* don't acquire the tag_pool_lock if we're not going + to call tag_pool_put_item() anyway */ + return; + { const std::lock_guard protect(tag_pool_lock); for (auto i : items) diff --git a/src/tag/Pool.hxx b/src/tag/Pool.hxx index 54df15c89..9ec9ab0cb 100644 --- a/src/tag/Pool.hxx +++ b/src/tag/Pool.hxx @@ -28,9 +28,11 @@ extern Mutex tag_pool_lock; struct TagItem; struct StringView; +[[nodiscard]] TagItem * tag_pool_get_item(TagType type, StringView value) noexcept; +[[nodiscard]] TagItem * tag_pool_dup_item(TagItem *item) noexcept;