Compare commits

...

33 Commits

Author SHA1 Message Date
Max Kellermann
64c39af556 release v0.22.10 2021-08-06 18:16:59 +02:00
Max Kellermann
04eb911a51 mixer/alsa: use cached values to work around rounding errors
This replaces 967af60327 with a more
effective workaround.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/822
2021-08-06 18:16:37 +02:00
Max Kellermann
351b39e0c5 mixer/alsa: skip the snd_mixer_handle_events() call in alsa_mixer_elem_callback()
snd_mixer_handle_events() has already been called by
DispatchSockets().  This way, we can also skip the exception handler.
2021-08-06 18:03:36 +02:00
Max Kellermann
3b6d4e6673 mixer/alsa: move alsa_mixer_elem_callback() into the AlsaMixer class 2021-08-06 18:01:19 +02:00
Max Kellermann
e8f328d8ad mixer/alsa: move code to GetPercentVolume() 2021-08-06 17:56:30 +02:00
Max Kellermann
5f5b5f63af mixer/alsa: move code to NormalizedToPercent() 2021-08-06 17:55:59 +02:00
Max Kellermann
ad6e303047 mixer/alsa: move code to GetNormalizedVolume() 2021-08-06 17:53:45 +02:00
Max Kellermann
b0e9538855 build/openssl: pass --cross-compile-prefix to ./Configure 2021-08-06 17:30:47 +02:00
Max Kellermann
694debd4cc build/openssl: pass RANLIB=... to "make install"
The "install_dev" target runs ranlib during installation, and this
can break the Android build.
2021-08-06 17:28:28 +02:00
Max Kellermann
0f56ddb805 python/build/libs.py: update OpenSSL to 3.0.0-beta2 2021-08-06 17:22:41 +02:00
Max Kellermann
dde77ec6bd python/build/libs.py: update CURL to 7.78.0 2021-08-06 17:20:52 +02:00
Max Kellermann
5d73eda115 doc/plugins.rst: move filter graph URL to ffmpeg.org 2021-08-06 17:20:52 +02:00
Max Kellermann
1985786ed2 db/simple: prune CUE entries from database for non-existent songs
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1019
2021-08-05 20:26:21 +02:00
Max Kellermann
8e0d39ae94 db/update/Playlist: prepend "../" only for relative URIs
Prepending "../" to absolute URIs would break them.
2021-08-05 20:19:33 +02:00
Max Kellermann
1761fb14af fs/Traits: add PathTraitsUTF8::IsAbsoluteOrHasScheme() 2021-08-05 20:09:06 +02:00
Max Kellermann
ef2fc4e6f6 db/simple/Directory: remove obsolete API doc 2021-08-05 19:05:03 +02:00
Max Kellermann
b979245d6c decoder/Bridge: call UpdateStreamTag() only if there is no pending seek
If UpdateStreamTag() gets called while an initial seek is pending, the
result will never be submitted to a MusicChunk.  By avoiding the
UpdateStreamTag() call in that case (by moving UpdateStreamTag() to
after the PrepareInitialSeek() check), the song_tag is preserved until
UpdateStreamTag() is called again from SubmitData().

This fixes missing tags in the "httpd" output.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1137
2021-08-05 18:02:45 +02:00
Max Kellermann
17b0ac75ca output/oss: always enable PcmExport for alsa_channel_order
We need this even when AFMT_S24_PACKED is not available, for the
correct channel order in multi-channel files.  Internally, MPD uses
FLAC channel order, but OSS uses the same channel order as ALSA.
2021-08-05 15:11:54 +02:00
Max Kellermann
bde64a13e2 tag/Builder: do not acquire tag_pool_lock if TagItem list is empty 2021-08-05 14:32:58 +02:00
Max Kellermann
96875921b7 tag/Builder: use std::swap() in move operator
This way, we save the overhead for acquiring the tag_pool_lock.
2021-08-05 14:28:37 +02:00
Cebtenzzre
551c941b5a tag/Builder: don't ignore the result of tag_pool_dup_item
Also, use RemoveAll() instead of directly clearing TagBuilder::items in
most cases, as its elements represent references that must be released.

