release v0.22.5

-----BEGIN PGP SIGNATURE-----
 
 iQJEBAABCgAuFiEEA5IzWngIOJSkMBxDI26KWMbbRRIFAmAq1woQHG1heEBtdXNp
 Y3BkLm9yZwAKCRAjbopYxttFElB1EACItrIKgEywkzW3l+gmgSjtwwQOiLfg+0Zg
 Z3YgpegDvhmjBVXAHFDlhnXf//zCr286ZmCCVItrz2eGHYX2lvul0SdYxp/+Kebk
 WrCez6LMecaoGjbgiwQ70u/stNkX85ZT62CznNyYvwMx4bRhXXgOuBdKYTAZTvT8
 ABvfL+Ari7TBi88qCAaufmxyv7VFOaZg8+GpV1unIlHE6vu3febzDffPdjfODmOe
 BpLILJJIzUd9p1tGmSCvNCUUHdElktbK1aSVS/0x2xdKG3eDKmPIhSdxdqOdunr7
 9us4Mg7ZB5REaRC0ZfxR6P+vId0uIT3kpyDqs5i8Zao1WwmCdZhvaMMxJ3KF0MVs
 q4Lb99LMF2xAvsoA4x+wY0E0SlFrBhySrFY/i4gaBd9ctzQsbxID3cOZhSbEmQnk
 VNlPK/cYtWVHouLzSOUZeg3/nyMMWXTXy87esB/JdKWqushYLFqy/WIIJvKh4dRL
 YTEJtGeAe7wn9BPoD5Sf1xaj9ULw5CG/Z72inMk1rdzQBn+sWypb8HwJiGtHH43Q
 3YwTSAg/Z3MuxcMM1F9ce/IeE+sqCtOZKgTTpdp56hPlHMV9Fa0v7mnMHz508jB/
 4ZwAm3eEbCy14IKtW1jfKwA/IgPnF6bR6D1nn7F9SKnBG+hdHsyAVyHaTsXbfO4u
 0RZ5Y9vxdQ==
 =I2BI
 -----END PGP SIGNATURE-----

Merge tag 'v0.22.5'

release v0.22.5
This commit is contained in:
Max Kellermann 2021-02-15 22:50:16 +01:00
commit ecc07e4e98
17 changed files with 152 additions and 30 deletions

12
NEWS
View File

@ -2,13 +2,23 @@ ver 0.23 (not yet released)
* protocol * protocol
- new command "getvol" - new command "getvol"
ver 0.22.5 (not yet released) ver 0.22.5 (2021/02/15)
* protocol
- error for malformed ranges instead of ignoring silently
- better error message for open-ended range with "move"
* database
- simple: fix missing CUE sheet metadata in "addid" command
* tags * tags
- id: translate TPE3 to Conductor, not Performer - id: translate TPE3 to Conductor, not Performer
* archive * archive
- iso9660: another fix for unaligned reads - iso9660: another fix for unaligned reads
* output * output
- httpd: error handling on Windows improved - httpd: error handling on Windows improved
- pulse: fix deadlock with "always_on"
* Windows:
- enable https:// support (via Schannel)
* Android
- work around "Permission denied" on mpd.conf
ver 0.22.4 (2021/01/21) ver 0.22.4 (2021/01/21)
* protocol * protocol

View File

@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application android:allowBackup="true" <application android:allowBackup="true"
android:requestLegacyExternalStorage="true"
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:banner="@drawable/icon" android:banner="@drawable/icon"
android:label="@string/app_name"> android:label="@string/app_name">

View File

@ -5,8 +5,8 @@ android_ndk = get_option('android_ndk')
android_sdk = get_option('android_sdk') android_sdk = get_option('android_sdk')
android_abi = get_option('android_abi') android_abi = get_option('android_abi')
android_sdk_build_tools_version = '27.0.0' android_sdk_build_tools_version = '29.0.3'
android_sdk_platform = 'android-23' android_sdk_platform = 'android-29'
android_build_tools_dir = join_paths(android_sdk, 'build-tools', android_sdk_build_tools_version) android_build_tools_dir = join_paths(android_sdk, 'build-tools', android_sdk_build_tools_version)
android_sdk_platform_dir = join_paths(android_sdk, 'platforms', android_sdk_platform) android_sdk_platform_dir = join_paths(android_sdk, 'platforms', android_sdk_platform)

View File

