Compare commits

...

231 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
Max Kellermann
18628bf89e release v0.22.9 2021-06-23 20:56:13 +02:00
Yetangitu
2052b461af Fix android build error when confronted with package versions ending in +revision_information
The script seems to assume package version numbers always end in numeric versions with an optional alpha-suffix. Alas, were it only so simple... Sometimes the package is called fizzbang-1.2.3+release_info in which case the build fails. No more!

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1177
2021-06-23 20:53:46 +02:00
Max Kellermann
5019bdcd52 TagAny: invoke ScanGenericTags() on remote files
This fixes reading ID3 tags on remote files with the commands
"readcomments" and "readpicture".

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1180
2021-06-23 20:49:30 +02:00
Naglis Jonaitis
8be0bcbdb9 doc/plugins.rst: mention default libsamplerate type 2021-06-23 15:51:31 +02:00
Naglis Jonaitis
af72a22ed8 doc/user.rst: document restore_paused 2021-06-23 15:50:41 +02:00
Naglis Jonaitis
6ed9668fea doc, README.md: update IRC server name/URL 2021-06-23 15:48:42 +02:00
Max Kellermann
175d2c6d29 Main: use AtScopeExit() to call ZeroconfDeinit()
Make sure that ZeroconfDeinit() gets called even if startup fails with
an exception.  Fixes an assertion failure because an Avahi TimerEvent
is still active.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1192
2021-06-22 20:31:45 +02:00
Max Kellermann
ab487b9a99 Android: use startForegroundService() in Android 8+
Fixes the error:

 IllegalStateException: Not allowed to start service Intent { cmp=org.musicpd/.Main (has extras) }: app is in background