Closes #1023
2021-08-05 14:25:55 +02:00
Cebtenzzre
624c77ab43 tag/Builder: another missing RemoveAll() call 2021-08-05 14:25:05 +02:00
Cebtenzzre
ba13b4b5d6 tag/Builder: use RemoveAll() to give up references 2021-08-05 14:23:48 +02:00
Cebtenzzre
4b2d9e544c tag/Pool: add [[nodiscard]] 2021-08-05 14:20:59 +02:00
Max Kellermann
97c43954e8 input/tidal: remove defunct unmaintained plugin
This plugin has been defunct for several years.  Tidal has not ever
replied to any of my emails, so they're apparently not interested in
MPD support.
2021-08-05 13:52:05 +02:00
Max Kellermann
9fa3984a2f input/icy: adjust offset at end of stream in Read()
ProxyInputStream::Read() assigns the `offset` field, which is the
wrong offset because it does not consider Icy metadata removed from
the stream.  Therefore, after every ProxyInputStream::Read() call,
IcyInputStream::Read() needs to override this offset.  This was
missing at the end of the stream, when Read()==0.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1216
2021-08-02 16:40:04 +02:00
Max Kellermann
5355335f19 db/simple/ExportedSong: check src.OwnsTag(), not this->OwnsTag()
this->OwnsTag() accesses fields that are not yet initialized.
2021-07-30 13:10:09 +02:00
Max Kellermann
64fa76c568 command/file: support "albumart" for virtual tracks in CUE sheets
Instead of checking for "cover.jpg" in the virtual directory
representing the CUE sheet, check its enclosing directory.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1206
2021-07-16 09:04:35 +02:00
Max Kellermann
19a44076cf command/file: pass directory uri to read_stream_art() 2021-07-16 08:31:58 +02:00
Max Kellermann
809a18913a fs/Traits: add overload GetParent(string_view) 2021-07-16 08:30:34 +02:00
Max Kellermann
5eab2d96f4 output/winmm: fix struct/class mismatch 2021-07-16 08:30:34 +02:00
Max Kellermann
716784f632 increment version number to 0.22.10 2021-07-16 07:23:00 +02:00
Naglis Jonaitis
eb630ca655 doc/user.rst: rectify admin permission
Updating the database no longer requires the `admin` permission, only
`control` is needed (changed in 2abad0f479).

See also: #1124
2021-06-24 16:44:38 +02:00
45 changed files with 365 additions and 1744 deletions

18
NEWS
View File

@@ -1,3 +1,21 @@
ver 0.22.10 (2021/08/06)
* protocol
- support "albumart" for virtual tracks in CUE sheets
* database
- simple: fix crash bug
- simple: fix absolute paths in CUE "as_directory" entries
- simple: prune CUE entries from database for non-existent songs
* 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
- httpd: fix missing tag after seeking into a new song
- oss: fix channel order of multi-channel files
* mixer
- alsa: fix yet more rounding errors
ver 0.22.9 (2021/06/23)
* database
- simple: load all .mpdignore files of all parent directories

View File

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
android:versionCode="57"
android:versionName="0.22.9">
android:versionCode="58"
android:versionName="0.22.10">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>

View File

@@ -91,8 +91,9 @@ class AndroidNdkToolchain:
self.arch = arch
self.install_prefix = install_prefix
self.toolchain_arch = abi_info['toolchain_arch']
toolchain_path = os.path.join(ndk_path, 'toolchains', abi_info['toolchain_arch'] + '-' + gcc_version, 'prebuilt', build_arch)
toolchain_path = os.path.join(ndk_path, 'toolchains', self.toolchain_arch + '-' + gcc_version, 'prebuilt', build_arch)
llvm_path = os.path.join(ndk_path, 'toolchains', 'llvm', 'prebuilt', build_arch)
llvm_triple = abi_info['llvm_triple'] + android_api_level

View File

@@ -38,7 +38,7 @@ author = 'Max Kellermann'
# built documents.
#
# The short X.Y version.
version = '0.22.9'
version = '0.22.10'
# The full version, including alpha/beta/rc tags.
release = version

View File

@@ -295,37 +295,6 @@ in the form ``qobuz://track/ID``, e.g.:
* - **format_id N**
- The `Qobuz format identifier <https://github.com/Qobuz/api-documentation/blob/master/endpoints/track/getFileUrl.md#parameters>`_, 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
<http://tidal.com/>`_. 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
@@ -1200,7 +1169,7 @@ This plugin requires building with ``libavfilter`` (FFmpeg).
* - **graph "..."**
- Specifies the ``libavfilter`` graph; read the `FFmpeg
documentation
<https://libav.org/documentation/libavfilter.html#Filtergraph-syntax-1>`_
<https://ffmpeg.org/ffmpeg-filters.html#Filtergraph-syntax-1>`_
for details