@ -68,11 +68,11 @@ There are two active branches in the git repository:
- the "unstable" branch called ``master`` where new features are - the "unstable" branch called ``master`` where new features are
merged. This will become the next major release eventually. merged. This will become the next major release eventually.
- the "stable" branch (currently called ``v0.21.x``) where only bug - the "stable" branch (currently called ``v0.22.x``) where only bug
fixes are merged. fixes are merged.
Once :program:`MPD` 0.22 is released, a new branch called ``v0.22.x`` Once :program:`MPD` 0.23 is released, a new branch called ``v0.23.x``
will be created for 0.22 bug-fix releases; after that, ``v0.21.x`` will be created for 0.23 bug-fix releases; after that, ``v0.22.x``
will eventually cease to be maintained. will eventually cease to be maintained.
After bug fixes have been added to the "stable" branch, it will be After bug fixes have been added to the "stable" branch, it will be

View File

@ -689,6 +689,11 @@ Whenever possible, ids should be used.
(directories add recursively). ``URI`` (directories add recursively). ``URI``
can also be a single file. can also be a single file.
Clients that are connected via local socket may add arbitrary
local files (URI is an absolute path). Exmaple::
add "/home/foo/Music/bar.ogg"
.. _command_addid: .. _command_addid:
:command:`addid {URI} [POSITION]` :command:`addid {URI} [POSITION]`

View File

@ -407,6 +407,9 @@ curl = AutotoolsProject(
'--disable-progress-meter', '--disable-progress-meter',
'--disable-alt-svc', '--disable-alt-svc',
'--without-gnutls', '--without-nss', '--without-libssh2', '--without-gnutls', '--without-nss', '--without-libssh2',
# native Windows SSL/TLS support, option ignored on non-Windows builds
'--with-schannel',
], ],
patches='src/lib/curl/patches', patches='src/lib/curl/patches',

View File

@ -326,6 +326,11 @@ CommandResult
handle_move(Client &client, Request args, [[maybe_unused]] Response &r) handle_move(Client &client, Request args, [[maybe_unused]] Response &r)
{ {
RangeArg range = args.ParseRange(0); RangeArg range = args.ParseRange(0);
if (range.IsOpenEnded()) {
r.Error(ACK_ERROR_ARG, "Open-ended range not supported");
return CommandResult::ERROR;
}
int to = args.ParseInt(1); int to = args.ParseInt(1);
client.GetPartition().MoveRange(range.start, range.end, to); client.GetPartition().MoveRange(range.start, range.end, to);
return CommandResult::OK; return CommandResult::OK;

View File

@ -37,6 +37,15 @@ public:
ExportedSong(const char *_uri, Tag &&_tag) noexcept ExportedSong(const char *_uri, Tag &&_tag) noexcept
:LightSong(_uri, tag_buffer), :LightSong(_uri, tag_buffer),
tag_buffer(std::move(_tag)) {} tag_buffer(std::move(_tag)) {}
/* this custom move constructor is necessary so LightSong::tag
points to this instance's #Tag field instead of leaving a
dangling reference to the source object's #Tag field */
ExportedSong(ExportedSong &&src) noexcept
:LightSong(src, tag_buffer),
tag_buffer(std::move(src.tag_buffer)) {}
ExportedSong &operator=(ExportedSong &&) = delete;
}; };
#endif #endif

View File