2021-05-31 20:45:31 +02:00
Max Kellermann
ac59ec34f9 decoder/ffmpeg: fix build failure with FFmpeg 3.4
av_demuxer_iterate() was added in libavformat 58.9.100.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1178
2021-05-31 18:10:06 +02:00
Max Kellermann
82da57b7ce decoder/ffmpeg: suppress -Wunused with libavformat<58.6.100 2021-05-31 16:49:48 +02:00
Max Kellermann
aa6dac9bd2 db/proxy: suppress -Wunused with libmpdclient<2.12 2021-05-31 16:49:08 +02:00
Max Kellermann
a26bf261a9 input/last: call Close() in Open()
Prevents a possible bug which occurs when the caller-provided open()
function throws; then the "uri" field is never set.
2021-05-27 14:04:28 +02:00
Max Kellermann
c692286c67 input/last: clear "uri" field in Close()
Prevent false negative after the stream was closed automatically after
20 seconds.
2021-05-27 14:03:33 +02:00
Max Kellermann
3775766605 NEWS: mention new FFmpeg/ID3v2 tags 2021-05-26 13:07:03 +02:00
Max Kellermann
38e24208f6 decoder/ffmpeg: support the tags "album-sort", "artist-sort" 2021-05-26 13:04:47 +02:00
Max Kellermann
fbaedf2262 decoder/ffmpeg: support the "sort_album" tag
From libavformat/mov.c.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1173
2021-05-26 13:03:47 +02:00
Max Kellermann
8f3341cefb decoder/ffmpeg: add comment 2021-05-26 13:03:41 +02:00
Max Kellermann
4ec4bab3a9 decoder/ffmpeg: remove "year" tag
This mapping was added 11 years ago in commit 766b9fd453, but FFmpeg
doesn't appear to support it.
2021-05-26 13:03:27 +02:00
Max Kellermann
6d567bcd35 decoder/ffmpeg: fix ArtistSort and AlbumArtistSort mapping
These were added 11 years ago in commit 766b9fd453, but I cannot find
any evidence in the FFmpeg repository that these names were ever
supported.  This commit adds the tags as they are currently present in
libavformat/mov.c.
2021-05-26 13:03:26 +02:00
Max Kellermann
363d9f0180 db/update/Walk: load all .mpdignore files of all parent directories
When updating everything, this did work, but if updating only a
subdirectory, the ".mpdignore" in the parents were not used.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1172
2021-05-25 22:42:44 +02:00
Max Kellermann
db0682a469 db/update/Walk: move code to LoadExcludeList() 2021-05-25 22:38:01 +02:00
Max Kellermann
7a6823dcdf zeroconf/AvahiPoll: the struct timeval is an absolute time point
Fixes broken libavahi-client timeouts.
2021-05-25 22:25:45 +02:00
Max Kellermann
bce144a232 zeroconf/AvahiPoll: move code to Schedule() 2021-05-25 22:23:55 +02:00
Max Kellermann
0cef84cac6 zeroconf/AvahiPoll: rename "timer" to "event" 2021-05-25 22:23:55 +02:00
Max Kellermann
56c0733b42 meson.build: disable -Wsuggest-override with GCC 8 2021-05-25 22:23:55 +02:00
Max Kellermann
0b0acb3981 meson.build: add more C++ warning flags 2021-05-25 22:03:49 +02:00
Max Kellermann
1375dcc4ec meson.build: sort warning options 2021-05-25 21:49:03 +02:00
Max Kellermann
6aeb0e335b meson.build: add comment for -Wno-non-virtual-dtor 2021-05-25 21:48:19 +02:00
Max Kellermann
c1e2537851 meson.build: add comment for clang-only warning options 2021-05-25 21:45:39 +02:00
Max Kellermann
8c690fb737 decoder/mad: move variable declaration into "case" 2021-05-25 21:34:09 +02:00
Max Kellermann
dad1c21b59 zeroconf/avahi: move variable declaration into "case" 2021-05-25 21:34:09 +02:00
Max Kellermann
dd10b2bd61 meson.build: remove warning options implied by -Wall -Wextra 2021-05-25 21:24:44 +02:00
Max Kellermann
48c7c540df meson.build: use add_project_arguments() instead of add_global_arguments()
Don't propagate MPD-specific compiler flags to subprojects.
2021-05-25 21:08:06 +02:00
Max Kellermann
281270cd2a meson.build: remove unused variables common_cflags, common_cxxflags 2021-05-25 21:07:05 +02:00
Max Kellermann
02502514f6 meson.build: require clang 7 (remove bug workaround) 2021-05-25 21:06:55 +02:00
Max Kellermann
1bc02123f9 meson.build: remove "-pedantic", implied by Meson
Meson adds "-Wpedantic" in warning_level 3 (which is MPD's default).
2021-05-25 21:01:15 +02:00
Max Kellermann
3488a47c41 subprojects/sqlite3.wrap: add SQLite wrap 2021-05-25 20:51:03 +02:00
Max Kellermann
fd82d67678 sticker/Database: pass NarrowPath to sqlite3_open()
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1171
2021-05-25 18:45:45 +02:00
Max Kellermann
e66c12105b lib/sqlite/meson.build: add missing external dependency on libsqlite 2021-05-25 18:41:43 +02:00
Namkhai B
dbe12a6b90 util/RuntimeError: Disable format-security for gcc
Fixes building under GCC 11
2021-05-25 18:19:19 +02:00
Philippe Antoine
d3a680cc87 meson: set only sanitizers for fuzzer when unspecified
That is when meson option b_sanitize is not used
2021-05-24 09:03:16 +02:00
Max Kellermann
62fc4d5cf4 increment version number to 0.22.9 2021-05-24 09:03:07 +02:00
Max Kellermann
14465be847 release v0.22.8 2021-05-22 17:33:36 +02:00
Max Kellermann
0e49de867d input/last: add nullptr check to Open(), fixes assertion failure
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1168
2021-05-22 17:33:25 +02:00
Max Kellermann
f2e4529707 increment version number to 0.22.8 2021-05-22 17:32:00 +02:00
Max Kellermann
3547fc7e61 release v0.22.7 2021-05-19 18:13:26 +02:00
Max Kellermann
466a05bc52 CommandLine: update copyright year in --version output 2021-05-19 18:09:38 +02:00
Max Kellermann
6de4064cca client/Response, command/file: use %lu instead of %zu on Windows
Fixes -Wformat warnings.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1150
2021-05-19 18:06:40 +02:00
Max Kellermann
bcf0fdd3a8 meson.build: define NOUSER on Windows for lighter windows.h
A few exceptions are needed for sources which include COM header
(directly or indirectly).

This fixes lots of shadow warnings, see
https://github.com/MusicPlayerDaemon/MPD/issues/1150
2021-05-19 18:02:49 +02:00
Max Kellermann
a8f05a7efc win32/HResult: un-inline HRESULTToString() to reduce header dependencies 2021-05-19 17:48:42 +02:00
Max Kellermann
c64a3b5dbb fs/Glob: un-inline the Windows version to reduce header dependencies 2021-05-19 17:41:23 +02:00
Max Kellermann
16c38c438f fs/Glob: use defaulted move constructor 2021-05-19 17:40:23 +02:00
Max Kellermann
48cc4a6ced fs/Glob: remove redundant #ifdefs 2021-05-19 17:40:03 +02:00
Max Kellermann
a169a05e41 win32, ...: avoid including windows.h
Include the most specific header documented by MSDN instead.
2021-05-19 17:25:32 +02:00
Max Kellermann
a6cb3139db meson.build: disable Windows header features not needed by MPD 2021-05-19 17:16:16 +02:00
Max Kellermann
239a83324e meson.build: document Windows definitions 2021-05-19 17:15:25 +02:00
Max Kellermann
8efa5c7641 output/wasapi: use "%lu" in log calls
"%lu" is portable - it works with both POSIX and Microsoft flavors.

Fixes a part of https://github.com/MusicPlayerDaemon/MPD/issues/1150
2021-05-19 17:10:49 +02:00
Max Kellermann
28e7be248f util/RuntimeError: disable -Wformat-security as a kludge 2021-05-19 14:57:20 +02:00
Max Kellermann
c3f9b38c97 command/PlaylistCommands: pass real_uri to LookupRemoteTag()
For querying tags, the real song URI should be used, because if the
(display) URI is different, requesting it will not produce a usable
response.  This is a theoretical problem because none of the existing
playlist plugins sets the real_uri.

This requires changing the URI comparison in playlist::TagModified().

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1154
2021-05-18 21:35:09 +02:00
Max Kellermann
dbb18a401b command/file: cache the last "albumart" file
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1156
2021-05-18 17:04:09 +02:00
Max Kellermann
e1e41708af input/LastInputStream: new class 2021-05-18 17:04:09 +02:00
Max Kellermann
638dfc3981 {input,storage}/curl: set CURLOPT_HTTPAUTH=CURLAUTH_BASIC
With the default value CURLAUTH_ANY, libcurl needs to probe for
authentication methods first, and only the second request will have an
Authorization header.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1155
2021-05-17 19:26:05 +02:00
Max Kellermann
7c09e44ad4 python/build/libs.py: update OpenSSL to 3.0.0-alpha16 2021-05-17 18:42:05 +02:00
Max Kellermann
365b798f33 python/build/libs.py: update FFmpeg to 4.4 2021-05-17 18:42:05 +02:00
Max Kellermann
6f51d910ee python/build/libs.py: update CURL to 7.76.1 2021-05-17 18:42:05 +02:00
Max Kellermann
1215818572 doc/meson.build: remove "upload" target
Since we migrated to readthedocs.io, we don't need this target
anymore.  And Meson 0.58.0 apparently has a change breaking this
target.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1161
2021-05-17 18:33:00 +02:00
skidoo23
514ed33a02 python/build: update Boost URL and version 2021-05-11 13:11:16 +02:00
Max Kellermann
bfed47b82d .travis.yml: switch the OSX build to xcode11.6
The clang/libc++ version in xcode10.3 does not support C++17 properly
and cannot build MPD.
2021-04-28 13:40:36 +02:00
Max Kellermann
8c51440057 test/test_mixramp: workaround for -Wdouble-promotion 2021-04-28 13:00:42 +02:00
Max Kellermann
018858ec97 .travis.yml: install standard Homebrew GTest formula 2021-04-27 16:23:23 +02:00
Max Kellermann
3c1988b68f .travis.yml: switch from Ubuntu Bionic to Ubuntu Focal (20.04) 2021-04-27 16:23:23 +02:00
Max Kellermann
5452428d69 .travis.yml: switch to ppa:ricotz/toolchain for ninja 1.8 on Ubuntu Trusty
The old "ppa:mstipicevic/ninja-build-1-7-2" just provides ninja 1.7
which is too old and breaks the build.
2021-04-27 16:23:23 +02:00
Max Kellermann
d6bf6e161a .travis.yml: remove obsolete comment 2021-04-27 15:48:19 +02:00
Max Kellermann
a71b76bb3c test/test_pcm_format: another workaround for -Wdouble-promotion 2021-04-26 23:25:36 +02:00
Max Kellermann
c1429500b2 test/test_pcm_format: work around -Wdouble-promotion 2021-04-26 22:21:12 +02:00
Max Kellermann
0f02bbc2fe output/jack: enable on Windows
This enables the JACK output plugin on Windows, but doesn't link
against libjack64.dll, instead loads the DLL at runtime with
LoadLibrary().  This kludge avoids the extremely fragile JACK shared
memory protocol by using the system's libjack64.dll, without requiring
the same DLL at build time.
2021-04-26 21:47:20 +02:00
Max Kellermann
b885f358a5 output/control: add missing nullptr checks
Fixes crash when pausing the default partition after an output was
moved to another partition.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1147
2021-04-26 21:34:58 +02:00
Max Kellermann
650a30d794 Revert "tag/Pool: use strncmp() without strlen() to compare strings"
This reverts commit 1532983fb5.  This
optimization was bad because now all strings match if they are a
prefix of another string, and this caused collisions in the tag string
pool, corrupting the database.
2021-04-15 16:15:44 +02:00
Max Kellermann
1dc71f383a python/build/boost.py: touch boost/version.hpp to avoid reinstalling all the time 2021-04-07 13:50:05 +02:00
John Regan
6dfebf7df9 gme: add support for rsn files
Upcoming release of game-music-emu will support it, details here: https://bitbucket.org/mpyne/game-music-emu/pull-requests/23/rsn-support
2021-03-13 08:40:25 +01:00
Shen-Ta Hsieh
4bcdcca7f5 output/wasapi: use calculated new buffer instead old one 2021-03-13 08:39:56 +01:00
bitkeeper
c08a8581ee Added cross-origin header to http headers of the http output.
The current http output doesn't provide a header for cross-origin support. This prevents to use the mpd http stream directly from an other webapplication due the origin from the webpage differs from then the audio stream.

The fix is to add the following header to the http response:
Access-Control-Allow-Origin: *
2021-03-10 21:27:19 +01:00
Max Kellermann
25b0194036 output/wasapi: implement Drain() 2021-03-10 21:05:48 +01:00
Max Kellermann
77fe727e69 output/wasapi: move the "is_started" flag to class WasapiOutputThread 2021-03-10 20:43:28 +01:00
Max Kellermann
73f9824ddf output/wasapi: eliminate friend declaration 2021-03-10 20:38:41 +01:00
Max Kellermann
1fe0c673bc output/wasapi: implement Cancel() properly
Calling consume_all() is illegal in the producer thread.
2021-03-10 20:38:27 +01:00
Max Kellermann
8a045207a7 output/wasapi: add field paused
Fixes bogus Delay() results at the start of playback, because Delay()
thinks the output is paused.
2021-03-10 20:09:37 +01:00
Max Kellermann
fe7c5a4208 output/wasapi: initialize is_started in Open() 2021-03-10 20:07:22 +01:00
Max Kellermann
8024f7e84d output/wasapi: move the thread->Play() call right before the consumed_size check
Fixes a bogus assertion failure (which can now be removed).
2021-03-10 20:07:19 +01:00
Max Kellermann
14f0134097 output/wasapi: make device_config const 2021-03-10 20:05:14 +01:00
Max Kellermann
1da27be84d output/wasapi: move runtime fields below configuration fields 2021-03-10 20:00:08 +01:00
Max Kellermann
08135f2cb7 output/wasapi: make configuration fields const 2021-03-10 19:58:33 +01:00
Max Kellermann
5907656bbb output/wasapi: stop the IAudioClient while paused
Instead of generating silence, do nothing, don't waste CPU time.
2021-03-10 17:48:49 +01:00
Max Kellermann
2ac2bd26f8 output/wasapi: combine two if statements to one switch 2021-03-10 17:45:54 +01:00
Max Kellermann
a2be91aea5 output/wasapi: add method WasapiOutputThread::InterruptWaiter() 2021-03-10 17:42:26 +01:00
Max Kellermann
579428172e output/wasapi: remove the broken Delay() calculation code
This code is complicated - and broken: the producer thread is not
allowed to call consumer methods.  Also the code is not necessary
because this plugin implements Interrupt().
2021-03-10 17:39:07 +01:00
Max Kellermann
3e484637f9 output/wasapi: rename OpenDevice() to ChooseDevice()
OpenDevice was a confusing name because it does not actually open a
device.
2021-03-10 17:34:10 +01:00
Max Kellermann
3e93c392d7 output/wasapi: make enumerator a local variable 2021-03-10 17:23:41 +01:00
Max Kellermann
0a97e68aa9 output/wasapi: start after the buffer has been filled
Postpone the Start() call until there is something to be played.
2021-03-08 23:03:25 +01:00
Max Kellermann
69783a44c8 output/wasapi: move Start()/Stop() calls to WasapiOutputThread::Work() 2021-03-08 22:58:20 +01:00
Max Kellermann
d72263d28d win32/HResult: support AUDCLNT_E_NOT_{INITIALIZED,STOPPED} 2021-03-08 22:57:44 +01:00
Max Kellermann
24a205a1aa win32/HResult: try to use FormatMessage() 2021-03-08 22:54:46 +01:00
Max Kellermann
3a948515ce output/wasapi: check for exceptions after Wait()
This finishes problems which occur early in the WasapiOutputThread;
previously, the error was ignored and the output blocked forever
without doing anything (and without reporting the error).
2021-03-08 22:46:40 +01:00
Max Kellermann
9ade93983c output/wasapi: rename method WaitDataPoped() to Wait() 2021-03-08 22:44:49 +01:00
Max Kellermann
6931ce9558 output/wasapi: make the Thread a field, not a base class 2021-03-08 22:30:19 +01:00
Max Kellermann
d6fb07a3e4 output/wasapi: start the WasapiOutputThread in its constructor 2021-03-08 22:29:33 +01:00
Max Kellermann
01d3c2705e output/wasapi: Finish() calls Join() 2021-03-08 22:28:36 +01:00
Max Kellermann
29346dc9c5 output/wasapi: remove the thread management code from DoDisable()
This is duplicate; this has already been done in Close().
2021-03-08 22:27:46 +01:00
Max Kellermann
d19b3df3b0 test/run_output: call AudioOutput::Drain() 2021-03-08 22:20:55 +01:00
Max Kellermann
798e68ef62 output/wasapi: don't clear the exception in CheckException()
This is pointless; the method cannot be called again anyway.
2021-03-08 22:18:48 +01:00
Max Kellermann
79397db5b4 output/wasapi: remove the "thrown" field
It is pointless to let WasapiOutputThread wait for the
CheckException() call.
2021-03-08 22:17:45 +01:00
Max Kellermann
9256190a9b output/wasapi: move catch block to the Work() function level
If an exception has been caught, the method cannot continue playback,
therefore it doesn't make sense to have the "catch" block inside the
"while" block (and not break the loop after catching an exception).
2021-03-08 22:15:36 +01:00
Max Kellermann
3a0dbb0a67 output/wasapi: make WasapiOutputThread::is_exclusive const 2021-03-08 22:09:23 +01:00
Max Kellermann
3d6c9d1b88 output/wasapi: catch all exception 2021-03-08 22:06:29 +01:00
Max Kellermann
5823e79fe7 output/wasapi: remove broken Drain() implementation
The current Drain() implementation does what Cancel() should do; it
does not wait for completion, but instead discards the buffer.
2021-03-08 21:41:34 +01:00
Max Kellermann
5f656dffda output/wasapi: implement Cancel() 2021-03-08 19:58:15 +01:00
Max Kellermann
34d4d9157a output/wasapi: add inline 2021-03-08 19:57:40 +01:00
Max Kellermann
22c329cdb4 output/wasapi: convert pointer to reference 2021-03-08 19:56:56 +01:00
Max Kellermann
980ef82216 output/wasapi: move SetEventHandle() call to thread constructor 2021-03-08 17:52:44 +01:00
Max Kellermann
84a06a72df output/wasapi: fix coding style 2021-03-08 17:52:43 +01:00
Max Kellermann
4833d0891d output/wasapi: eliminate kErrorId 2021-03-08 17:47:07 +01:00
Max Kellermann
cd53ca22c6 output/wasapi: remove unused function SafeTry() 2021-03-08 17:43:36 +01:00
Max Kellermann
4d9af9a81b test/run_{input,output,convert}: switch file descriptors to binary mode
Fixes those programs on Windows.
2021-03-08 17:27:23 +01:00
Max Kellermann
d61341c0e3 io/FileDescriptor: add method SetBinaryMode() 2021-03-08 17:25:36 +01:00
Max Kellermann
eff50b263a test/run_output: use class StaticFifoBuffer 2021-03-08 17:01:48 +01:00
Max Kellermann
2bebc79363 test/run_convert: use std::byte 2021-03-08 16:58:43 +01:00
Max Kellermann
e777fb4edb test/run_convert: pass FileDescriptor to RunConvert() 2021-03-08 16:51:57 +01:00
Max Kellermann
3fb25d4062 test/run_convert: move code to RunConvert() 2021-03-08 16:46:21 +01:00
Max Kellermann
e227596c20 test/run_output: pass FileDescriptor to run_output() 2021-03-08 16:44:15 +01:00
Max Kellermann
ec76583c33 win32/Com: add COINIT_DISABLE_OLE1DDE
MSDN documentation suggests always passing this flag to reduce
overhead for an "obsolete technology".
2021-03-08 14:03:33 +01:00
Max Kellermann
927f1e03a3 win32/Com: make COINIT_APARTMENTTHREADED the default constructor 2021-03-08 14:02:49 +01:00
Max Kellermann
f2c679cfec win32/Com: remove the unused COINIT_MULTITHREADED constructor 2021-03-08 14:02:49 +01:00
Max Kellermann
6a75c48dba win32/HResult: add MakeHResultError()
None of the current FormatHResultError() callers need the format string.
2021-03-08 13:46:36 +01:00
Max Kellermann
48bdd09f64 win32/ComWorker: fold class COMWorkerThread into class COMWorker 2021-03-07 18:22:59 +01:00
Max Kellermann
cf108c389f win32/ComWorker: remove parameter passing from Async()
Parameters should better be captured.  This removes some complexity
from Async().
2021-03-07 18:20:59 +01:00
Max Kellermann
90d97053a8 win32/ComWorker: make COMWorker a real class, no static members 2021-03-06 20:46:29 +01:00
Shen-Ta Hsieh
e1fe9ebcd6 output/wasapi: Add dop support for WASAPI
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1102
2021-03-05 19:40:32 +01:00
Max Kellermann
93016ac6ab output/wasapi: check AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED
Stop early, don't try more formats if it is clear that we have no
chance.
2021-03-05 19:33:38 +01:00
Max Kellermann
fc20a1f10a output/wasapi: EnumerateDevices() logs, no std::vector 2021-03-05 19:27:52 +01:00
Max Kellermann
a4257e51d5 output/wasapi: reimplement SearchDevice() without EnumerateDevices() 2021-03-05 19:25:42 +01:00
Max Kellermann
2f2b3f1cdc output/wasapi: SearchDevice() returns IMMDevice 2021-03-05 19:25:42 +01:00
Max Kellermann
2ff6a9ad2b output/wasapi: GetDevice() returns IMMDevice 2021-03-05 19:25:42 +01:00
Max Kellermann
17d4873b60 output/wasapi: use default device only if none was configured 2021-03-05 19:25:42 +01:00
Max Kellermann
8b41c4f384 output/wasapi: release the COMWorker if OpenDevice() fails
Fixes assertion failure in the Thread destructor.
2021-03-05 19:25:42 +01:00
Max Kellermann
17f7098e27 output/wasapi: SafeTry() catches all exceptions
Fixes crash due to std::stoul() throwing std::invalid_argument.
2021-03-05 19:12:22 +01:00
Max Kellermann
9ff790b7bb output/wasapi: move COM utilities to separate headers 2021-03-05 18:33:31 +01:00
Max Kellermann
ebc1fe2821 win32/ComPtr: operator*() returns reference 2021-03-05 17:39:48 +01:00
Max Kellermann
bc2988144e test/run_output: use OptionParser, add option "--verbose" 2021-03-05 17:39:48 +01:00
Max Kellermann
b1a9958c66 test/run_output: add struct CommandLine 2021-03-05 17:39:48 +01:00
Max Kellermann
e6a81bb95c output/wasapi: split the header
Reduce header dependencies.
2021-03-05 16:43:44 +01:00
Max Kellermann
9521c1ad58 output/wasapi: use forward declarations in the header 2021-03-05 16:42:38 +01:00
Max Kellermann
6d65cc48d7 output/wasapi: use [[gnu::pure]] 2021-03-05 16:42:15 +01:00
Max Kellermann
681956a963 output/wasapi: include cleanup 2021-03-05 16:42:14 +01:00
Max Kellermann
052f64d648 output/wasapi: include config.h for ENABLE_DSD 2021-03-05 16:35:21 +01:00
Max Kellermann
afe621c25c output/wasapi: move to separate directory 2021-03-05 16:28:36 +01:00
Max Kellermann
637cf8a039 win32/WinEvent: add default value to Wait() 2021-03-05 16:05:32 +01:00
Max Kellermann
2011a6e2ee win32/WinEvent: un-inline the constructor
Reduce header dependencies.
2021-03-05 16:01:23 +01:00
Max Kellermann
d54830de12 thread/WindowsFuture: include cleanup 2021-03-05 13:50:16 +01:00
Max Kellermann
a7e7312cca win32/HResult: un-inline HResultCategory::message() 2021-03-05 13:40:40 +01:00
Max Kellermann
6b83fc6b57 win32/HResult: un-inline FormatHResultError()
Reduce header dependencies.
2021-03-05 13:40:38 +01:00
Max Kellermann
74f9e07151 win32/HResult: include cleanup 2021-03-05 13:40:37 +01:00
Max Kellermann
82a61ab3be win32/meson.build: fix syntax error 2021-03-05 13:40:34 +01:00
Max Kellermann
54c1794cee win32: build static library
Fixes linker failure on test/run_output.exe
2021-03-05 13:32:58 +01:00
Max Kellermann
c962a6be76 test/run_convert: fix Windows compiler errors 2021-03-05 13:24:28 +01:00
Max Kellermann
922c4bf3f0 test/TestLookupFile: fix Windows compiler errors 2021-03-05 13:22:34 +01:00
Max Kellermann
932756efce win32/ComWorker: fix the FormatHResultError() return type
Casting to std::runtime_error loses information (and prevents RVO).
2021-03-05 13:17:40 +01:00
Max Kellermann
7838265482 win32/ComWorker: remove debug log messages 2021-03-05 13:16:09 +01:00
Max Kellermann
b14b0e5634 win32/ComWorker: reorder includes 2021-03-05 13:15:45 +01:00
Max Kellermann
4d2d0e7bb8 win32/ComWorker: include cleanup 2021-03-05 13:15:21 +01:00
Rosen Penev
44378b7dbe use structured binding declarations
Shorter.

Signed-off-by: Rosen Penev <rosenp@gmail.com>
2021-03-04 20:28:02 +01:00
Shen-Ta Hsieh
da642b2890 src/output: add algorithm for finding usable AudioFormat
* Use PcmExport for 24bit packed output
2021-03-04 18:53:58 +01:00
Shen-Ta Hsieh
6f77af20d0 src/output: Set fallback setting for DSD 2021-03-04 18:50:56 +01:00
Shen-Ta Hsieh
010f65a1d6 src/output: Add Interrupt interface 2021-03-04 18:50:09 +01:00
Shen-Ta Hsieh
c46f97454a src/output: Reopen device on error 2021-03-04 18:49:28 +01:00
Shen-Ta Hsieh
844dbd2ec5 src/output: Use WinEvent for as a condition_variable without lock 2021-03-04 18:46:26 +01:00
Shen-Ta Hsieh
db7caa2dac src/output: Move event and spsc_queue into thread object 2021-03-04 18:45:56 +01:00
Shen-Ta Hsieh
2974737746 src/win32: Add ComWorker to run all COM function on same thread 2021-03-04 18:43:43 +01:00
Shen-Ta Hsieh
b1d7567226 win32: Add ComWorker to run all COM function on same thread 2021-03-04 18:43:16 +01:00
Max Kellermann
5103eb3039 meson.build: compile Win32Main.cxx only on Windows 2021-03-04 18:43:00 +01:00
Shen-Ta Hsieh
0cccdcf9b2 src/win32: Add support for COINIT_APARTMENTTHREADED 2021-03-04 18:37:56 +01:00
Shen-Ta Hsieh
22b840c2f1 win32/Com: use if with init-statement 2021-03-04 18:37:35 +01:00
Shen-Ta Hsieh
ed1a995bff thread: Add Future implement for mingw32 without pthread 2021-03-04 18:26:46 +01:00
Shen-Ta Hsieh
0f39dc1edb output/wasapi: use AUDCLNT_BUFFERFLAGS_SILENT for paused output 2021-03-04 18:17:57 +01:00
Max Kellermann
dc9103befe util/AllocatedString: remove Null(), IsNull() 2021-03-04 18:05:29 +01:00
Max Kellermann
67760f5283 util/AllocatedString: support casting a nulled instance to string_view 2021-03-04 18:05:29 +01:00
Max Kellermann
99405a4c93 util/AllocatedString: add operator=() 2021-03-04 18:05:26 +01:00
Max Kellermann
b833c5d2c7 util/AllocatedString: replace Clone() with copy constructor 2021-03-04 18:04:21 +01:00
Max Kellermann
bca5d79f88 util/AllocatedString: add const_pointer constructor 2021-03-04 18:04:17 +01:00
Max Kellermann
6e1c8edf09 util/AllocatedString: add string_view constructor
Replaces the static Duplicate() method.
2021-03-04 18:04:11 +01:00
Max Kellermann
32b7b2e2fa util/AllocatedString: add default constructor 2021-03-04 18:04:06 +01:00
Max Kellermann
cfb7f8ab84 util/AllocatedString: rename to BasicAllocatedString
To make things simpler, AllocatedString is now a non-template class.
2021-03-04 18:03:56 +01:00
Érico Rolim
8d80280ab9 time/ISO8601: don't use glibc extension in strptime.
Per the manual for strptime, %F is equivalent %Y-%m-%d, so use that
directly.
2021-03-04 17:49:51 +01:00
Érico Rolim
c95e3dc065 storage/plugins/CurlStorage: don't use glibc extension in
ParseTimePoint.

%Z is a glibc extension to strptime, and is a no-op there, due to the
mapping between timezone names and their definition (especially when the
name comes from a different machine) being ambiguous / impossible.  Time
in HTTP headers is guaranteed to be UTC.

Passing an unknown format to strptime() implementations that don't
support it will generally cause them to return NULL, which will lead to
ParseTimePoint throwing an exception and ParseTimeStamp using an
unnecessary fallback.

Since the timezone name goes at the end of the string, we don't need to
use %Z to skip it (could be an issue in a different time stamp format),
so simply removing %Z works best.
2021-03-04 17:48:23 +01:00
Max Kellermann
00a520a4c3 doc/user.rst: update Windows&Android build dependencies
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1112
2021-02-26 00:59:10 +01:00
Max Kellermann
6eba621045 decoder/ffmpeg: fix build problem with FFmpeg 3.4
Regression by commit a22d1c88d7

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1097
2021-02-22 13:36:46 +01:00
Max Kellermann
a9ad8fa505 decoder/ffmpeg: move code to IsSeekable(AVFormatContext) 2021-02-22 13:33:25 +01:00
Max Kellermann
85427826aa increment version number to 0.22.7 2021-02-17 14:36:06 +01:00
Florian Schlichting
25e0a90402 fix typo in protocol.rst
Exmaple -> Example
2021-02-17 03:18:23 +01:00
164 changed files with 3804 additions and 3142 deletions

View File

@@ -2,70 +2,37 @@ language: cpp
jobs:
include:
# Ubuntu Bionic (18.04) with GCC 7
# Ubuntu Focal (20.04) with GCC 9.3
- os: linux
dist: bionic
dist: focal
addons:
apt:
sources:
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
packages:
- meson
- libgtest-dev
- libboost-dev
- python3.6
- python3-urllib3
- ninja-build
before_install:
- wget https://bootstrap.pypa.io/get-pip.py
- /usr/bin/python3.6 get-pip.py --user --no-cache-dir
install:
- /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson --no-cache-dir
env:
- MATRIX_EVAL="export PATH=\$HOME/.local/bin:\$PATH"
# Ubuntu Bionic (18.04) with GCC 7 on big-endian
# Ubuntu Focal (20.04) with GCC 9.3 on big-endian
- os: linux
arch: s390x
dist: bionic
dist: focal
addons:
apt:
sources:
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
packages:
- meson
- libgtest-dev
- libboost-dev
- python3.6
- python3-urllib3
- ninja-build
before_install:
- wget https://bootstrap.pypa.io/get-pip.py
- /usr/bin/python3.6 get-pip.py --user --no-cache-dir
install:
- /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson --no-cache-dir
env:
- MATRIX_EVAL="export PATH=\$HOME/.local/bin:\$PATH"
# Ubuntu Bionic (18.04) with GCC 7 on ARM64
# Ubuntu Focal (20.04) with GCC 9.3 on ARM64
- os: linux
arch: arm64
dist: bionic
dist: focal
addons:
apt:
sources:
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
packages:
- meson
- libgtest-dev
- libboost-dev
- python3.6
- python3-urllib3
- ninja-build
before_install:
- wget https://bootstrap.pypa.io/get-pip.py
- /usr/bin/python3.6 get-pip.py --user --no-cache-dir
install:
- /usr/bin/python3.6 $HOME/.local/bin/pip install --user meson --no-cache-dir
env:
- MATRIX_EVAL="export PATH=\$HOME/.local/bin:\$PATH"
# Ubuntu Trusty (16.04) with GCC 8
- os: linux
@@ -75,7 +42,7 @@ jobs:
sources:
- ubuntu-toolchain-r-test
- sourceline: 'ppa:mhier/libboost-latest'
- sourceline: 'ppa:mstipicevic/ninja-build-1-7-2'
- sourceline: 'ppa:ricotz/toolchain'
- sourceline: 'ppa:deadsnakes/ppa' # for Python 3.7 (required by Meson)
packages:
- g++-8
@@ -94,12 +61,13 @@ jobs:
- MATRIX_EVAL="export CC='ccache gcc-8' CXX='ccache g++-8' LDFLAGS=-fuse-ld=gold PATH=\$HOME/.local/bin:\$PATH"
- os: osx
osx_image: xcode10.3
osx_image: xcode11.6
addons:
homebrew:
packages:
- ccache
- meson
- googletest
- icu4c
- ffmpeg
- libnfs
@@ -117,7 +85,6 @@ jobs:
- faad2
- wavpack
- libmpdclient
update: true
env:
- MATRIX_EVAL="export PATH=/usr/local/opt/ccache/libexec:$PATH HOMEBREW_NO_ANALYTICS=1"
@@ -134,13 +101,6 @@ before_install:
- eval "${MATRIX_EVAL}"
install:
# C++14
# Work around "Target /usr/local/lib/libgtest.a is a symlink
# belonging to nss. You can unlink it" during gtest install
- test "$TRAVIS_OS_NAME" != "osx" || brew unlink nss
- test "$TRAVIS_OS_NAME" != "osx" || brew install https://gist.githubusercontent.com/Kronuz/96ac10fbd8472eb1e7566d740c4034f8/raw/gtest.rb
before_script:
- ccache -s

55
NEWS
View File

@@ -1,3 +1,58 @@
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
* tags
- fix "readcomments" and "readpicture" on remote files with ID3 tags
* decoder
- ffmpeg: support the tags "sort_album", "album-sort", "artist-sort"
- ffmpeg: fix build failure with FFmpeg 3.4
* Android
- fix auto-start on boot in Android 8 or later
* Windows
- fix build failure with SQLite
ver 0.22.8 (2021/05/22)
* fix crash bug in "albumart" command (0.22.7 regression)
ver 0.22.7 (2021/05/19)
* protocol
- don't use glibc extension to parse time stamps
- optimize the "albumart" command
* input
- curl: send user/password in the first request, save one roundtrip
* decoder
- ffmpeg: fix build problem with FFmpeg 3.4
- gme: support RSN files
* storage
- curl: don't use glibc extension
* database
- simple: fix database corruption bug
* output
- fix crash when pausing with multiple partitions
- jack: enable on Windows
- httpd: send header "Access-Control-Allow-Origin: *"
- wasapi: add algorithm for finding usable audio format
- wasapi: use default device only if none was configured
- wasapi: add DoP support
ver 0.22.6 (2021/02/16)
* fix missing tags on songs in queue

View File

@@ -14,7 +14,7 @@ For basic installation instructions
- [Manual](http://www.musicpd.org/doc/user/)
- [Forum](http://forum.musicpd.org/)
- [IRC](irc://chat.freenode.net/#mpd)
- [IRC](ircs://irc.libera.chat:6697/#mpd)
- [Bug tracker](https://github.com/MusicPlayerDaemon/MPD/issues/)
# Developers

View File

@@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
android:versionCode="54"
android:versionName="0.22.6">
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

@@ -414,6 +414,15 @@ public class Main extends Service implements Runnable {
* start Main service without any callback
*/
public static void start(Context context, boolean wakelock) {
context.startService(new Intent(context, Main.class).putExtra("wakelock", wakelock));
Intent intent = new Intent(context, Main.class)
.putExtra("wakelock", wakelock);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
/* in Android 8+, we need to use this method
or else we'll get "IllegalStateException:
app is in background" */
context.startForegroundService(intent);
else
context.startService(intent);
}
}

View File

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

View File

@@ -22,20 +22,6 @@ if get_option('html_manual')
install: true,
install_dir: join_paths(get_option('datadir'), 'doc', meson.project_name()),
)
custom_target(
'upload',
input: sphinx_output,
output: 'upload',
build_always_stale: true,
command: [
'rsync', '-vpruz', '--delete', meson.current_build_dir() + '/',
'www.musicpd.org:/var/www/mpd/doc/',
'--chmod=Dug+rwx,Do+rx,Fug+rw,Fo+r',
'--include=html', '--include=html/**',
'--exclude=*',
],
)
endif
if get_option('manpages')

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
@@ -715,7 +684,7 @@ A resampler using `libsamplerate <http://www.mega-nerd.com/SRC/>`_ a.k.a. Secret
* - Name
- Description
* - **type**
- The interpolator type. See below for a list of known types.
- The interpolator type. Defaults to :samp:`2`. See below for a list of known types.
The following converter types are provided by libsamplerate:
@@ -910,6 +879,10 @@ jack
The jack plugin connects to a `JACK server <http://jackaudio.org/>`_.
On Windows, this plugin loads :file:`libjack64.dll` at runtime. This
means you need to `download and install the JACK windows build
<https://jackaudio.org/downloads/>`_.
.. list-table::
:widths: 20 80
:header-rows: 1
@@ -1171,6 +1144,8 @@ The `Windows Audio Session API <https://docs.microsoft.com/en-us/windows/win32/c
- Enumerate all devices in log while playing started. Useful for device configuration. The default value is "no".
* - **exclusive yes|no**
- Exclusive mode blocks all other audio source, and get best audio quality without resampling. Stopping playing release the exclusive control of the output device. The default value is "no".
* - **dop yes|no**
- Enable DSD over PCM. Require exclusive mode. The default value is "no".
.. _filter_plugins:
@@ -1194,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

@@ -678,7 +678,7 @@ Whenever possible, ids should be used.
can also be a single file.
Clients that are connected via local socket may add arbitrary
local files (URI is an absolute path). Exmaple::
local files (URI is an absolute path). Example::
add "/home/foo/Music/bar.ogg"

View File

@@ -55,7 +55,7 @@ and unpack it (or `clone the git repository
In any case, you need:
* a C++17 compiler (e.g. GCC 8 or clang 5)
* a C++17 compiler (e.g. GCC 8 or clang 7)
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
<https://ninja-build.org/>`__
* Boost 1.58
@@ -141,6 +141,15 @@ Basically, there are two ways to compile :program:`MPD` for Windows:
This section is about the latter.
You need:
* `mingw-w64 <http://mingw-w64.org/doku.php>`__
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
<https://ninja-build.org/>`__
* cmake
* pkg-config
* quilt
Just like with the native build, unpack the :program:`MPD` source
tarball and change into the directory. Then, instead of
:program:`meson`, type:
@@ -168,6 +177,11 @@ You need:
* Android SDK
* `Android NDK r22 <https://developer.android.com/ndk/downloads>`_
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
<https://ninja-build.org/>`__
* cmake
* pkg-config
* quilt
Just like with the native build, unpack the :program:`MPD` source
tarball and change into the directory. Then, instead of
@@ -614,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.
@@ -674,6 +689,8 @@ The State File
- Specify the state file location. The parent directory must be writable by the :program:`MPD` user (+wx).
* - **state_file_interval SECONDS**
- Auto-save the state file this number of seconds after each state change. Defaults to 120 (2 minutes).
* - **restore_paused yes|no**
- If set to :samp:`yes`, then :program:`MPD` is put into pause mode instead of starting playback after startup. Default is :samp:`no`.
The Sticker Database
^^^^^^^^^^^^^^^^^^^^
@@ -1106,7 +1123,7 @@ Support
Getting Help
^^^^^^^^^^^^
The :program:`MPD` project runs a `forum <https://forum.musicpd.org/>`_ and an IRC channel (#mpd on Freenode) for requesting help. Visit the MPD help page for details on how to get help.
The :program:`MPD` project runs a `forum <https://forum.musicpd.org/>`_ and an IRC channel (#mpd on Libera.Chat) for requesting help. Visit the MPD help page for details on how to get help.
Common Problems
^^^^^^^^^^^^^^^

View File

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.22.6',
version: '0.22.10',
meson_version: '>= 0.49.0',
default_options: [
'c_std=c11',
@@ -24,8 +24,8 @@ c_compiler = meson.get_compiler('c')
if compiler.get_id() == 'gcc' and compiler.version().version_compare('<8')
warning('Your GCC version is too old. You need at least version 8.')
elif compiler.get_id() == 'clang' and compiler.version().version_compare('<5')
warning('Your clang version is too old. You need at least version 5.')
elif compiler.get_id() == 'clang' and compiler.version().version_compare('<7')
warning('Your clang version is too old. You need at least version 7.')
endif
version_conf = configuration_data()
@@ -42,57 +42,64 @@ common_cppflags = [
'-D_GNU_SOURCE',
]
common_cflags = [
]
common_cxxflags = [
test_global_common_flags = [
'-fvisibility=hidden',
]
test_common_flags = [
'-Wvla',
'-Wdouble-promotion',
'-fvisibility=hidden',
'-ffast-math',
'-ftree-vectorize',
]
test_global_cxxflags = test_global_common_flags + [
]
test_global_cflags = test_global_common_flags + [
]
test_cxxflags = test_common_flags + [
'-fno-threadsafe-statics',
'-fmerge-all-constants',
'-Wmissing-declarations',
'-Wshadow',
'-Wpointer-arith',
'-Wcast-qual',
'-Wwrite-strings',
'-Wsign-compare',
'-Wcomma',
'-Wcomma-subscript',
'-Wextra-semi',
'-Wmismatched-tags',
'-Wmissing-declarations',
'-Woverloaded-virtual',
'-Wshadow',
'-Wsign-promo',
'-Wunused',
'-Wvolatile',
'-Wvirtual-inheritance',
'-Wwrite-strings',
# a vtable without a dtor is just fine
'-Wno-non-virtual-dtor',
# clang specific warning options:
'-Wcomma',
'-Wheader-hygiene',
'-Winconsistent-missing-destructor-override',
'-Wunreachable-code-break',
'-Wunused',
'-Wunreachable-code-aggressive',
'-Wused-but-marked-unused',
'-Wno-non-virtual-dtor',
]
if compiler.get_id() == 'clang'
# Workaround for clang bug
# https://bugs.llvm.org/show_bug.cgi?id=32611
test_cxxflags += '-funwind-tables'
if compiler.get_id() != 'gcc' or compiler.version().version_compare('>=9')
# The GCC 8 implementation of this flag is buggy: it complains even
# if "final" is present, which implies "override".
test_cxxflags += '-Wsuggest-override'
endif
test_cflags = test_common_flags + [
'-Wcast-qual',
'-Wmissing-prototypes',
'-Wshadow',
'-Wpointer-arith',
'-Wstrict-prototypes',
'-Wcast-qual',
'-Wwrite-strings',
'-pedantic',
]
test_ldflags = [
@@ -104,11 +111,11 @@ test_ldflags = [
]
if get_option('buildtype') != 'debug'
test_cxxflags += [
test_global_cxxflags += [
'-ffunction-sections',
'-fdata-sections',
]
test_cflags += [
test_global_cflags += [
'-ffunction-sections',
'-fdata-sections',
]
@@ -118,15 +125,20 @@ if get_option('buildtype') != 'debug'
endif
if get_option('fuzzer')
fuzzer_flags = ['-fsanitize=fuzzer,address,undefined']
fuzzer_flags = ['-fsanitize=fuzzer']
if get_option('b_sanitize') == 'none'
fuzzer_flags += ['-fsanitize=address,undefined']
endif
add_global_arguments(fuzzer_flags, language: 'cpp')
add_global_arguments(fuzzer_flags, language: 'c')
add_global_link_arguments(fuzzer_flags, language: 'cpp')
endif
add_global_arguments(common_cxxflags + compiler.get_supported_arguments(test_cxxflags), language: 'cpp')
add_global_arguments(common_cflags + c_compiler.get_supported_arguments(test_cflags), language: 'c')
add_global_link_arguments(compiler.get_supported_link_arguments(test_ldflags), language: 'cpp')
add_global_arguments(compiler.get_supported_arguments(test_global_cxxflags), language: 'cpp')
add_global_arguments(c_compiler.get_supported_arguments(test_global_cflags), language: 'c')
add_project_arguments(compiler.get_supported_arguments(test_cxxflags), language: 'cpp')
add_project_arguments(c_compiler.get_supported_arguments(test_cflags), language: 'c')
add_project_link_arguments(compiler.get_supported_link_arguments(test_ldflags), language: 'cpp')
is_linux = host_machine.system() == 'linux'
is_android = get_option('android_ndk') != ''
@@ -140,10 +152,29 @@ endif
if is_windows
common_cppflags += [
'-DWIN32_LEAN_AND_MEAN',
# enable Windows Vista APIs
'-DWINVER=0x0600', '-D_WIN32_WINNT=0x0600',
'-DSTRICT',
# enable Unicode support (TCHAR=wchar_t) in the Windows API (macro
# "UNICODE) and the C library (macro "_UNICODE")
'-DUNICODE', '-D_UNICODE',
# enable strict type checking in the Windows API headers
'-DSTRICT',
# reduce header bloat by disabling obscure and obsolete Windows
# APIs
'-DWIN32_LEAN_AND_MEAN',
# disable more Windows APIs which are not used by MPD
'-DNOGDI', '-DNOBITMAP', '-DNOCOMM',
'-DNOUSER',
# reduce COM header bloat
'-DCOM_NO_WINDOWS_H',
# disable Internet Explorer specific APIs
'-D_WIN32_IE=0',
]
subdir('win32')
@@ -272,7 +303,6 @@ sources = [
'src/LogInit.cxx',
'src/ls.cxx',
'src/Instance.cxx',
'src/win32/Win32Main.cxx',
'src/MusicBuffer.cxx',
'src/MusicPipe.cxx',
'src/MusicChunk.cxx',
@@ -320,6 +350,12 @@ sources = [
'src/PlaylistFile.cxx',
]
if is_windows
sources += [
'src/win32/Win32Main.cxx',
]
endif
if not is_android
sources += [
'src/CommandLine.cxx',
@@ -354,6 +390,7 @@ subdir('src/system')
subdir('src/thread')
subdir('src/net')
subdir('src/event')
subdir('src/win32')
subdir('src/apple')

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

@@ -21,3 +21,8 @@ class BoostProject(Project):
dest = os.path.join(includedir, 'boost')
shutil.rmtree(dest, ignore_errors=True)
shutil.copytree(os.path.join(src, 'boost'), dest)
# touch the boost/version.hpp file to ensure it's newer than
# the downloaded Boost tarball, to avoid reinstalling Boost on
# every run
os.utime(os.path.join(toolchain.install_prefix, self.installed))

47
python/build/jack.py Normal file
View File

@@ -0,0 +1,47 @@
import os, shutil
import re
from .project import Project
# This class installs just the public headers and a fake pkg-config
# file which defines the macro "DYNAMIC_JACK". This tells MPD's JACK
# output plugin to load the libjack64.dll dynamically using
# LoadLibrary(). This kludge avoids the runtime DLL dependency for
# users who don't use JACK, but still allows using the system JACK
# client library.
#
# The problem with JACK is that it uses an extremely fragile shared
# memory protocol to communicate with the daemon. One needs to use
# daemon and client library from the same build. That's why we don't
# build libjack statically here; it would probably not be compatible
# with the user's JACK daemon.
class JackProject(Project):
def __init__(self, url, md5, installed,
**kwargs):
m = re.match(r'.*/v([\d.]+)\.tar\.gz$', url)
self.version = m.group(1)
Project.__init__(self, url, md5, installed,
name='jack2', version=self.version,
base='jack2-' + self.version,
**kwargs)
def build(self, toolchain):
src = self.unpack(toolchain)
includes = ['jack.h', 'ringbuffer.h', 'systemdeps.h', 'transport.h', 'types.h', 'weakmacros.h']
includedir = os.path.join(toolchain.install_prefix, 'include', 'jack')
os.makedirs(includedir, exist_ok=True)
for i in includes:
shutil.copyfile(os.path.join(src, 'common', 'jack', i),
os.path.join(includedir, i))
with open(os.path.join(toolchain.install_prefix, 'lib', 'pkgconfig', 'jack.pc'), 'w') as f:
print("prefix=" + toolchain.install_prefix, file=f)
print("", file=f)
print("Name: jack", file=f)
print("Description: dummy", file=f)
print("Version: " + self.version, file=f)
print("Libs: ", file=f)
print("Cflags: -DDYNAMIC_JACK", file=f)

View File

@@ -9,6 +9,7 @@ from build.autotools import AutotoolsProject
from build.ffmpeg import FfmpegProject
from build.openssl import OpenSSLProject
from build.boost import BoostProject
from build.jack import JackProject
libmpdclient = MesonProject(
'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.19.tar.xz',
@@ -149,8 +150,8 @@ gme = CmakeProject(
)
ffmpeg = FfmpegProject(
'http://ffmpeg.org/releases/ffmpeg-4.3.1.tar.xz',
'ad009240d46e307b4e03a213a0f49c11b650e445b1f8be0dda2a9212b34d2ffb',
'http://ffmpeg.org/releases/ffmpeg-4.4.tar.xz',
'06b10a183ce5371f915c6bb15b7b1fffbe046e8275099c96affc29e17645d909',
'lib/libavcodec.a',
[
'--disable-shared', '--enable-static',
@@ -378,14 +379,14 @@ ffmpeg = FfmpegProject(
)
openssl = OpenSSLProject(
'https://www.openssl.org/source/openssl-3.0.0-alpha10.tar.gz',
'b1699acf2148db31f12edf5ebfdf12a92bfd3f0e60538d169710408a3cd3b138',
'https://www.openssl.org/source/openssl-3.0.0-beta2.tar.gz',
'e76ab22879201b12f014393ee4becec7f264d8f6955b1036839128002868df71',
'include/openssl/ossl_typ.h',
)
curl = AutotoolsProject(
'http://curl.haxx.se/download/curl-7.74.0.tar.xz',
'999d5f2c403cf6e25d58319fdd596611e455dd195208746bc6e6d197a77e878b',
'https://curl.se/download/curl-7.78.0.tar.xz',
'be42766d5664a739c3974ee3dfbbcbe978a4ccb1fe628bb1d9b59ac79e445fb5',
'lib/libcurl.a',
[
'--disable-shared', '--enable-static',
@@ -443,8 +444,14 @@ libnfs = AutotoolsProject(
autoreconf=True,
)
jack = JackProject(
'https://github.com/jackaudio/jack2/archive/v1.9.17.tar.gz',
'38f674bbc57852a8eb3d9faa1f96a0912d26f7d5df14c11005ad499c8ae352f2',
'lib/pkgconfig/jack.pc',
)
boost = BoostProject(
'https://dl.bintray.com/boostorg/release/1.75.0/source/boost_1_75_0.tar.bz2',
'953db31e016db7bb207f11432bef7df100516eeb746843fa0486a222e3fd49cb',
'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2',
'f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41',
'include/boost/version.hpp',
)

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

@@ -113,7 +113,7 @@ static void version()
printf("Music Player Daemon " VERSION " (%s)"
"\n"
"Copyright 2003-2007 Warren Dukes <warren.dukes@gmail.com>\n"
"Copyright 2008-2018 Max Kellermann <max.kellermann@gmail.com>\n"
"Copyright 2008-2021 Max Kellermann <max.kellermann@gmail.com>\n"
"This is free software; see the source for copying conditions. There is NO\n"
"warranty; not even MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n",
GIT_VERSION);

View File

@@ -477,6 +477,7 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
#endif
ZeroconfInit(raw_config, instance.event_loop);
AtScopeExit() { ZeroconfDeinit(); };
#ifdef ENABLE_DATABASE
if (create_db) {
@@ -537,9 +538,6 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
instance.state_file->Write();
instance.BeginShutdownUpdate();
ZeroconfDeinit();
instance.BeginShutdownPartitions();
}

View File

@@ -42,9 +42,9 @@ RemoteTagCache::Lookup(const std::string &uri) noexcept
std::unique_lock<Mutex> lock(mutex);
KeyMap::insert_commit_data hint;
auto result = map.insert_check(uri, Item::Hash(), Item::Equal(), hint);
if (result.second) {
auto *item = new Item(*this, uri);
auto [tag, value] = map.insert_check(uri, Item::Hash(), Item::Equal(), hint);
if (value) {
auto item = new Item(*this, uri);
map.insert_commit(*item, hint);
waiting_list.push_back(*item);
lock.unlock();
@@ -70,15 +70,13 @@ RemoteTagCache::Lookup(const std::string &uri) noexcept
ItemResolved(*item);
return;
}
} else if (result.first->scanner) {
} else if (tag->scanner) {
/* already scanning this one - no-op */
} else {
/* already finished: re-invoke the handler */
auto &item = *result.first;
idle_list.erase(waiting_list.iterator_to(item));
invoke_list.push_back(item);
idle_list.erase(waiting_list.iterator_to(*tag));
invoke_list.push_back(*tag);
ScheduleInvokeHandlers();
}

View File

@@ -25,6 +25,7 @@
#include "client/Client.hxx"
#include "protocol/Ack.hxx"
#include "fs/AllocatedPath.hxx"
#include "input/InputStream.hxx"
#include "util/Compiler.h"
#include "util/UriExtract.hxx"
#include "LocateUri.hxx"
@@ -32,8 +33,13 @@
static void
TagScanStream(const char *uri, TagHandler &handler)
{
if (!tag_stream_scan(uri, handler))
Mutex mutex;
auto is = InputStream::OpenReady(uri, mutex);
if (!tag_stream_scan(*is, handler))
throw ProtocolError(ACK_ERROR_NO_EXIST, "Failed to load file");
ScanGenericTags(*is, handler);
}
static void

View File

@@ -23,6 +23,7 @@
#include "Message.hxx"
#include "command/CommandResult.hxx"
#include "command/CommandListBuilder.hxx"
#include "input/LastInputStream.hxx"
#include "tag/Mask.hxx"
#include "event/FullyBufferedSocket.hxx"
#include "event/TimerEvent.hxx"
@@ -90,6 +91,13 @@ public:
*/
size_t binary_limit = 8192;
/**
* This caches the last "albumart" InputStream instance, to
* avoid repeating the search for each chunk requested by this
* client.
*/
LastInputStream last_album_art;
private:
static constexpr size_t MAX_SUBSCRIPTIONS = 16;

View File

@@ -44,7 +44,8 @@ Client::Client(EventLoop &_loop, Partition &_partition,
partition(&_partition),
permission(_permission),
uid(_uid),
num(_num)
num(_num),
last_album_art(_loop)
{
timeout_event.Schedule(client_timeout);
}

View File

@@ -61,7 +61,12 @@ Response::WriteBinary(ConstBuffer<void> payload) noexcept
{
assert(payload.size <= client.binary_limit);
return Format("binary: %zu\n", payload.size) &&
return
#ifdef _WIN32
Format("binary: %lu\n", (unsigned long)payload.size) &&
#else
Format("binary: %zu\n", payload.size) &&
#endif
Write(payload.data, payload.size) &&
Write("\n");
}

View File

@@ -34,8 +34,7 @@ Client::Subscribe(const char *channel) noexcept
if (num_subscriptions >= MAX_SUBSCRIPTIONS)
return Client::SubscribeResult::FULL;
auto r = subscriptions.insert(channel);
if (!r.second)
if (!subscriptions.insert(channel).second)
return Client::SubscribeResult::ALREADY;
++num_subscriptions;

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,13 +191,19 @@ 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());
Mutex mutex;
InputStreamPtr is = find_stream_art(art_directory, mutex);
/* to avoid repeating the search for each chunk request by the
same client, use the #LastInputStream class to cache the
#InputStream instance */
auto *is = client.last_album_art.Open(art_directory, [](std::string_view directory,
Mutex &mutex){
return find_stream_art(directory, mutex);
});
if (is == nullptr) {
r.Error(ACK_ERROR_NO_EXIST, "No file exists");
@@ -219,18 +229,58 @@ read_stream_art(Response &r, const char *uri, size_t offset)
std::size_t read_size = 0;
if (buffer_size > 0) {
std::unique_lock<Mutex> lock(mutex);
std::unique_lock<Mutex> lock(is->mutex);
is->Seek(lock, offset);
read_size = is->Read(lock, buffer.get(), buffer_size);
}
#ifdef _WIN32
r.Format("size: %lu\n", (unsigned long)art_file_size);
#else
r.Format("size: %" PRIoffset "\n", art_file_size);
#endif
r.WriteBinary({buffer.get(), read_size});
return CommandResult::OK;
}
#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)
{
@@ -240,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
@@ -261,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);
@@ -306,7 +365,11 @@ public:
return;
}
#ifdef _WIN32
response.Format("size: %lu\n", (unsigned long)buffer.size);
#else
response.Format("size: %zu\n", buffer.size);
#endif
if (mime_type != nullptr)
response.Format("type: %s\n", mime_type);

View File

@@ -92,7 +92,7 @@ handle_load(Client &client, Request args, [[maybe_unused]] Response &r)
auto &instance = client.GetInstance();
const unsigned new_size = playlist.GetLength();
for (unsigned i = old_size; i < new_size; ++i)
instance.LookupRemoteTag(playlist.queue.Get(i).GetURI());
instance.LookupRemoteTag(playlist.queue.Get(i).GetRealURI());
return CommandResult::OK;
}

View File

@@ -57,9 +57,9 @@ Print(Response &r, TagType group, const TagCountMap &m) noexcept
{
assert(unsigned(group) < TAG_NUM_OF_ITEM_TYPES);
for (const auto &i : m) {
tag_print(r, group, i.first.c_str());
PrintSearchStats(r, i.second);
for (const auto &[tag, stats] : m) {
tag_print(r, group, tag.c_str());
PrintSearchStats(r, stats);
}
}
@@ -68,8 +68,7 @@ stats_visitor_song(SearchStats &stats, const LightSong &song) noexcept
{
stats.n_songs++;
const auto duration = song.GetDuration();
if (!duration.IsNegative())
if (const auto duration = song.GetDuration(); !duration.IsNegative())
stats.total_duration += duration;
}
@@ -77,8 +76,7 @@ static void
CollectGroupCounts(TagCountMap &map, const Tag &tag,
const char *value) noexcept
{
auto r = map.insert(std::make_pair(value, SearchStats()));
SearchStats &s = r.first->second;
auto &s = map.insert(std::make_pair(value, SearchStats())).first->second;
++s.n_songs;
if (!tag.duration.IsNegative())
s.total_duration += tag.duration;

View File

@@ -195,11 +195,11 @@ PrintUniqueTags(Response &r, ConstBuffer<TagType> tag_types,
const char *const name = tag_item_names[tag_types.front()];
tag_types.pop_front();
for (const auto &i : map) {
r.Format("%s: %s\n", name, i.first.c_str());
for (const auto &[key, tag] : map) {
r.Format("%s: %s\n", name, key.c_str());
if (!tag_types.empty())
PrintUniqueTags(r, tag_types, i.second);
PrintUniqueTags(r, tag_types, tag);
}
}

View File

@@ -424,6 +424,7 @@ SendGroup(mpd_connection *connection, TagType group)
return mpd_search_add_group_tag(connection, tag);
#else
(void)connection;
(void)group;
throw std::runtime_error("Grouping requires libmpdclient 2.12");
#endif

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
{
@@ -138,13 +155,10 @@ Directory::LookupDirectory(std::string_view _uri) noexcept
Directory *d = this;
do {
auto s = uri.Split(PathTraitsUTF8::SEPARATOR);
if (s.first.empty())
auto [name, rest] = uri.Split(PathTraitsUTF8::SEPARATOR);
if (name.empty())
break;
const auto name = s.first;
const auto rest = s.second;
Directory *tmp = d->FindChild(name);
if (tmp == nullptr)
/* not found */

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
@@ -312,6 +334,29 @@ UpdateWalk::SkipSymlink(const Directory *directory,
#endif
}
static void
LoadExcludeListOrThrow(const Storage &storage, const Directory &directory,
ExcludeList &exclude_list)
{
Mutex mutex;
auto is = InputStream::OpenReady(storage.MapUTF8(PathTraitsUTF8::Build(directory.GetPath(),
".mpdignore")).c_str(),
mutex);
exclude_list.Load(std::move(is));
}
static void
LoadExcludeListOrLog(const Storage &storage, const Directory &directory,
ExcludeList &exclude_list) noexcept
{
try {
LoadExcludeListOrThrow(storage, directory, exclude_list);
} catch (...) {
if (!IsFileNotFound(std::current_exception()))
LogError(std::current_exception());
}
}
bool
UpdateWalk::UpdateDirectory(Directory &directory,
const ExcludeList &exclude_list,
@@ -331,17 +376,7 @@ UpdateWalk::UpdateDirectory(Directory &directory,
}
ExcludeList child_exclude_list(exclude_list);
try {
Mutex mutex;
auto is = InputStream::OpenReady(storage.MapUTF8(PathTraitsUTF8::Build(directory.GetPath(),
".mpdignore")).c_str(),
mutex);
child_exclude_list.Load(std::move(is));
} catch (...) {
if (!IsFileNotFound(std::current_exception()))
LogError(std::current_exception());
}
LoadExcludeListOrLog(storage, directory, child_exclude_list);
if (!child_exclude_list.IsEmpty())
RemoveExcludedFromDirectory(directory, child_exclude_list);
@@ -427,26 +462,46 @@ UpdateWalk::DirectoryMakeUriParentChecked(Directory &root,
StringView uri(_uri);
while (true) {
auto s = uri.Split('/');
const std::string_view name = s.first;
const auto rest = s.second;
auto [name, rest] = uri.Split('/');
if (rest == nullptr)
break;
if (!name.empty()) {
directory = DirectoryMakeChildChecked(*directory,
std::string(name).c_str(),
s.first);
name);
if (directory == nullptr)
break;
}
uri = s.second;
uri = rest;
}
return directory;
}
static void
LoadExcludeLists(std::forward_list<ExcludeList> &lists,
const Storage &storage, const Directory &directory) noexcept
{
assert(!lists.empty());
if (!directory.IsRoot())
LoadExcludeLists(lists, storage, *directory.parent);
lists.emplace_front();
LoadExcludeListOrLog(storage, directory, lists.front());
}
static auto
LoadExcludeLists(const Storage &storage, const Directory &directory) noexcept
{
std::forward_list<ExcludeList> lists;
lists.emplace_front();
LoadExcludeLists(lists, storage, directory);
return lists;
}
inline void
UpdateWalk::UpdateUri(Directory &root, const char *uri) noexcept
try {
@@ -467,9 +522,8 @@ try {
return;
}
ExcludeList exclude_list;
UpdateDirectoryChild(*parent, exclude_list, name, info);
const auto exclude_lists = LoadExcludeLists(storage, *parent);
UpdateDirectoryChild(*parent, exclude_lists.front(), name, info);
} catch (...) {
LogError(std::current_exception());
}
@@ -498,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

@@ -464,6 +464,17 @@ FfmpegCheckTag(DecoderClient &client, InputStream *is,
client.SubmitTag(is, tag.Commit());
}
static bool
IsSeekable(const AVFormatContext &format_context) noexcept
{
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 6, 100)
return (format_context.ctx_flags & AVFMTCTX_UNSEEKABLE) != 0;
#else
(void)format_context;
return false;
#endif
}
static void
FfmpegDecode(DecoderClient &client, InputStream *input,
AVFormatContext &format_context)
@@ -521,7 +532,7 @@ FfmpegDecode(DecoderClient &client, InputStream *input,
client.Ready(audio_format,
input
? input->IsSeekable()
: (format_context.ctx_flags & AVFMTCTX_UNSEEKABLE) != 0,
: IsSeekable(format_context),
total_time);
FfmpegParseMetaData(client, format_context, audio_stream);
@@ -648,6 +659,8 @@ ffmpeg_scan_stream(InputStream &is, TagHandler &handler)
return FfmpegScanStream(*f, handler);
}
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
static void
ffmpeg_uri_decode(DecoderClient &client, const char *uri)
{
@@ -679,6 +692,8 @@ ffmpeg_protocols() noexcept
return protocols;
}
#endif
/**
* A list of extensions found for the formats supported by ffmpeg.
* This list is current as of 02-23-09; To find out if there are more
@@ -802,6 +817,8 @@ static const char *const ffmpeg_mime_types[] = {
constexpr DecoderPlugin ffmpeg_decoder_plugin =
DecoderPlugin("ffmpeg", ffmpeg_decode, ffmpeg_scan_stream)
.WithInit(ffmpeg_init, ffmpeg_finish)
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
.WithProtocols(ffmpeg_protocols, ffmpeg_uri_decode)
#endif
.WithSuffixes(ffmpeg_suffixes)
.WithMimeTypes(ffmpeg_mime_types);

View File

@@ -30,11 +30,22 @@ extern "C" {
#include <libavutil/dict.h>
}
/**
* FFmpeg specific tag name mappings, as supported by
* libavformat/id3v2.c, libavformat/mov.c and others.
*/
static constexpr struct tag_table ffmpeg_tags[] = {
{ "year", TAG_DATE },
{ "author-sort", TAG_ARTIST_SORT },
/* from libavformat/id3v2.c, libavformat/mov.c */
{ "album_artist", TAG_ALBUM_ARTIST },
{ "album_artist-sort", TAG_ALBUM_ARTIST_SORT },
/* from libavformat/id3v2.c */
{ "album-sort", TAG_ALBUM_SORT },
{ "artist-sort", TAG_ARTIST_SORT },
/* from libavformat/mov.c */
{ "sort_album_artist", TAG_ALBUM_ARTIST_SORT },
{ "sort_album", TAG_ALBUM_SORT },
{ "sort_artist", TAG_ARTIST_SORT },
/* sentinel */
{ nullptr, TAG_NUM_OF_ITEM_TYPES }

View File

@@ -344,7 +344,7 @@ gme_container_scan(Path path_fs)
static const char *const gme_suffixes[] = {
"ay", "gbs", "gym", "hes", "kss", "nsf",
"nsfe", "sap", "spc", "vgm", "vgz",
"nsfe", "rsn", "sap", "spc", "vgm", "vgz",
nullptr
};

View File

@@ -889,8 +889,6 @@ inline bool
MadDecoder::HandleCurrentFrame() noexcept
{
switch (mute_frame) {
DecoderCommand cmd;
case MadDecoderMuteFrame::SKIP:
mute_frame = MadDecoderMuteFrame::NONE;
break;
@@ -899,8 +897,8 @@ MadDecoder::HandleCurrentFrame() noexcept
mute_frame = MadDecoderMuteFrame::NONE;
UpdateTimerNextFrame();
break;
case MadDecoderMuteFrame::NONE:
cmd = SynthAndSubmit();
case MadDecoderMuteFrame::NONE: {
const auto cmd = SynthAndSubmit();
UpdateTimerNextFrame();
if (cmd == DecoderCommand::SEEK) {
assert(input_stream.IsSeekable());
@@ -922,6 +920,7 @@ MadDecoder::HandleCurrentFrame() noexcept
} else if (cmd != DecoderCommand::NONE)
return false;
}
}
return true;
}

View File

@@ -456,7 +456,7 @@ sidplay_file_decode(DecoderClient &client, Path path_fs)
} while (cmd != DecoderCommand::STOP);
}
static AllocatedString<char>
static AllocatedString
Windows1252ToUTF8(const char *s) noexcept
{
#ifdef HAVE_ICU_CONVERTER
@@ -469,9 +469,9 @@ Windows1252ToUTF8(const char *s) noexcept
* Fallback to not transcoding windows-1252 to utf-8, that may result
* in invalid utf-8 unless nonprintable characters are replaced.
*/
auto t = AllocatedString<char>::Duplicate(s);
AllocatedString t(s);
for (size_t i = 0; t[i] != AllocatedString<char>::SENTINEL; i++)
for (size_t i = 0; t[i] != AllocatedString::SENTINEL; i++)
if (!IsPrintableASCII(t[i]))
t[i] = '?';
@@ -479,7 +479,7 @@ Windows1252ToUTF8(const char *s) noexcept
}
gcc_pure
static AllocatedString<char>
static AllocatedString
GetInfoString(const SidTuneInfo &info, unsigned i) noexcept
{
#ifdef HAVE_SIDPLAYFP
@@ -496,7 +496,7 @@ GetInfoString(const SidTuneInfo &info, unsigned i) noexcept
}
gcc_pure
static AllocatedString<char>
static AllocatedString
GetDateString(const SidTuneInfo &info) noexcept
{
/*
@@ -507,12 +507,12 @@ GetDateString(const SidTuneInfo &info) noexcept
* author or group> may be for example Rob Hubbard. A full field
* may be for example "1987 Rob Hubbard".
*/
AllocatedString<char> release = GetInfoString(info, 2);
AllocatedString release = GetInfoString(info, 2);
/* Keep the <year> part only for the date. */
for (size_t i = 0; release[i] != AllocatedString<char>::SENTINEL; i++)
for (size_t i = 0; release[i] != AllocatedString::SENTINEL; i++)
if (std::isspace(release[i])) {
release[i] = AllocatedString<char>::SENTINEL;
release[i] = AllocatedString::SENTINEL;
break;
}

View File

@@ -24,7 +24,7 @@
#ifdef _WIN32
#include <windows.h>
#include <fileapi.h>
#include <tchar.h>
/**

36
src/fs/Glob.cxx Normal file
View File

@@ -0,0 +1,36 @@
/*
* 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.
*/
#ifdef _WIN32
// COM needs the "MSG" typedef, and shlwapi.h includes COM headers
#undef NOUSER
#endif
#include "Glob.hxx"
#ifdef _WIN32
#include <shlwapi.h>
bool
Glob::Check(const char *name_fs) const noexcept
{
return PathMatchSpecA(name_fs, pattern.c_str());
}
#endif

View File

@@ -24,45 +24,44 @@
#ifdef HAVE_FNMATCH
#define HAVE_CLASS_GLOB
#include <string>
#include <fnmatch.h>
#elif defined(_WIN32)
#define HAVE_CLASS_GLOB
#include <string>
#include <shlwapi.h>
#endif
#ifdef HAVE_CLASS_GLOB
#include "util/Compiler.h"
#include <string>
/**
* A pattern that matches file names. It may contain shell wildcards
* (asterisk and question mark).
*/
class Glob {
#if defined(HAVE_FNMATCH) || defined(_WIN32)
std::string pattern;
#endif
public:
#if defined(HAVE_FNMATCH) || defined(_WIN32)
explicit Glob(const char *_pattern)
:pattern(_pattern) {}
Glob(Glob &&other)
:pattern(std::move(other.pattern)) {}
#endif
Glob(Glob &&other) noexcept = default;
Glob &operator=(Glob &&other) noexcept = default;
gcc_pure
bool Check(const char *name_fs) const noexcept {
#ifdef HAVE_FNMATCH
return fnmatch(pattern.c_str(), name_fs, 0) == 0;
#elif defined(_WIN32)
return PathMatchSpecA(name_fs, pattern.c_str());
#endif
}
bool Check(const char *name_fs) const noexcept;
};
#endif
#ifdef HAVE_FNMATCH
inline bool
Glob::Check(const char *name_fs) const noexcept
{
return fnmatch(pattern.c_str(), name_fs, 0) == 0;
}
#endif
#endif /* HAVE_CLASS_GLOB */
#endif

View File

@@ -29,7 +29,7 @@
NarrowPath::NarrowPath(Path _path) noexcept
:value(WideCharToMultiByte(CP_ACP, _path.c_str()))
{
if (value.IsNull())
if (value == nullptr)
/* fall back to empty string */
value = Value::Empty();
}

View File

@@ -36,7 +36,7 @@
*/
class NarrowPath {
#ifdef _UNICODE
using Value = AllocatedString<>;
using Value = AllocatedString;
#else
using Value = StringPointer<>;
#endif

View File

@@ -17,6 +17,10 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifdef _WIN32
#undef NOUSER // COM needs the "MSG" typedef, and shlobj.h includes COM headers
#endif
#include "StandardDirectory.hxx"
#include "FileSystem.hxx"
#include "XDG.hxx"

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

@@ -42,7 +42,10 @@
#include <cstdint>
#ifdef _WIN32
#include <windows.h>
#include <fileapi.h>
#include <windef.h> // for HWND (needed by winbase.h)
#include <handleapi.h> // for INVALID_HANDLE_VALUE
#include <winbase.h> // for FILE_END
#endif
#if defined(__linux__) && !defined(ANDROID)

View File

@@ -35,7 +35,10 @@
#include "util/Compiler.h"
#ifdef _WIN32
#include <windows.h>
#include <fileapi.h>
#include <handleapi.h> // for INVALID_HANDLE_VALUE
#include <windef.h> // for HWND (needed by winbase.h)
#include <winbase.h> // for FILE_CURRENT
#else
#include "io/UniqueFileDescriptor.hxx"
#endif

View File

@@ -3,6 +3,7 @@ fs_sources = [
'Traits.cxx',
'Config.cxx',
'Charset.cxx',
'Glob.cxx',
'Path.cxx',
'Path2.cxx',
'AllocatedPath.cxx',

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

@@ -0,0 +1,46 @@
/*
* 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 "LastInputStream.hxx"
#include "InputStream.hxx"
#include <cassert>
LastInputStream::LastInputStream(EventLoop &event_loop) noexcept
:close_timer(event_loop, BIND_THIS_METHOD(OnCloseTimer))
{
}
LastInputStream::~LastInputStream() noexcept = default;
void
LastInputStream::Close() noexcept
{
uri.clear();
is.reset();
close_timer.Cancel();
}
void
LastInputStream::OnCloseTimer() noexcept
{
assert(is);
is.reset();
}

View File

@@ -0,0 +1,86 @@
/*
* 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 MPD_LAST_INPUT_STREAM_HXX
#define MPD_LAST_INPUT_STREAM_HXX
#include "Ptr.hxx"
#include "thread/Mutex.hxx"
#include "event/TimerEvent.hxx"
#include <string>
/**
* A helper class which maintains an #InputStream that is opened once
* and may be reused later for some time. It will be closed
* automatically after some time.
*
* This class is not thread-safe. All methods must be called on the
* thread which runs the #EventLoop.
*/
class LastInputStream {
std::string uri;
Mutex mutex;
InputStreamPtr is;
TimerEvent close_timer;
public:
explicit LastInputStream(EventLoop &event_loop) noexcept;
~LastInputStream() noexcept;
/**
* Open an #InputStream instance with the given opener
* function, but returns the cached instance if it matches.
*
* This object keeps owning the #InputStream; the caller shall
* not close it.
*/
template<typename U, typename O>
InputStream *Open(U &&new_uri, O &&open) {
if (new_uri == uri) {
if (is)
/* refresh the timeout */
ScheduleClose();
return is.get();
}
Close();
is = open(new_uri, mutex);
uri = std::forward<U>(new_uri);
if (is)
ScheduleClose();
return is.get();
}
void Close() noexcept;
private:
void ScheduleClose() noexcept {
close_timer.Schedule(std::chrono::seconds(20));
}
void OnCloseTimer() noexcept;
};
#endif

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

@@ -8,6 +8,7 @@ input_api = static_library(
'ThreadInputStream.cxx',
'AsyncInputStream.cxx',
'ProxyInputStream.cxx',
'LastInputStream.cxx',
include_directories: inc,
dependencies: [
boost_dep,

View File

@@ -402,8 +402,8 @@ CurlInputStream::CurlInputStream(EventLoop &event_loop, const char *_url,
{
request_headers.Append("Icy-Metadata: 1");
for (const auto &i : headers)
request_headers.Append((i.first + ":" + i.second).c_str());
for (const auto &[key, header] : headers)
request_headers.Append((key + ":" + header).c_str());
}
CurlInputStream::~CurlInputStream() noexcept
@@ -421,6 +421,10 @@ CurlInputStream::InitEasy()
request->SetOption(CURLOPT_MAXREDIRS, 5L);
request->SetOption(CURLOPT_FAILONERROR, 1L);
/* this option eliminates the probe request when
username/password are specified */
request->SetOption(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
if (proxy != nullptr)
request->SetOption(CURLOPT_PROXY, proxy);

View File

@@ -174,8 +174,8 @@ QobuzClient::MakeUrl(const char *object, const char *method,
uri += method;
QueryStringBuilder q;
for (const auto &i : query)
q(uri, i.first.c_str(), i.second.c_str());
for (const auto &[key, url] : query)
q(uri, key.c_str(), url.c_str());
q(uri, "app_id", app_id);
return uri;
@@ -195,11 +195,11 @@ QobuzClient::MakeSignedUrl(const char *object, const char *method,
QueryStringBuilder q;
std::string concatenated_query(object);
concatenated_query += method;
for (const auto &i : query) {
q(uri, i.first.c_str(), i.second.c_str());
for (const auto &[key, url] : query) {
q(uri, key.c_str(), url.c_str());
concatenated_query += i.first;
concatenated_query += i.second;
concatenated_query += key;
concatenated_query += url;
}
q(uri, "app_id", app_id);

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

@@ -172,7 +172,15 @@ FileDescriptor::CreatePipe(FileDescriptor &r, FileDescriptor &w) noexcept
#endif
}
#ifndef _WIN32
#ifdef _WIN32
void
FileDescriptor::SetBinaryMode() noexcept
{
_setmode(fd, _O_BINARY);
}
#else // !_WIN32
bool
FileDescriptor::CreatePipeNonBlock(FileDescriptor &r,

View File

@@ -148,10 +148,13 @@ public:
#ifdef _WIN32
void EnableCloseOnExec() noexcept {}
void DisableCloseOnExec() noexcept {}
void SetBinaryMode() noexcept;
#else
static bool CreatePipeNonBlock(FileDescriptor &r,
FileDescriptor &w) noexcept;
void SetBinaryMode() noexcept {}
/**
* Enable non-blocking mode on this file descriptor.
*/

View File

@@ -36,16 +36,16 @@ EncodeForm(CURL *curl,
{
std::string result;
for (const auto &i : fields) {
for (const auto &[key, field] : fields) {
if (!result.empty())
result.push_back('&');
result.append(i.first);
result.append(key);
result.push_back('=');
if (!i.second.empty()) {
CurlString value(curl_easy_escape(curl, i.second.data(),
i.second.length()));
if (!field.empty()) {
CurlString value(
curl_easy_escape(curl, field.data(), field.length()));
if (value)
result.append(value);
}

View File

@@ -1,19 +1,20 @@
diff -ur curl-7.63.0.orig/lib/url.c curl-7.63.0/lib/url.c
--- curl-7.63.0.orig/lib/url.c 2019-01-21 10:15:51.368019445 +0100
+++ curl-7.63.0/lib/url.c 2019-01-21 10:19:16.307523984 +0100
@@ -3057,6 +3057,7 @@
Index: curl-7.71.1/lib/url.c
===================================================================
--- curl-7.71.1.orig/lib/url.c
+++ curl-7.71.1/lib/url.c
@@ -2871,6 +2871,7 @@
}
conn->bits.netrc = FALSE;
+#ifndef __BIONIC__
if(data->set.use_netrc != CURL_NETRC_IGNORED &&
(!*userp || !**userp || !*passwdp || !**passwdp)) {
if(data->set.use_netrc && !data->set.str[STRING_USERNAME]) {
bool netrc_user_changed = FALSE;
@@ -3090,6 +3091,7 @@
}
bool netrc_passwd_changed = FALSE;
@@ -2895,6 +2896,7 @@
conn->bits.user_passwd = TRUE; /* enable user+password */
}
}
+#endif
/* for updated strings, we update them in the URL */
if(user_changed) {
if(*userp) {

View File

@@ -38,13 +38,13 @@
#include <string.h>
AllocatedString<>
AllocatedString
IcuCaseFold(std::string_view src) noexcept
try {
#ifdef HAVE_ICU
const auto u = UCharFromUTF8(src);
if (u.IsNull())
return AllocatedString<>::Duplicate(src);
return AllocatedString(src);
AllocatedArray<UChar> folded(u.size() * 2U);
@@ -54,7 +54,7 @@ try {
U_FOLD_CASE_DEFAULT,
&error_code);
if (folded_length == 0 || error_code != U_ZERO_ERROR)
return AllocatedString<>::Duplicate(src);
return AllocatedString(src);
folded.SetSize(folded_length);
return UCharToUTF8({folded.begin(), folded.size()});
@@ -63,7 +63,7 @@ try {
#error not implemented
#endif
} catch (...) {
return AllocatedString<>::Duplicate(src);
return AllocatedString(src);
}
#endif /* HAVE_ICU_CASE_FOLD */

View File

@@ -27,9 +27,9 @@
#include <string_view>
template<typename T> class AllocatedString;
class AllocatedString;
AllocatedString<char>
AllocatedString
IcuCaseFold(std::string_view src) noexcept;
#endif

View File

@@ -88,7 +88,7 @@ IcuCollate(std::string_view a, std::string_view b) noexcept
b.data(), b.size(), &code);
#elif defined(_WIN32)
AllocatedString<wchar_t> wa = nullptr, wb = nullptr;
BasicAllocatedString<wchar_t> wa, wb;
try {
wa = MultiByteToWideChar(CP_UTF8, a);

View File

@@ -46,7 +46,7 @@ IcuCompare::IcuCompare(std::string_view _needle) noexcept
#else
IcuCompare::IcuCompare(std::string_view _needle) noexcept
:needle(AllocatedString<>::Duplicate(_needle)) {}
:needle(_needle) {}
#endif
@@ -56,7 +56,7 @@ IcuCompare::operator==(const char *haystack) const noexcept
#ifdef HAVE_ICU_CASE_FOLD
return StringIsEqual(IcuCaseFold(haystack).c_str(), needle.c_str());
#elif defined(_WIN32)
if (needle.IsNull())
if (needle == nullptr)
/* the MultiByteToWideChar() call in the constructor
has failed, so let's always fail the comparison */
return false;
@@ -83,7 +83,7 @@ IcuCompare::IsIn(const char *haystack) const noexcept
return StringFind(IcuCaseFold(haystack).c_str(),
needle.c_str()) != nullptr;
#elif defined(_WIN32)
if (needle.IsNull())
if (needle == nullptr)
/* the MultiByteToWideChar() call in the constructor
has failed, so let's always fail the comparison */
return false;

View File

@@ -38,11 +38,11 @@ class IcuCompare {
#ifdef _WIN32
/* Windows API functions work with wchar_t strings, so let's
cache the MultiByteToWideChar() result for performance */
AllocatedString<wchar_t> needle;
#else
AllocatedString<> needle;
using AllocatedString = BasicAllocatedString<wchar_t>;
#endif
AllocatedString needle;
public:
IcuCompare():needle(nullptr) {}
@@ -50,12 +50,12 @@ public:
IcuCompare(const IcuCompare &src) noexcept
:needle(src
? src.needle.Clone()
? AllocatedString(src.needle)
: nullptr) {}
IcuCompare &operator=(const IcuCompare &src) noexcept {
needle = src
? src.needle.Clone()
? AllocatedString(src.needle)
: nullptr;
return *this;
}
@@ -65,7 +65,7 @@ public:
gcc_pure
operator bool() const noexcept {
return !needle.IsNull();
return needle != nullptr;
}
gcc_pure

View File

@@ -77,7 +77,7 @@ IcuConverter::Create(const char *charset)
#ifdef HAVE_ICU
#elif defined(HAVE_ICONV)
static AllocatedString<char>
static AllocatedString
DoConvert(iconv_t conv, std::string_view src)
{
// TODO: dynamic buffer?
@@ -95,12 +95,12 @@ DoConvert(iconv_t conv, std::string_view src)
if (in_left > 0)
throw std::runtime_error("Charset conversion failed");
return AllocatedString<>::Duplicate({buffer, sizeof(buffer) - out_left});
return AllocatedString({buffer, sizeof(buffer) - out_left});
}
#endif
AllocatedString<char>
AllocatedString
IcuConverter::ToUTF8(std::string_view s) const
{
#ifdef HAVE_ICU
@@ -128,7 +128,7 @@ IcuConverter::ToUTF8(std::string_view s) const
#endif
}
AllocatedString<char>
AllocatedString
IcuConverter::FromUTF8(std::string_view s) const
{
#ifdef HAVE_ICU
@@ -151,7 +151,7 @@ IcuConverter::FromUTF8(std::string_view s) const
throw std::runtime_error(FormatString("Failed to convert from Unicode: %s",
u_errorName(code)).c_str());
return AllocatedString<>::Duplicate({buffer, size_t(target - buffer)});
return AllocatedString({buffer, size_t(target - buffer)});
#elif defined(HAVE_ICONV)
return DoConvert(from_utf8, s);

View File

@@ -40,7 +40,7 @@
struct UConverter;
#endif
template<typename T> class AllocatedString;
class AllocatedString;
/**
* This class can convert strings with a certain character set to and
@@ -85,7 +85,7 @@ public:
* Throws std::runtime_error on error.
*/
gcc_nonnull_all
AllocatedString<char> ToUTF8(std::string_view s) const;
AllocatedString ToUTF8(std::string_view s) const;
/**
* Convert the string from UTF-8.
@@ -93,7 +93,7 @@ public:
* Throws std::runtime_error on error.
*/
gcc_nonnull_all
AllocatedString<char> FromUTF8(std::string_view s) const;
AllocatedString FromUTF8(std::string_view s) const;
};
#endif

View File

@@ -48,7 +48,7 @@ UCharFromUTF8(std::string_view src)
return dest;
}
AllocatedString<>
AllocatedString
UCharToUTF8(std::basic_string_view<UChar> src)
{
/* worst-case estimate */
@@ -65,5 +65,5 @@ UCharToUTF8(std::basic_string_view<UChar> src)
throw std::runtime_error(u_errorName(error_code));
dest[dest_length] = 0;
return AllocatedString<>::Donate(dest.release());
return AllocatedString::Donate(dest.release());
}

View File

@@ -25,7 +25,7 @@
#include <string_view>
template<typename T> class AllocatedArray;
template<typename T> class AllocatedString;
class AllocatedString;
/**
* Wrapper for u_strFromUTF8().
@@ -40,7 +40,7 @@ UCharFromUTF8(std::string_view src);
*
* Throws std::runtime_error on error.
*/
AllocatedString<char>
AllocatedString
UCharToUTF8(std::basic_string_view<UChar> src);
#endif

View File

@@ -25,7 +25,7 @@
#include <windows.h>
AllocatedString<char>
AllocatedString
WideCharToMultiByte(unsigned code_page, std::wstring_view src)
{
int length = WideCharToMultiByte(code_page, 0, src.data(), src.size(),
@@ -42,10 +42,10 @@ WideCharToMultiByte(unsigned code_page, std::wstring_view src)
throw MakeLastError("Failed to convert from Unicode");
buffer[length] = '\0';
return AllocatedString<char>::Donate(buffer.release());
return AllocatedString::Donate(buffer.release());
}
AllocatedString<wchar_t>
BasicAllocatedString<wchar_t>
MultiByteToWideChar(unsigned code_page, std::string_view src)
{
int length = MultiByteToWideChar(code_page, 0, src.data(), src.size(),
@@ -60,5 +60,5 @@ MultiByteToWideChar(unsigned code_page, std::string_view src)
throw MakeLastError("Failed to convert to Unicode");
buffer[length] = L'\0';
return AllocatedString<wchar_t>::Donate(buffer.release());
return BasicAllocatedString<wchar_t>::Donate(buffer.release());
}

View File

@@ -24,20 +24,21 @@
#include <string_view>
template<typename T> class AllocatedString;
class AllocatedString;
template<typename T> class BasicAllocatedString;
/**
* Throws std::system_error on error.
*/
gcc_pure gcc_nonnull_all
AllocatedString<char>
AllocatedString
WideCharToMultiByte(unsigned code_page, std::wstring_view src);
/**
* Throws std::system_error on error.
*/
gcc_pure gcc_nonnull_all
AllocatedString<wchar_t>
BasicAllocatedString<wchar_t>
MultiByteToWideChar(unsigned code_page, std::string_view src);
#endif

182
src/lib/jack/Dynamic.hxx Normal file
View File

@@ -0,0 +1,182 @@
/*
* 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 "system/Error.hxx"
/* sorry for this horrible piece of code - there's no elegant way to
load DLLs at runtime */
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-function-type"
#endif
using jack_set_error_function_t = std::add_pointer_t<decltype(jack_set_error_function)>;
static jack_set_error_function_t _jack_set_error_function;
using jack_set_info_function_t = std::add_pointer_t<decltype(jack_set_info_function)>;
static jack_set_info_function_t _jack_set_info_function;
using jack_client_open_t = std::add_pointer_t<decltype(jack_client_open)>;
static jack_client_open_t _jack_client_open;
using jack_client_close_t = std::add_pointer_t<decltype(jack_client_close)>;
static jack_client_close_t _jack_client_close;
using jack_connect_t = std::add_pointer_t<decltype(jack_connect)>;
static jack_connect_t _jack_connect;
using jack_activate_t = std::add_pointer_t<decltype(jack_activate)>;
static jack_activate_t _jack_activate;
using jack_deactivate_t = std::add_pointer_t<decltype(jack_deactivate)>;
static jack_deactivate_t _jack_deactivate;
using jack_get_sample_rate_t = std::add_pointer_t<decltype(jack_get_sample_rate)>;
static jack_get_sample_rate_t _jack_get_sample_rate;
using jack_set_process_callback_t = std::add_pointer_t<decltype(jack_set_process_callback)>;
static jack_set_process_callback_t _jack_set_process_callback;
using jack_on_info_shutdown_t = std::add_pointer_t<decltype(jack_on_info_shutdown)>;
static jack_on_info_shutdown_t _jack_on_info_shutdown;
using jack_free_t = std::add_pointer_t<decltype(jack_free)>;
static jack_free_t _jack_free;
using jack_get_ports_t = std::add_pointer_t<decltype(jack_get_ports)>;
static jack_get_ports_t _jack_get_ports;
using jack_port_register_t = std::add_pointer_t<decltype(jack_port_register)>;
static jack_port_register_t _jack_port_register;
using jack_port_name_t = std::add_pointer_t<decltype(jack_port_name)>;
static jack_port_name_t _jack_port_name;
using jack_port_get_buffer_t = std::add_pointer_t<decltype(jack_port_get_buffer)>;
static jack_port_get_buffer_t _jack_port_get_buffer;
using jack_ringbuffer_create_t = std::add_pointer_t<decltype(jack_ringbuffer_create)>;
static jack_ringbuffer_create_t _jack_ringbuffer_create;
using jack_ringbuffer_free_t = std::add_pointer_t<decltype(jack_ringbuffer_free)>;
static jack_ringbuffer_free_t _jack_ringbuffer_free;
using jack_ringbuffer_get_write_vector_t = std::add_pointer_t<decltype(jack_ringbuffer_get_write_vector)>;
static jack_ringbuffer_get_write_vector_t _jack_ringbuffer_get_write_vector;
using jack_ringbuffer_write_advance_t = std::add_pointer_t<decltype(jack_ringbuffer_write_advance)>;
static jack_ringbuffer_write_advance_t _jack_ringbuffer_write_advance;
using jack_ringbuffer_read_space_t = std::add_pointer_t<decltype(jack_ringbuffer_read_space)>;
static jack_ringbuffer_read_space_t _jack_ringbuffer_read_space;
using jack_ringbuffer_read_t = std::add_pointer_t<decltype(jack_ringbuffer_read)>;
static jack_ringbuffer_read_t _jack_ringbuffer_read;
using jack_ringbuffer_read_advance_t = std::add_pointer_t<decltype(jack_ringbuffer_read_advance)>;
static jack_ringbuffer_read_advance_t _jack_ringbuffer_read_advance;
using jack_ringbuffer_reset_t = std::add_pointer_t<decltype(jack_ringbuffer_reset)>;
static jack_ringbuffer_reset_t _jack_ringbuffer_reset;
template<typename T>
static void
GetFunction(HMODULE h, const char *name, T &result)
{
auto f = GetProcAddress(h, name);
if (f == nullptr)
throw FormatRuntimeError("No such libjack function: %s", name);
result = reinterpret_cast<T>(f);
}
static void
LoadJackLibrary()
{
#ifdef _WIN64
#define LIBJACK "libjack64"
#else
#define LIBJACK "libjack"
#endif
auto libjack = LoadLibraryA(LIBJACK);
if (!libjack)
throw FormatLastError("Failed to load " LIBJACK ".dll");
GetFunction(libjack, "jack_set_error_function", _jack_set_error_function);
GetFunction(libjack, "jack_set_info_function", _jack_set_info_function);
GetFunction(libjack, "jack_client_open", _jack_client_open);
GetFunction(libjack, "jack_client_close", _jack_client_close);
GetFunction(libjack, "jack_connect", _jack_connect);
GetFunction(libjack, "jack_activate", _jack_activate);
GetFunction(libjack, "jack_deactivate", _jack_deactivate);
GetFunction(libjack, "jack_free", _jack_free);
GetFunction(libjack, "jack_get_sample_rate", _jack_get_sample_rate);
GetFunction(libjack, "jack_set_process_callback", _jack_set_process_callback);
GetFunction(libjack, "jack_on_info_shutdown", _jack_on_info_shutdown);
GetFunction(libjack, "jack_get_ports", _jack_get_ports);
GetFunction(libjack, "jack_port_register", _jack_port_register);
GetFunction(libjack, "jack_port_name", _jack_port_name);
GetFunction(libjack, "jack_port_get_buffer", _jack_port_get_buffer);
GetFunction(libjack, "jack_ringbuffer_create", _jack_ringbuffer_create);
GetFunction(libjack, "jack_ringbuffer_free", _jack_ringbuffer_free);
GetFunction(libjack, "jack_ringbuffer_get_write_vector", _jack_ringbuffer_get_write_vector);
GetFunction(libjack, "jack_ringbuffer_write_advance", _jack_ringbuffer_write_advance);
GetFunction(libjack, "jack_ringbuffer_read_space", _jack_ringbuffer_read_space);
GetFunction(libjack, "jack_ringbuffer_read", _jack_ringbuffer_read);
GetFunction(libjack, "jack_ringbuffer_read_advance", _jack_ringbuffer_read_advance);
GetFunction(libjack, "jack_ringbuffer_reset", _jack_ringbuffer_reset);
}
#define jack_set_error_function _jack_set_error_function
#define jack_set_info_function _jack_set_info_function
#define jack_client_open _jack_client_open
#define jack_client_close _jack_client_close
#define jack_connect _jack_connect
#define jack_activate _jack_activate
#define jack_deactivate _jack_deactivate
#define jack_free _jack_free
#define jack_get_sample_rate _jack_get_sample_rate
#define jack_set_process_callback _jack_set_process_callback
#define jack_on_info_shutdown _jack_on_info_shutdown
#define jack_get_ports _jack_get_ports
#define jack_port_register _jack_port_register
#define jack_port_name _jack_port_name
#define jack_port_get_buffer _jack_port_get_buffer
#define jack_ringbuffer_create _jack_ringbuffer_create
#define jack_ringbuffer_free _jack_ringbuffer_free
#define jack_ringbuffer_get_write_vector _jack_ringbuffer_get_write_vector
#define jack_ringbuffer_write_advance _jack_ringbuffer_write_advance
#define jack_ringbuffer_read_space _jack_ringbuffer_read_space
#define jack_ringbuffer_read _jack_ringbuffer_read
#define jack_ringbuffer_read_advance _jack_ringbuffer_read_advance
#define jack_ringbuffer_reset _jack_ringbuffer_reset
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif

View File

@@ -1,5 +1,7 @@
if enable_database
sqlite_dep = dependency('sqlite3', version: '>= 3.7.3', required: get_option('sqlite'))
sqlite_dep = dependency('sqlite3', version: '>= 3.7.3',
fallback: ['sqlite3', 'sqlite3_dep'],
required: get_option('sqlite'))
else
sqlite_dep = dependency('', required: false)
endif
@@ -21,4 +23,7 @@ sqlite = static_library(
sqlite_dep = declare_dependency(
link_with: sqlite,
dependencies: [
sqlite_dep,
],
)

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

@@ -17,103 +17,102 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#undef NOUSER // COM needs the "MSG" typedef
#include "output/plugins/wasapi/ForMixer.hxx"
#include "output/plugins/wasapi/AudioClient.hxx"
#include "output/plugins/wasapi/Device.hxx"
#include "mixer/MixerInternal.hxx"
#include "output/plugins/WasapiOutputPlugin.hxx"
#include "win32/Com.hxx"
#include "win32/ComPtr.hxx"
#include "win32/ComWorker.hxx"
#include "win32/HResult.hxx"
#include <cmath>
#include <endpointvolume.h>
#include <optional>
#include <audioclient.h>
#include <endpointvolume.h>
#include <mmdeviceapi.h>
class WasapiMixer final : public Mixer {
WasapiOutput &output;
std::optional<COM> com;
public:
WasapiMixer(WasapiOutput &_output, MixerListener &_listener)
: Mixer(wasapi_mixer_plugin, _listener), output(_output) {}
void Open() override { com.emplace(); }
void Open() override {}
void Close() noexcept override { com.reset(); }
void Close() noexcept override {}
int GetVolume() override {
HRESULT result;
float volume_level;
auto com_worker = wasapi_output_get_com_worker(output);
if (!com_worker)
return -1;
if (wasapi_is_exclusive(output)) {
ComPtr<IAudioEndpointVolume> endpoint_volume;
result = wasapi_output_get_device(output)->Activate(
__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr,
endpoint_volume.AddressCast());
if (FAILED(result)) {
throw FormatHResultError(
result, "Unable to get device endpoint volume");
auto future = com_worker->Async([&]() -> int {
HRESULT result;
float volume_level;
if (wasapi_is_exclusive(output)) {
auto endpoint_volume =
Activate<IAudioEndpointVolume>(*wasapi_output_get_device(output));
result = endpoint_volume->GetMasterVolumeLevelScalar(
&volume_level);
if (FAILED(result)) {
throw MakeHResultError(result,
"Unable to get master "
"volume level");
}
} else {
auto session_volume =
GetService<ISimpleAudioVolume>(*wasapi_output_get_client(output));
result = session_volume->GetMasterVolume(&volume_level);
if (FAILED(result)) {
throw MakeHResultError(
result, "Unable to get master volume");
}
}
result = endpoint_volume->GetMasterVolumeLevelScalar(
&volume_level);
if (FAILED(result)) {
throw FormatHResultError(
result, "Unable to get master volume level");
}
} else {
ComPtr<ISimpleAudioVolume> session_volume;
result = wasapi_output_get_client(output)->GetService(
__uuidof(ISimpleAudioVolume),
session_volume.AddressCast<void>());
if (FAILED(result)) {
throw FormatHResultError(
result, "Unable to get client session volume");
}
result = session_volume->GetMasterVolume(&volume_level);
if (FAILED(result)) {
throw FormatHResultError(result,
"Unable to get master volume");
}
}
return std::lround(volume_level * 100.0f);
return std::lround(volume_level * 100.0f);
});
return future.get();
}
void SetVolume(unsigned volume) override {
HRESULT result;
const float volume_level = volume / 100.0f;
auto com_worker = wasapi_output_get_com_worker(output);
if (!com_worker)
throw std::runtime_error("Cannot set WASAPI volume");
if (wasapi_is_exclusive(output)) {
ComPtr<IAudioEndpointVolume> endpoint_volume;
result = wasapi_output_get_device(output)->Activate(
__uuidof(IAudioEndpointVolume), CLSCTX_ALL, nullptr,
endpoint_volume.AddressCast());
if (FAILED(result)) {
throw FormatHResultError(
result, "Unable to get device endpoint volume");
}
com_worker->Async([&]() {
HRESULT result;
const float volume_level = volume / 100.0f;
result = endpoint_volume->SetMasterVolumeLevelScalar(volume_level,
nullptr);
if (FAILED(result)) {
throw FormatHResultError(
result, "Unable to set master volume level");
}
} else {
ComPtr<ISimpleAudioVolume> session_volume;
result = wasapi_output_get_client(output)->GetService(
__uuidof(ISimpleAudioVolume),
session_volume.AddressCast<void>());
if (FAILED(result)) {
throw FormatHResultError(
result, "Unable to get client session volume");
}
if (wasapi_is_exclusive(output)) {
auto endpoint_volume =
Activate<IAudioEndpointVolume>(*wasapi_output_get_device(output));
result = session_volume->SetMasterVolume(volume_level, nullptr);
if (FAILED(result)) {
throw FormatHResultError(result,
"Unable to set master volume");
result = endpoint_volume->SetMasterVolumeLevelScalar(
volume_level, nullptr);
if (FAILED(result)) {
throw MakeHResultError(
result,
"Unable to set master volume level");
}
} else {
auto session_volume =
GetService<ISimpleAudioVolume>(*wasapi_output_get_client(output));
result = session_volume->SetMasterVolume(volume_level,
nullptr);
if (FAILED(result)) {
throw MakeHResultError(
result, "Unable to set master volume");
}
}
}
}).get();
}
};

View File

@@ -182,8 +182,8 @@ UdisksNeighborExplorer::GetList() const noexcept
NeighborExplorer::List result;
for (const auto &i : by_uri)
result.emplace_front(i.second);
for (const auto &[t, r] : by_uri)
result.emplace_front(r);
return result;
}

View File

@@ -118,7 +118,7 @@ AudioOutputControl::GetLogName() const noexcept
{
assert(!IsDummy());
return output->GetLogName();
return output ? output->GetLogName() : name.c_str();
}
Mixer *
@@ -364,7 +364,7 @@ AudioOutputControl::LockPlay() noexcept
void
AudioOutputControl::LockPauseAsync() noexcept
{
if (output->mixer != nullptr && !output->SupportsPause())
if (output && output->mixer != nullptr && !output->SupportsPause())
/* the device has no pause mode: close the mixer,
unless its "global" flag is set (checked by
mixer_auto_close()) */

View File

@@ -40,8 +40,7 @@ printAudioDevices(Response &r, const MultipleOutputs &outputs)
ao.GetName(), ao.GetPluginName(),
ao.IsEnabled());
for (const auto &a : ao.GetAttributes())
r.Format("attribute: %s=%s\n",
a.first.c_str(), a.second.c_str());
for (const auto &[attribute, value] : ao.GetAttributes())
r.Format("attribute: %s=%s\n", attribute.c_str(), value.c_str());
}
}

View File

@@ -42,7 +42,7 @@
#include "plugins/WinmmOutputPlugin.hxx"
#endif
#ifdef ENABLE_WASAPI_OUTPUT
#include "plugins/WasapiOutputPlugin.hxx"
#include "plugins/wasapi/WasapiOutputPlugin.hxx"
#endif
#include "util/StringAPI.hxx"

View File

@@ -44,6 +44,10 @@ static constexpr unsigned MAX_PORTS = 16;
static constexpr size_t jack_sample_size = sizeof(jack_default_audio_sample_t);
#ifdef DYNAMIC_JACK
#include "lib/jack/Dynamic.hxx"
#endif // _WIN32
class JackOutput final : public AudioOutput {
/**
* libjack options passed to jack_client_open().
@@ -463,6 +467,10 @@ JackOutput::Disable() noexcept
static AudioOutput *
mpd_jack_init(EventLoop &, const ConfigBlock &block)
{
#ifdef DYNAMIC_JACK
LoadJackLibrary();
#endif
jack_set_error_function(mpd_jack_error);
#ifdef HAVE_JACK_SET_INFO_FUNCTION

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

Some files were not shown because too many files have changed in this diff Show More