View File

@@ -628,7 +628,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.

View File

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.22.9',
version: '0.22.10',
meson_version: '>= 0.49.0',
default_options: [
'c_std=c11',

View File

@@ -104,7 +104,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

View File

@@ -379,14 +379,14 @@ ffmpeg = FfmpegProject(
)
openssl = OpenSSLProject(
'https://www.openssl.org/source/openssl-3.0.0-alpha16.tar.gz',
'08ce8244b59d75f40f91170dfcb012bf25309cdcb1fef9502e39d694f883d1d1',
'https://www.openssl.org/source/openssl-3.0.0-beta2.tar.gz',
'e76ab22879201b12f014393ee4becec7f264d8f6955b1036839128002868df71',
'include/openssl/ossl_typ.h',
)
curl = AutotoolsProject(
'https://curl.se/download/curl-7.76.1.tar.xz',
'64bb5288c39f0840c07d077e30d9052e1cbb9fa6c2dc52523824cc859e679145',
'https://curl.se/download/curl-7.78.0.tar.xz',
'be42766d5664a739c3974ee3dfbbcbe978a4ccb1fe628bb1d9b59ac79e445fb5',
'lib/libcurl.a',
[
'--disable-shared', '--enable-static',

View File

@@ -17,6 +17,12 @@ class OpenSSLProject(MakeProject):
'build_libs',
]
def get_make_install_args(self, toolchain):
# OpenSSL's Makefile runs "ranlib" during installation
return MakeProject.get_make_install_args(self, toolchain) + [
'RANLIB=' + toolchain.ranlib,
]
def build(self, toolchain):
src = self.unpack(toolchain, out_of_tree=False)
@@ -42,6 +48,7 @@ class OpenSSLProject(MakeProject):
}
openssl_arch = openssl_archs[toolchain.arch]
cross_compile_prefix = toolchain.toolchain_arch + '-'
subprocess.check_call(['./Configure',
'no-shared',
@@ -50,6 +57,7 @@ class OpenSSLProject(MakeProject):
'no-tests',
'no-asm', # "asm" causes build failures on Windows
openssl_arch,
'--cross-compile-prefix=' + cross_compile_prefix,
'--prefix=' + toolchain.install_prefix],
cwd=src, env=toolchain.env)
MakeProject.build(self, toolchain, src)

View File

@@ -20,7 +20,7 @@ class Project:
self.base = base
if name is None or version is None:
m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*(?:-alpha\d+)?)(\+.*)?$', self.base)
m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*(?:-(?:alpha|beta)\d+)?)$', self.base)
if name is None: name = m.group(1)
if version is None: version = m.group(2)

View File

@@ -27,11 +27,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"
@@ -187,10 +191,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<Client &>(r.GetClient());
@@ -243,6 +246,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)
{
@@ -252,7 +290,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
@@ -273,7 +317,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);

View File