@ -181,6 +181,14 @@ class AudioOutputControl {
*/ */
bool open = false; bool open = false;
/**
* Is the device currently playing, i.e. is its buffer
* (likely) non-empty? If not, then it will never be drained.
*
* This field is only valid while the output is open.
*/
bool playing;
/** /**
* Is the device paused? i.e. the output thread is in the * Is the device paused? i.e. the output thread is in the
* ao_pause() loop. * ao_pause() loop.

View File

@ -53,7 +53,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
if (open && cf != output->filter_audio_format) if (open && cf != output->filter_audio_format)
/* if the filter's output format changes, the output /* if the filter's output format changes, the output
must be reopened as well */ must be reopened as well */
InternalCloseOutput(true); InternalCloseOutput(playing);
output->filter_audio_format = cf; output->filter_audio_format = cf;
@ -64,6 +64,7 @@ AudioOutputControl::InternalOpen2(const AudioFormat in_audio_format)
} }
open = true; open = true;
playing = false;
} else if (in_audio_format != output->out_audio_format) { } else if (in_audio_format != output->out_audio_format) {
/* reconfigure the final ConvertFilter for its new /* reconfigure the final ConvertFilter for its new
input AudioFormat */ input AudioFormat */
@ -285,6 +286,9 @@ AudioOutputControl::PlayChunk(std::unique_lock<Mutex> &lock) noexcept
assert(nbytes % output->out_audio_format.GetFrameSize() == 0); assert(nbytes % output->out_audio_format.GetFrameSize() == 0);
source.ConsumeData(nbytes); source.ConsumeData(nbytes);
/* there's data to be drained from now on */
playing = true;
} }
return true; return true;
@ -371,6 +375,9 @@ AudioOutputControl::InternalPause(std::unique_lock<Mutex> &lock) noexcept
} }
skip_delay = true; skip_delay = true;
/* ignore drain commands until we got something new to play */
playing = false;
} }
static void static void
@ -390,6 +397,10 @@ PlayFull(FilteredAudioOutput &output, ConstBuffer<void> _buffer)
inline void inline void
AudioOutputControl::InternalDrain() noexcept AudioOutputControl::InternalDrain() noexcept
{ {
/* after this method finishes, there's nothing left to be
drained */
playing = false;
try { try {
/* flush the filter and play its remaining output */ /* flush the filter and play its remaining output */
@ -518,6 +529,7 @@ AudioOutputControl::Task() noexcept
source.Cancel(); source.Cancel();
if (open) { if (open) {
playing = false;
const ScopeUnlock unlock(mutex); const ScopeUnlock unlock(mutex);
output->Cancel(); output->Cancel();
} }

View File

@ -56,8 +56,6 @@ class PulseOutput final : AudioOutput {
size_t writable; size_t writable;
bool pause;
/** /**
* Was Interrupt() called? This will unblock Play(). It will * Was Interrupt() called? This will unblock Play(). It will
* be reset by Cancel() and Pause(), as documented by the * be reset by Cancel() and Pause(), as documented by the
@ -113,6 +111,7 @@ public:
[[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override; [[nodiscard]] std::chrono::steady_clock::duration Delay() const noexcept override;
size_t Play(const void *chunk, size_t size) override; size_t Play(const void *chunk, size_t size) override;
void Drain() override;
void Cancel() noexcept override; void Cancel() noexcept override;
bool Pause() override; bool Pause() override;
@ -688,7 +687,6 @@ PulseOutput::Open(AudioFormat &audio_format)
"pa_stream_connect_playback() has failed"); "pa_stream_connect_playback() has failed");
} }
pause = false;
interrupted = false; interrupted = false;
} }
@ -699,17 +697,6 @@ PulseOutput::Close() noexcept
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
if (pa_stream_get_state(stream) == PA_STREAM_READY) {
pa_operation *o =
pa_stream_drain(stream,
pulse_output_stream_success_cb, this);
if (o == nullptr) {
LogPulseError(context,
"pa_stream_drain() has failed");
} else
pulse_wait_for_operation(mainloop, o);
}
DeleteStream(); DeleteStream();
if (context != nullptr && if (context != nullptr &&
@ -780,7 +767,7 @@ PulseOutput::Delay() const noexcept
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
auto result = std::chrono::steady_clock::duration::zero(); auto result = std::chrono::steady_clock::duration::zero();
if (pause && pa_stream_is_corked(stream) && if (pa_stream_is_corked(stream) &&
pa_stream_get_state(stream) == PA_STREAM_READY) pa_stream_get_state(stream) == PA_STREAM_READY)
/* idle while paused */ /* idle while paused */
result = std::chrono::seconds(1); result = std::chrono::seconds(1);
@ -796,8 +783,6 @@ PulseOutput::Play(const void *chunk, size_t size)
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
pause = false;
/* check if the stream is (already) connected */ /* check if the stream is (already) connected */
WaitStream(); WaitStream();
@ -840,6 +825,25 @@ PulseOutput::Play(const void *chunk, size_t size)
return size; return size;
} }
void
PulseOutput::Drain()
{
Pulse::LockGuard lock(mainloop);
if (pa_stream_get_state(stream) != PA_STREAM_READY ||
pa_stream_is_suspended(stream) ||
pa_stream_is_corked(stream))
return;
pa_operation *o =
pa_stream_drain(stream,
pulse_output_stream_success_cb, this);
if (o == nullptr)
throw MakePulseError(context, "pa_stream_drain() failed");
pulse_wait_for_operation(mainloop, o);
}
void void
PulseOutput::Cancel() noexcept PulseOutput::Cancel() noexcept
{ {
@ -876,7 +880,6 @@ PulseOutput::Pause()
Pulse::LockGuard lock(mainloop); Pulse::LockGuard lock(mainloop);
pause = true;
interrupted = false; interrupted = false;
/* check if the stream is (already/still) connected */ /* check if the stream is (already/still) connected */

View File

@ -24,6 +24,7 @@
#include "mixer/MixerList.hxx" #include "mixer/MixerList.hxx"
#include "thread/Cond.hxx" #include "thread/Cond.hxx"
#include "thread/Mutex.hxx" #include "thread/Mutex.hxx"
#include "thread/Name.hxx"
#include "thread/Thread.hxx" #include "thread/Thread.hxx"
#include "util/AllocatedString.hxx" #include "util/AllocatedString.hxx"
#include "util/Domain.hxx" #include "util/Domain.hxx"
@ -231,6 +232,7 @@ IAudioClient *wasapi_output_get_client(WasapiOutput &output) noexcept {
} }
void WasapiOutputThread::Work() noexcept { void WasapiOutputThread::Work() noexcept {
SetThreadName("Wasapi Output Worker");
FormatDebug(wasapi_output_domain, "Working thread started"); FormatDebug(wasapi_output_domain, "Working thread started");
try { try {
com.emplace(); com.emplace();

View File

@ -94,7 +94,7 @@ ParseCommandArgRange(const char *s)
s); s);
if (test == test2) if (test == test2)
value = std::numeric_limits<int>::max(); return RangeArg::OpenEnded(range.start);
if (value < 0) if (value < 0)
throw FormatProtocolError(ACK_ERROR_ARG, throw FormatProtocolError(ACK_ERROR_ARG,
@ -107,9 +107,13 @@ ParseCommandArgRange(const char *s)
range.end = (unsigned)value; range.end = (unsigned)value;
} else { } else {
range.end = (unsigned)value + 1; return RangeArg::Single(range.start);
} }
if (!range.IsWellFormed())
throw FormatProtocolError(ACK_ERROR_ARG,
"Malformed range: %s", s);
return range; return range;
} }

View File

@ -25,8 +25,22 @@
struct RangeArg { struct RangeArg {
unsigned start, end; unsigned start, end;
static constexpr RangeArg All() { /**
return { 0, std::numeric_limits<unsigned>::max() }; * Construct an open-ended range starting at the given index.
*/
static constexpr RangeArg OpenEnded(unsigned start) noexcept {
return { start, std::numeric_limits<unsigned>::max() };
}
static constexpr RangeArg All() noexcept {
return OpenEnded(0);
}
/**
* Construct an instance describing exactly one index.
*/
static constexpr RangeArg Single(unsigned i) noexcept {
return { i, i + 1 };
} }
constexpr bool operator==(RangeArg other) const noexcept { constexpr bool operator==(RangeArg other) const noexcept {
@ -37,13 +51,45 @@ struct RangeArg {
return !(*this == other); return !(*this == other);
} }
constexpr bool IsOpenEnded() const noexcept {
return end == All().end;
}
constexpr bool IsAll() const noexcept { constexpr bool IsAll() const noexcept {
return *this == All(); return *this == All();
} }
constexpr bool IsWellFormed() const noexcept {
return start <= end;
}
/**
* Is this range empty? A malformed range also counts as
* "empty" for this method.
*/
constexpr bool IsEmpty() const noexcept {
return start >= end;
}
/**
* Check if the range contains at least this number of items.
* Unlike Count(), this allows the object to be malformed.
*/
constexpr bool HasAtLeast(unsigned n) const noexcept {
return start + n <= end;
}
constexpr bool Contains(unsigned i) const noexcept { constexpr bool Contains(unsigned i) const noexcept {
return i >= start && i < end; return i >= start && i < end;
} }
/**
* Count the number of items covered by this range. This requires the
* object to be well-formed.
*/
constexpr unsigned Count() const noexcept {
return end - start;
}
}; };
#endif #endif

View File

@ -88,6 +88,19 @@ struct LightSong {
LightSong(const char *_uri, const Tag &_tag) noexcept LightSong(const char *_uri, const Tag &_tag) noexcept
:uri(_uri), tag(_tag) {} :uri(_uri), tag(_tag) {}
/**
* A copy constructor which copies all fields, but only sets
* the tag to a caller-provided reference. This is used by
* the #ExportedSong move constructor.
*/
LightSong(const LightSong &src, const Tag &_tag) noexcept
:directory(src.directory), uri(src.uri),
real_uri(src.real_uri),
tag(_tag),
mtime(src.mtime),
start_time(src.start_time), end_time(src.end_time),
audio_format(src.audio_format) {}
gcc_pure gcc_pure
std::string GetURI() const noexcept { std::string GetURI() const noexcept {
if (directory == nullptr) if (directory == nullptr)

View File

@ -112,6 +112,6 @@ template <typename T>
void swap(ComPtr<T> &lhs, ComPtr<T> &rhs) noexcept { void swap(ComPtr<T> &lhs, ComPtr<T> &rhs) noexcept {
lhs.swap(rhs); lhs.swap(rhs);
} }
} } // namespace std
#endif #endif

View File

@ -59,6 +59,7 @@ case x:
C(E_INVALIDARG); C(E_INVALIDARG);
C(E_OUTOFMEMORY); C(E_OUTOFMEMORY);
C(E_POINTER); C(E_POINTER);
C(NO_ERROR);
#undef C #undef C
} }
return std::string_view(); return std::string_view();