@@ -109,6 +109,23 @@ Directory::FindChild(std::string_view name) const noexcept
return nullptr;
}
bool
Directory::TargetExists(std::string_view _target) const noexcept
{
StringView target{_target};
if (target.SkipPrefix("../")) {
if (parent == nullptr)
return false;
return parent->TargetExists(target);
}
/* sorry for the const_cast ... */
const auto lr = const_cast<Directory *>(this)->LookupDirectory(target);
return lr.directory->FindSong(lr.rest) != nullptr;
}
void
Directory::PruneEmpty() noexcept
{

View File

@@ -118,13 +118,17 @@ public:
return new Directory(std::string(), nullptr);
}
bool IsPlaylist() const noexcept {
return device == DEVICE_PLAYLIST;
}
/**
* Is this really a regular file which is being treated like a
* directory?
*/
bool IsReallyAFile() const noexcept {
return device == DEVICE_INARCHIVE ||
device == DEVICE_PLAYLIST ||
IsPlaylist() ||
device == DEVICE_CONTAINER;
}
@@ -206,11 +210,13 @@ public:
* Looks up a directory by its relative URI.
*
* @param uri the relative URI
* @return the Directory, or nullptr if none was found
*/
gcc_pure
LookupResult LookupDirectory(std::string_view uri) noexcept;
[[gnu::pure]]
bool TargetExists(std::string_view target) const noexcept;
gcc_pure
bool IsEmpty() const noexcept {
return children.empty() &&

View File

@@ -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;

View File

@@ -30,6 +30,7 @@
#include "playlist/SongEnumerator.hxx"
#include "storage/FileInfo.hxx"
#include "storage/StorageInterface.hxx"
#include "fs/Traits.hxx"
#include "util/StringFormat.hxx"
#include "Log.hxx"
@@ -70,7 +71,14 @@ UpdateWalk::UpdatePlaylistFile(Directory &parent, std::string_view name,
auto db_song = std::make_unique<Song>(std::move(*song),
*directory);
db_song->target = "../" + db_song->filename;
db_song->target =
PathTraitsUTF8::IsAbsoluteOrHasScheme(db_song->filename.c_str())
? db_song->filename
/* prepend "../" to relative paths to
go from the virtual directory
(DEVICE_PLAYLIST) to the containing
directory */
: "../" + db_song->filename;
db_song->filename = StringFormat<64>("track%04u",
++track);

View File

@@ -133,6 +133,28 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept
}
}
void
UpdateWalk::PurgeDanglingFromPlaylists(Directory &directory) noexcept
{
/* recurse */
for (Directory &child : directory.children)
PurgeDanglingFromPlaylists(child);
if (!directory.IsPlaylist())
/* this check is only for virtual directories
representing a playlist file */
return;
directory.ForEachSongSafe([&](Song &song){
if (!song.target.empty() &&
!PathTraitsUTF8::IsAbsoluteOrHasScheme(song.target.c_str()) &&
!directory.TargetExists(song.target)) {
editor.DeleteSong(directory, &song);
modified = true;
}
});
}
#ifndef _WIN32
static bool
update_directory_stat(Storage &storage, Directory &directory) noexcept
@@ -530,5 +552,10 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard) noexcept
UpdateDirectory(root, exclude_list, info);
}
{
const ScopeDatabaseLock protect;
PurgeDanglingFromPlaylists(root);
}
return modified;
}

View File

@@ -85,6 +85,12 @@ private:
void PurgeDeletedFromDirectory(Directory &directory) noexcept;
/**
* Remove all virtual songs inside playlists whose "target"
* field points to a non-existing song file.
*/
void PurgeDanglingFromPlaylists(Directory &directory) noexcept;
void UpdateSongFile2(Directory &directory,
const char *name, const char *suffix,
const StorageFileInfo &info) noexcept;

View File

@@ -581,10 +581,6 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept
decoder_tag = std::make_unique<Tag>(std::move(tag));
/* check for a new stream tag */
UpdateStreamTag(is);
/* check if we're seeking */
if (PrepareInitialSeek())
@@ -593,6 +589,10 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept
function here */
return DecoderCommand::SEEK;
/* check for a new stream tag */
UpdateStreamTag(is);
/* send tag to music pipe */
if (stream_tag != nullptr)

View File

@@ -19,6 +19,7 @@
#include "Traits.hxx"
#include "util/StringCompare.hxx"
#include "util/UriExtract.hxx"
#include <string.h>
@@ -84,6 +85,22 @@ GetParentPathImpl(typename Traits::const_pointer p) noexcept
return {p, size_t(sep - p)};
}
template<typename Traits>
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>
typename Traits::const_pointer
RelativePathImpl(typename Traits::string_view base,
@@ -166,6 +183,12 @@ PathTraitsFS::GetParent(PathTraitsFS::const_pointer p) noexcept
return GetParentPathImpl<PathTraitsFS>(p);
}
PathTraitsFS::string_view
PathTraitsFS::GetParent(string_view p) noexcept
{
return GetParentPathImpl<PathTraitsFS>(p);
}
PathTraitsFS::const_pointer
PathTraitsFS::Relative(string_view base, const_pointer other) noexcept
{
@@ -198,6 +221,12 @@ PathTraitsUTF8::Build(string_view a, string_view b) noexcept
return BuildPathImpl<PathTraitsUTF8>(a, b);
}
bool
PathTraitsUTF8::IsAbsoluteOrHasScheme(const_pointer p) noexcept
{
return IsAbsolute(p) || uri_has_scheme(p);
}
PathTraitsUTF8::const_pointer
PathTraitsUTF8::GetBase(const_pointer p) noexcept
{
@@ -210,6 +239,12 @@ PathTraitsUTF8::GetParent(const_pointer p) noexcept
return GetParentPathImpl<PathTraitsUTF8>(p);
}
PathTraitsUTF8::string_view
PathTraitsUTF8::GetParent(string_view p) noexcept
{
return GetParentPathImpl<PathTraitsUTF8>(p);
}
PathTraitsUTF8::const_pointer
PathTraitsUTF8::Relative(string_view base, const_pointer other) noexcept
{

View File

@@ -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
@@ -246,6 +274,13 @@ struct PathTraitsUTF8 {
return IsSeparator(*p);
}
/**
* Is this any kind of absolute URI? (Unlike IsAbsolute(),
* this includes URIs/URLs with a scheme)
*/
[[gnu::pure]] [[gnu::nonnull]]
static bool IsAbsoluteOrHasScheme(const_pointer p) noexcept;
gcc_pure gcc_nonnull_all
static bool IsSpecialFilename(const_pointer name) noexcept {
return (name[0] == '.' && name[1] == 0) ||
@@ -277,6 +312,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

View File

@@ -104,8 +104,11 @@ IcyInputStream::Read(std::unique_lock<Mutex> &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) {

View File

@@ -57,7 +57,6 @@ static bool
ExpensiveSeeking(const char *uri) noexcept
{
return StringStartsWithCaseASCII(uri, "http://") ||
StringStartsWithCaseASCII(uri, "tidal://") ||
StringStartsWithCaseASCII(uri, "qobuz://") ||
StringStartsWithCaseASCII(uri, "https://");
}

View File

@@ -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 @@ const InputPlugin *const input_plugins[] = {
#ifdef ENABLE_ALSA
&input_plugin_alsa,
#endif
#ifdef ENABLE_TIDAL
&tidal_input_plugin,
#endif
#ifdef ENABLE_QOBUZ
&qobuz_input_plugin,
#endif

View File

@@ -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 <stdexcept>
/**
* 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<typename W>
TidalError(unsigned _status, unsigned _sub_status, W &&_what) noexcept
:std::runtime_error(std::forward<W>(_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

View File

@@ -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<TidalErrorParser>;
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<std::string, std::string> &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;
}

View File

@@ -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 <string>
#include <map>
template<typename T> 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<std::string, std::string> &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

View File

@@ -1,256 +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 "config/Block.hxx"
#include "thread/Mutex.hxx"
#include "util/Domain.hxx"
#include "util/Exception.hxx"
#include "util/StringCompare.hxx"
#include "Log.hxx"
#include <memory>
#include <utility>
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<TidalTrackRequest> 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);
}
/* 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<FailingInputStream>(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<Mutex> protect(mutex);
try {
TidalTrackHandler &h = *this;
track_request = std::make_unique<TidalTrackRequest>(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
{
FormatDebug(tidal_domain, "Tidal track '%s' resolves to %s",
track_id.c_str(), url.c_str());
const std::lock_guard<Mutex> 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<Mutex> protect(mutex);
if (retry_login && IsInvalidSession(e)) {
/* the session has expired - obtain a new session id
by logging in again */
FormatInfo(tidal_domain, "Session expired ('%s'), retrying to log in",
GetFullMessage(e).c_str());
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");
FormatWarning(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<TidalInputStream>(uri, track_id, mutex);
}
static std::unique_ptr<RemoteTagScanner>
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<TidalTagScanner>(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,
};

View File

@@ -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

View File

@@ -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 <cassert>
using Wrapper = Yajl::CallbacksWrapper<TidalLoginRequest::ResponseParser>;
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<CurlResponseParser>
TidalLoginRequest::MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers)
{
if (status != 200)
return std::make_unique<TidalErrorParser>(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<ResponseParser>();
}
void
TidalLoginRequest::FinishParser(std::unique_ptr<CurlResponseParser> p)
{
assert(dynamic_cast<ResponseParser *>(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;
}

View File

@@ -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<CurlResponseParser> MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
/* virtual methods from CurlResponseHandler */
void OnError(std::exception_ptr e) noexcept override;
};
#endif

View File

@@ -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<Mutex> 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<TidalLoginRequest>(*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
{
FormatDebug(tidal_domain, "Login successful, session=%s", _session.c_str());
{
const std::lock_guard<Mutex> protect(mutex);
login_request.reset();
session = std::move(_session);
}
ScheduleInvokeHandlers();
}
void
TidalSessionManager::OnTidalLoginError(std::exception_ptr e) noexcept
{
{
const std::lock_guard<Mutex> protect(mutex);
login_request.reset();
error = e;
}
ScheduleInvokeHandlers();
}
void
TidalSessionManager::InvokeHandlers() noexcept
{
const std::lock_guard<Mutex> protect(mutex);
while (!handlers.empty()) {
auto &h = handlers.front();
handlers.pop_front();
const ScopeUnlock unlock(mutex);
h.OnTidalSession();
}
}

View File

@@ -1,161 +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 <boost/intrusive/list.hpp>
#include <memory>
#include <string>
/**
* Callback class for #TidalSessionManager.
*
* Its methods must be thread-safe.
*/
class TidalSessionHandler
: public boost::intrusive::list_base_hook<boost::intrusive::link_mode<boost::intrusive::safe_link>>
{
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;
typedef boost::intrusive::list<TidalSessionHandler,
boost::intrusive::constant_time_size<false>> LoginHandlerList;
LoginHandlerList handlers;
std::unique_ptr<TidalLoginRequest> 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<Mutex> protect(mutex);
if (h.is_linked())
handlers.erase(handlers.iterator_to(h));
}
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<Mutex> 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

View File

@@ -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 <cassert>
using Wrapper = Yajl::CallbacksWrapper<TidalTagScanner::ResponseParser>;
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<CurlResponseParser>
TidalTagScanner::MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers)
{
if (status != 200)
return std::make_unique<TidalErrorParser>(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<ResponseParser>();
}
void
TidalTagScanner::FinishParser(std::unique_ptr<CurlResponseParser> p)
{
assert(dynamic_cast<ResponseParser *>(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;
}

View File

@@ -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<CurlResponseParser> MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
/* virtual methods from CurlResponseHandler */
void OnError(std::exception_ptr e) noexcept override;
};
#endif

View File

@@ -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 <cassert>
using Wrapper = Yajl::CallbacksWrapper<TidalTrackRequest::ResponseParser>;
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<CurlResponseParser>
TidalTrackRequest::MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers)
{
if (status != 200)
return std::make_unique<TidalErrorParser>(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<ResponseParser>();
}
void
TidalTrackRequest::FinishParser(std::unique_ptr<CurlResponseParser> p)
{
assert(dynamic_cast<ResponseParser *>(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;
}

View File

@@ -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<CurlResponseParser> MakeParser(unsigned status,
std::multimap<std::string, std::string> &&headers) override;
void FinishParser(std::unique_ptr<CurlResponseParser> p) override;
/* virtual methods from CurlResponseHandler */
void OnError(std::exception_ptr e) noexcept override;
};
#endif

View File

@@ -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,

View File

@@ -80,6 +80,24 @@ class AlsaMixer final : public Mixer {
AlsaMixerMonitor *monitor;
/**
* These fields are our workaround for rounding errors when
* the resolution of a mixer knob isn't fine enough to
* represent all 101 possible values (0..100).
*
* "desired_volume" is the percent value passed to
* SetVolume(), and "resulting_volume" is the volume which was
* actually set, and would be returned by the next
* GetPercentVolume() call.
*
* When GetVolume() is called, we compare the
* "resulting_volume" with the value returned by
* GetPercentVolume(), and if it's the same, we're still on
* the same value that was previously set (but may have been
* rounded down or up).
*/
int desired_volume, resulting_volume;
public:
AlsaMixer(EventLoop &_event_loop, MixerListener &_listener)
:Mixer(alsa_mixer_plugin, _listener),
@@ -95,6 +113,27 @@ public:
void Close() noexcept override;
int GetVolume() override;
void SetVolume(unsigned volume) override;
private:
[[gnu::const]]
static unsigned NormalizedToPercent(double normalized) noexcept {
return lround(100 * normalized);
}
[[gnu::pure]]
double GetNormalizedVolume() const noexcept {
return get_normalized_playback_volume(elem,
SND_MIXER_SCHN_FRONT_LEFT);
}
[[gnu::pure]]
unsigned GetPercentVolume() const noexcept {
return NormalizedToPercent(GetNormalizedVolume());
}
static int ElemCallback(snd_mixer_elem_t *elem,
unsigned mask) noexcept;
};
static constexpr Domain alsa_mixer_domain("alsa_mixer");
@@ -138,18 +177,26 @@ AlsaMixerMonitor::DispatchSockets() noexcept
*
*/
static int
alsa_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned mask)
int
AlsaMixer::ElemCallback(snd_mixer_elem_t *elem, unsigned mask) noexcept
{
AlsaMixer &mixer = *(AlsaMixer *)
snd_mixer_elem_get_callback_private(elem);
if (mask & SND_CTL_EVENT_MASK_VALUE) {
try {
int volume = mixer.GetVolume();
mixer.listener.OnMixerVolumeChanged(mixer, volume);
} catch (...) {
}
int volume = mixer.GetPercentVolume();
if (mixer.resulting_volume >= 0 &&
volume == mixer.resulting_volume)
/* still the same volume (this might be a
callback caused by SetVolume()) - switch to
desired_volume */
volume = mixer.desired_volume;
else
/* flush */
mixer.desired_volume = mixer.resulting_volume = -1;
mixer.listener.OnMixerVolumeChanged(mixer, volume);
}
return 0;
@@ -227,7 +274,7 @@ AlsaMixer::Setup()
throw FormatRuntimeError("no such mixer control: %s", control);
snd_mixer_elem_set_callback_private(elem, this);
snd_mixer_elem_set_callback(elem, alsa_mixer_elem_callback);
snd_mixer_elem_set_callback(elem, ElemCallback);
monitor = new AlsaMixerMonitor(event_loop, handle);
}
@@ -235,6 +282,8 @@ AlsaMixer::Setup()
void
AlsaMixer::Open()
{
desired_volume = resulting_volume = -1;
int err;
err = snd_mixer_open(&handle, 0);
@@ -273,7 +322,12 @@ AlsaMixer::GetVolume()
throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
snd_strerror(err));
return lround(100 * get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT));
int volume = GetPercentVolume();
if (resulting_volume >= 0 && volume == resulting_volume)
/* we're still on the value passed to SetVolume() */
volume = desired_volume;
return volume;
}
void
@@ -281,12 +335,13 @@ AlsaMixer::SetVolume(unsigned volume)
{
assert(handle != nullptr);
double cur = get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT);
int delta = volume - lround(100.*cur);
int err = set_normalized_playback_volume(elem, cur + 0.01*delta, delta);
int err = set_normalized_playback_volume(elem, 0.01*volume, 1);
if (err < 0)
throw FormatRuntimeError("failed to set ALSA volume: %s",
snd_strerror(err));
desired_volume = volume;
resulting_volume = GetPercentVolume();
}
const MixerPlugin alsa_mixer_plugin = {

View File

@@ -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 <cassert>
@@ -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<PcmExport> 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);

View File

@@ -29,7 +29,7 @@
#include <windef.h>
#include <mmsystem.h>
struct WinmmOutput;
class WinmmOutput;
extern const struct AudioOutputPlugin winmm_output_plugin;

View File

@@ -92,8 +92,8 @@ playlist_check_translate_song(DetachedSong &song, std::string_view base_uri,
}
#endif
if (base_uri.data() != nullptr && !uri_has_scheme(uri) &&
!PathTraitsUTF8::IsAbsolute(uri))
if (base_uri.data() != nullptr &&
!PathTraitsUTF8::IsAbsoluteOrHasScheme(uri))
song.SetURI(PathTraitsUTF8::Build(base_uri, uri));
return playlist_check_load_song(song, loader);

View File

@@ -60,7 +60,7 @@ DetachedSong::IsInDatabase() const noexcept
GetRealURI() is never relative */
const char *_uri = GetURI();
return !uri_has_scheme(_uri) && !PathTraitsUTF8::IsAbsolute(_uri);
return !PathTraitsUTF8::IsAbsoluteOrHasScheme(_uri);
}
SignedSongTime

View File

@@ -36,10 +36,12 @@ TagBuilder::TagBuilder(const Tag &other) noexcept
{
items.reserve(other.num_items);
const std::lock_guard<Mutex> 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<Mutex> 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<Mutex> 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<Mutex> 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<Mutex> 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<Mutex> 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<Mutex> protect(tag_pool_lock);
for (auto i : items)

View File

@@ -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;