Compare commits

...

90 Commits

Author SHA1 Message Date
Max Kellermann
94c196108d release v0.22.11 2021-08-24 22:15:22 +02:00
Max Kellermann
263d1ba002 Main: playlist_directory defaults to "/sdcard/Android/data/org.musicpd/files/playlists"
Closes https://github.com/MusicPlayerDaemon/MPD/issues/1233
2021-08-24 22:12:27 +02:00
Max Kellermann
2dba06dc34 android/Context: add GetExternalFilesDir() 2021-08-24 22:03:53 +02:00
Max Kellermann
811860c3b4 android/Context: use [[gnu::pure]] 2021-08-24 21:54:22 +02:00
Max Kellermann
8439119e24 filter/ffmpeg: support double-precision samples
Insert an "aformat" filter which converts double-precision to
single-precision.

Closes https://github.com/MusicPlayerDaemon/MPD/issues/1235
2021-08-24 13:45:57 +02:00
Max Kellermann
b5b40d8235 filter/ffmpeg: automatically retry with "aformat"
If DetectFilterOutputFormat() fails to determine the output format,
insert an "aformat" filter which attempts to force a specific output
format.

Fixes part 2 of of https://github.com/MusicPlayerDaemon/MPD/issues/1235
2021-08-24 13:31:13 +02:00
Max Kellermann
b904f8af03 lib/ffmpeg/Filter: add FilterContext::MakeAformat() 2021-08-24 13:30:17 +02:00
Max Kellermann
ebfbb74f9e lib/ffmpeg/DetectFilterFormat: return AudioFormat::Undefined() on EAGAIN 2021-08-24 13:30:03 +02:00
Max Kellermann
7b4225aa1f lib/ffmpeg/Filter: add ParseSingleInOut()
Merge some duplicate code.
2021-08-24 13:29:08 +02:00
Max Kellermann
71a5311b06 lib/ffmpeg/Filter: eliminate class FilterContext
Since AVFilterContext are freed automatically, this wrapper class
serves no purpose.  Let's remove it.
2021-08-24 13:04:34 +02:00
Max Kellermann
a62a35e1db lib/ffmpeg/Filter: remove FilterContext destructor
Fixes potential double-free bugs which currently did not occur because
the destructors happened to be called in the right order.
2021-08-24 12:56:05 +02:00
Max Kellermann
ca2439f595 filter/ffmpeg: pass "channel_layout" instead of "channels" to buffersrc
Fixes part 1 of https://github.com/MusicPlayerDaemon/MPD/issues/1235
2021-08-23 21:38:13 +02:00
Max Kellermann
f9a0db716a android: build with NDK r23 2021-08-23 20:58:19 +02:00
Samir Benmendil
cfe024ea13 command/file: return directory_uri if real_uri is unset
Prevent a segfault when accessing album art.

Fix  
2021-08-17 10:55:43 +02:00
Max Kellermann
993d85125e increment version number to 0.22.11 2021-08-17 10:55:10 +02:00
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 
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: 
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
73 changed files with 876 additions and 1929 deletions

42
NEWS

@@ -1,3 +1,45 @@
ver 0.22.11 (2021/08/24)
* protocol
- fix "albumart" crash
* filter
- ffmpeg: pass "channel_layout" instead of "channels" to buffersrc
- ffmpeg: fix "av_buffersink_get_frame() failed: Resource temporarily unavailable"
- ffmpeg: support double-precision samples (by converting to single precision)
* Android
- build with NDK r23
- playlist_directory defaults to "/sdcard/Android/data/org.musicpd/files/playlists"
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)

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

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

@@ -24,15 +24,13 @@ android_abis = {
'armeabi-v7a': {
'arch': 'arm-linux-androideabi',
'ndk_arch': 'arm',
'toolchain_arch': 'arm-linux-androideabi',
'llvm_triple': 'armv7-linux-androideabi',
'cflags': '-fpic -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=softfp',
'cflags': '-fpic -mfpu=neon -mfloat-abi=softfp',
},
'arm64-v8a': {
'arch': 'aarch64-linux-android',
'ndk_arch': 'arm64',
'toolchain_arch': 'aarch64-linux-android',
'llvm_triple': 'aarch64-linux-android',
'cflags': '-fpic',
},
@@ -40,7 +38,6 @@ android_abis = {
'x86': {
'arch': 'i686-linux-android',
'ndk_arch': 'x86',
'toolchain_arch': 'x86',
'llvm_triple': 'i686-linux-android',
'cflags': '-fPIC -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32',
},
@@ -48,7 +45,6 @@ android_abis = {
'x86_64': {
'arch': 'x86_64-linux-android',
'ndk_arch': 'x86_64',
'toolchain_arch': 'x86_64',
'llvm_triple': 'x86_64-linux-android',
'cflags': '-fPIC -m64',
},
@@ -84,36 +80,28 @@ class AndroidNdkToolchain:
ndk_arch = abi_info['ndk_arch']
android_api_level = '21'
# select the NDK compiler
gcc_version = '4.9'
install_prefix = os.path.join(arch_path, 'root')
self.arch = arch
self.install_prefix = install_prefix
toolchain_path = os.path.join(ndk_path, 'toolchains', abi_info['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
common_flags = '-Os -g'
common_flags += ' ' + abi_info['cflags']
toolchain_bin = os.path.join(toolchain_path, 'bin')
llvm_bin = os.path.join(llvm_path, 'bin')
self.cc = os.path.join(llvm_bin, 'clang')
self.cxx = os.path.join(llvm_bin, 'clang++')
common_flags += ' -target ' + llvm_triple + ' -gcc-toolchain ' + toolchain_path
common_flags += ' -target ' + llvm_triple
common_flags += ' -fvisibility=hidden -fdata-sections -ffunction-sections'
# required flags from https://android.googlesource.com/platform/ndk/+/ndk-release-r20/docs/BuildSystemMaintainers.md#additional-required-arguments
common_flags += ' -fno-addrsig'
self.ar = os.path.join(toolchain_bin, arch + '-ar')
self.ranlib = os.path.join(toolchain_bin, arch + '-ranlib')
self.nm = os.path.join(toolchain_bin, arch + '-nm')
self.strip = os.path.join(toolchain_bin, arch + '-strip')
self.ar = os.path.join(llvm_bin, 'llvm-ar')
self.ranlib = os.path.join(llvm_bin, 'llvm-ranlib')
self.nm = os.path.join(llvm_bin, 'llvm-nm')
self.strip = os.path.join(llvm_bin, 'llvm-strip')
self.cflags = common_flags
self.cxxflags = common_flags

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

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

@@ -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:
@@ -1200,7 +1169,7 @@ This plugin requires building with ``libavfilter`` (FFmpeg).
* - **graph "..."**
- Specifies the ``libavfilter`` graph; read the `FFmpeg
documentation
<https://libav.org/documentation/libavfilter.html#Filtergraph-syntax-1>`_
<https://ffmpeg.org/ffmpeg-filters.html#Filtergraph-syntax-1>`_
for details

@@ -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
@@ -176,7 +176,7 @@ Compiling for Android
You need:
* Android SDK
* `Android NDK r22 <https://developer.android.com/ndk/downloads>`_
* `Android NDK r23 <https://developer.android.com/ndk/downloads>`_
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
<https://ninja-build.org/>`__
* cmake
@@ -628,7 +628,8 @@ By default, all clients are unauthenticated and have a full set of permissions.
* - **control**
- Allows all other player and playlist manipulations.
* - **admin**
- Allows database updates and allows shutting down :program:`MPD`.
- Allows manipulating outputs, stickers and partitions,
mounting/unmounting storage and shutting down :program:`MPD`.
:code:`local_permissions` may be used to assign other permissions to clients connecting on a local socket.
@@ -688,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
^^^^^^^^^^^^^^^^^^^^
@@ -1120,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
^^^^^^^^^^^^^^^

@@ -1,7 +1,7 @@
project(
'mpd',
['c', 'cpp'],
version: '0.22.8',
version: '0.22.11',
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') != ''

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

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

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

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

@@ -157,7 +157,17 @@ glue_daemonize_init(const struct options *options,
static void
glue_mapper_init(const ConfigData &config)
{
mapper_init(config.GetPath(ConfigOption::PLAYLIST_DIR));
auto playlist_directory = config.GetPath(ConfigOption::PLAYLIST_DIR);
#ifdef ANDROID
/* if there is no explicit configuration, store playlists in
"/sdcard/Android/data/org.musicpd/files/playlists" */
if (playlist_directory.IsNull())
playlist_directory = context->GetExternalFilesDir(Java::GetEnv(),
"playlists");
#endif
mapper_init(std::move(playlist_directory));
}
#ifdef ENABLE_DATABASE
@@ -477,6 +487,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 +548,6 @@ MainConfigured(const struct options &options, const ConfigData &raw_config)
instance.state_file->Write();
instance.BeginShutdownUpdate();
ZeroconfDeinit();
instance.BeginShutdownPartitions();
}

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

@@ -26,6 +26,25 @@
#include "AudioManager.hxx"
AllocatedPath
Context::GetExternalFilesDir(JNIEnv *env, const char *_type) noexcept
{
assert(_type != nullptr);
Java::Class cls{env, env->GetObjectClass(Get())};
jmethodID method = env->GetMethodID(cls, "getExternalFilesDir",
"(Ljava/lang/String;)Ljava/io/File;");
assert(method);
Java::String type{env, _type};
jobject file = env->CallObjectMethod(Get(), method, type.Get());
if (Java::DiscardException(env) || file == nullptr)
return nullptr;
return Java::File::ToAbsolutePath(env, file);
}
AllocatedPath
Context::GetCacheDir(JNIEnv *env) const noexcept
{

@@ -30,10 +30,14 @@ public:
Context(JNIEnv *env, jobject obj) noexcept
:Java::GlobalObject(env, obj) {}
gcc_pure
[[gnu::pure]]
AllocatedPath GetExternalFilesDir(JNIEnv *env,
const char *type) noexcept;
[[gnu::pure]]
AllocatedPath GetCacheDir(JNIEnv *env) const noexcept;
gcc_pure
[[gnu::pure]]
AudioManager *GetAudioManager(JNIEnv *env) noexcept;
};

@@ -33,10 +33,10 @@ namespace Environment {
/**
* Determine the mount point of the external SD card.
*/
gcc_pure
[[gnu::pure]]
AllocatedPath getExternalStorageDirectory() noexcept;
gcc_pure
[[gnu::pure]]
AllocatedPath getExternalStoragePublicDirectory(const char *type) noexcept;
}

@@ -27,11 +27,15 @@
#include "client/Response.hxx"
#include "util/CharUtil.hxx"
#include "util/OffsetPointer.hxx"
#include "util/ScopeExit.hxx"
#include "util/StringCompare.hxx"
#include "util/StringView.hxx"
#include "util/UriExtract.hxx"
#include "tag/Handler.hxx"
#include "tag/Generic.hxx"
#include "TagAny.hxx"
#include "db/Interface.hxx"
#include "song/LightSong.hxx"
#include "storage/StorageInterface.hxx"
#include "fs/AllocatedPath.hxx"
#include "fs/FileInfo.hxx"
@@ -187,10 +191,9 @@ find_stream_art(std::string_view directory, Mutex &mutex)
}
static CommandResult
read_stream_art(Response &r, const char *uri, size_t offset)
read_stream_art(Response &r, const std::string_view art_directory,
size_t offset)
{
const auto art_directory = PathTraitsUTF8::GetParent(uri);
// TODO: eliminate this const_cast
auto &client = const_cast<Client &>(r.GetClient());
@@ -243,6 +246,44 @@ read_stream_art(Response &r, const char *uri, size_t offset)
}
#ifdef ENABLE_DATABASE
/**
* Attempt to locate the "real" directory where the given song is
* stored. This attempts to resolve "virtual" directories/songs,
* e.g. expanded CUE sheet contents.
*/
[[gnu::pure]]
static std::string_view
RealDirectoryOfSong(Client &client, const char *song_uri,
std::string_view directory_uri) noexcept
try {
const auto *db = client.GetDatabase();
if (db == nullptr)
return directory_uri;
const auto *song = db->GetSong(song_uri);
if (song == nullptr)
return directory_uri;
AtScopeExit(db, song) { db->ReturnSong(song); };
if (song->real_uri == nullptr)
return directory_uri;
const char *real_uri = song->real_uri;
/* this is a simplification which is just enough for CUE
sheets (but may be incomplete): for each "../", go one
level up */
while ((real_uri = StringAfterPrefix(real_uri, "../")) != nullptr)
directory_uri = PathTraitsUTF8::GetParent(directory_uri);
return directory_uri;
} catch (...) {
/* ignore all exceptions from Database::GetSong() */
return directory_uri;
}
static CommandResult
read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset)
{
@@ -252,7 +293,13 @@ read_db_art(Client &client, Response &r, const char *uri, const uint64_t offset)
return CommandResult::ERROR;
}
std::string uri2 = storage->MapUTF8(uri);
return read_stream_art(r, uri2.c_str(), offset);
std::string_view directory_uri =
RealDirectoryOfSong(client,
uri,
PathTraitsUTF8::GetParent(uri2.c_str()));
return read_stream_art(r, directory_uri, offset);
}
#endif
@@ -273,7 +320,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);

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

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

@@ -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() &&

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

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

@@ -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);
@@ -445,6 +480,28 @@ UpdateWalk::DirectoryMakeUriParentChecked(Directory &root,
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 {
@@ -465,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());
}
@@ -496,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;
}

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

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

@@ -470,6 +470,7 @@ 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
}
@@ -658,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)
{
@@ -689,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
@@ -812,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);

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

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

@@ -32,12 +32,12 @@ extern "C" {
FfmpegFilter::FfmpegFilter(const AudioFormat &in_audio_format,
const AudioFormat &_out_audio_format,
Ffmpeg::FilterGraph &&_graph,
Ffmpeg::FilterContext &&_buffer_src,
Ffmpeg::FilterContext &&_buffer_sink) noexcept
AVFilterContext &_buffer_src,
AVFilterContext &_buffer_sink) noexcept
:Filter(_out_audio_format),
graph(std::move(_graph)),
buffer_src(std::move(_buffer_src)),
buffer_sink(std::move(_buffer_sink)),
buffer_src(_buffer_src),
buffer_sink(_buffer_sink),
in_format(Ffmpeg::ToFfmpegSampleFormat(in_audio_format.format)),
in_sample_rate(in_audio_format.sample_rate),
in_channels(in_audio_format.channels),
@@ -61,7 +61,7 @@ FfmpegFilter::FilterPCM(ConstBuffer<void> src)
memcpy(frame.GetData(0), src.data, src.size);
int err = av_buffersrc_add_frame(buffer_src.get(), frame.get());
int err = av_buffersrc_add_frame(&buffer_src, frame.get());
if (err < 0)
throw MakeFfmpegError(err, "av_buffersrc_write_frame() failed");
@@ -69,7 +69,7 @@ FfmpegFilter::FilterPCM(ConstBuffer<void> src)
frame.Unref();
err = av_buffersink_get_frame(buffer_sink.get(), frame.get());
err = av_buffersink_get_frame(&buffer_sink, frame.get());
if (err < 0) {
if (err == AVERROR(EAGAIN) || err == AVERROR_EOF)
return nullptr;

@@ -30,7 +30,7 @@
*/
class FfmpegFilter final : public Filter {
Ffmpeg::FilterGraph graph;
Ffmpeg::FilterContext buffer_src, buffer_sink;
AVFilterContext &buffer_src, &buffer_sink;
Ffmpeg::Frame frame;
FfmpegBuffer interleave_buffer;
@@ -51,8 +51,8 @@ public:
FfmpegFilter(const AudioFormat &in_audio_format,
const AudioFormat &_out_audio_format,
Ffmpeg::FilterGraph &&_graph,
Ffmpeg::FilterContext &&_buffer_src,
Ffmpeg::FilterContext &&_buffer_sink) noexcept;
AVFilterContext &_buffer_src,
AVFilterContext &_buffer_sink) noexcept;
/* virtual methods from class Filter */
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;

@@ -37,39 +37,79 @@ public:
std::unique_ptr<Filter> Open(AudioFormat &af) override;
};
/**
* Fallback for PreparedFfmpegFilter::Open() just in case the filter's
* native output format could not be determined.
*
* TODO: improve the MPD filter API to allow returning the output
* format later, and eliminate this kludge
*/
static auto
OpenWithAformat(const char *graph_string, AudioFormat &in_audio_format)
{
Ffmpeg::FilterGraph graph;
auto &buffer_src =
Ffmpeg::MakeAudioBufferSource(in_audio_format, *graph);
auto &buffer_sink = Ffmpeg::MakeAudioBufferSink(*graph);
AudioFormat out_audio_format = in_audio_format;
auto &aformat = Ffmpeg::MakeAformat(out_audio_format, *graph);
int error = avfilter_link(&aformat, 0, &buffer_sink, 0);
if (error < 0)
throw MakeFfmpegError(error, "avfilter_link() failed");
graph.ParseSingleInOut(graph_string, aformat, buffer_src);
graph.CheckAndConfigure();
return std::make_unique<FfmpegFilter>(in_audio_format,
out_audio_format,
std::move(graph),
buffer_src,
buffer_sink);
}
std::unique_ptr<Filter>
PreparedFfmpegFilter::Open(AudioFormat &in_audio_format)
{
Ffmpeg::FilterGraph graph;
auto buffer_src =
Ffmpeg::FilterContext::MakeAudioBufferSource(in_audio_format,
*graph);
auto &buffer_src =
Ffmpeg::MakeAudioBufferSource(in_audio_format, *graph);
auto buffer_sink = Ffmpeg::FilterContext::MakeAudioBufferSink(*graph);
auto &buffer_sink = Ffmpeg::MakeAudioBufferSink(*graph);
Ffmpeg::FilterInOut io_sink("out", *buffer_sink);
Ffmpeg::FilterInOut io_src("in", *buffer_src);
auto io = graph.Parse(graph_string, std::move(io_sink),
std::move(io_src));
/* if the filter's output format is not supported by MPD, this
"aformat" filter is inserted at the end and takes care for
the required conversion */
auto &aformat = Ffmpeg::MakeAutoAformat(*graph);
if (io.first.get() != nullptr)
throw std::runtime_error("FFmpeg filter has an open input");
if (io.second.get() != nullptr)
throw std::runtime_error("FFmpeg filter has an open output");
int error = avfilter_link(&aformat, 0, &buffer_sink, 0);
if (error < 0)
throw MakeFfmpegError(error, "avfilter_link() failed");
graph.ParseSingleInOut(graph_string, aformat, buffer_src);
graph.CheckAndConfigure();
const auto out_audio_format =
Ffmpeg::DetectFilterOutputFormat(in_audio_format, *buffer_src,
*buffer_sink);
Ffmpeg::DetectFilterOutputFormat(in_audio_format, buffer_src,
buffer_sink);
if (!out_audio_format.IsDefined())
/* the filter's native output format could not be
determined yet, but we need to know it now; as a
workaround for this MPD API deficiency, try again
with an "aformat" filter which forces a specific
output format */
return OpenWithAformat(graph_string, in_audio_format);
return std::make_unique<FfmpegFilter>(in_audio_format,
out_audio_format,
std::move(graph),
std::move(buffer_src),
std::move(buffer_sink));
buffer_src,
buffer_sink);
}
static std::unique_ptr<PreparedFilter>

@@ -42,24 +42,13 @@ OpenHdcdFilter(AudioFormat &in_audio_format)
{
Ffmpeg::FilterGraph graph;
auto buffer_src =
Ffmpeg::FilterContext::MakeAudioBufferSource(in_audio_format,
*graph);
auto &buffer_src =
Ffmpeg::MakeAudioBufferSource(in_audio_format,
*graph);
auto buffer_sink = Ffmpeg::FilterContext::MakeAudioBufferSink(*graph);
Ffmpeg::FilterInOut io_sink("out", *buffer_sink);
Ffmpeg::FilterInOut io_src("in", *buffer_src);
auto io = graph.Parse(hdcd_graph, std::move(io_sink),
std::move(io_src));
if (io.first.get() != nullptr)
throw std::runtime_error("FFmpeg filter has an open input");
if (io.second.get() != nullptr)
throw std::runtime_error("FFmpeg filter has an open output");
auto &buffer_sink = Ffmpeg::MakeAudioBufferSink(*graph);
graph.ParseSingleInOut(hdcd_graph, buffer_sink, buffer_src);
graph.CheckAndConfigure();
auto out_audio_format = in_audio_format;
@@ -69,8 +58,8 @@ OpenHdcdFilter(AudioFormat &in_audio_format)
return std::make_unique<FfmpegFilter>(in_audio_format,
out_audio_format,
std::move(graph),
std::move(buffer_src),
std::move(buffer_sink));
buffer_src,
buffer_sink);
}
class PreparedHdcdFilter final : public PreparedFilter {

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

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

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

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

@@ -32,6 +32,7 @@ LastInputStream::~LastInputStream() noexcept = default;
void
LastInputStream::Close() noexcept
{
uri.clear();
is.reset();
close_timer.Cancel();
}

@@ -64,8 +64,7 @@ public:
return is.get();
}
is.reset();
close_timer.Cancel();
Close();
is = open(new_uri, mutex);
uri = std::forward<U>(new_uri);

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

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

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

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

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

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

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

@@ -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();
}
}

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

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

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

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

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

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

@@ -17,9 +17,50 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef INPUT_TIDAL_HXX
#define INPUT_TIDAL_HXX
#ifndef MPD_FFMPEG_CHANNEL_LAYOUT_HXX
#define MPD_FFMPEG_CHANNEL_LAYOUT_HXX
extern const struct InputPlugin tidal_input_plugin;
extern "C" {
#include <libavutil/channel_layout.h>
}
/**
* Convert a MPD channel count to a libavutil channel_layout bit mask.
*/
static constexpr uint64_t
ToFfmpegChannelLayout(unsigned channels) noexcept
{
switch (channels) {
case 1:
return AV_CH_LAYOUT_MONO;
case 2:
return AV_CH_LAYOUT_STEREO;
case 3:
return AV_CH_LAYOUT_SURROUND;
case 4:
// TODO is this AV_CH_LAYOUT_2_2?
return AV_CH_LAYOUT_QUAD;
case 5:
// TODO is this AV_CH_LAYOUT_5POINT0_BACK?
return AV_CH_LAYOUT_5POINT0;
case 6:
return AV_CH_LAYOUT_5POINT1;
case 7:
return AV_CH_LAYOUT_6POINT1;
case 8:
return AV_CH_LAYOUT_7POINT1;
default:
/* unreachable */
return 0;
}
}
#endif

@@ -62,8 +62,14 @@ DetectFilterOutputFormat(const AudioFormat &in_audio_format,
frame.Unref();
err = av_buffersink_get_frame(&buffer_sink, frame.get());
if (err < 0)
if (err < 0) {
if (err == AVERROR(EAGAIN))
/* one sample was not enough input data for
the given filter graph */
return AudioFormat::Undefined();
throw MakeFfmpegError(err, "av_buffersink_get_frame() failed");
}
const SampleFormat sample_format = FromFfmpegSampleFormat(AVSampleFormat(frame->format));
if (sample_format == SampleFormat::UNDEFINED)

@@ -35,6 +35,9 @@ namespace Ffmpeg {
* between.
*
* This function can throw if the FFmpeg filter fails.
*
* @return the output format or AudioFormat::Undefined() if it was not
* possible to determine the format
*/
AudioFormat
DetectFilterOutputFormat(const AudioFormat &in_audio_format,

@@ -18,10 +18,13 @@
*/
#include "Filter.hxx"
#include "ChannelLayout.hxx"
#include "SampleFormat.hxx"
#include "pcm/AudioFormat.hxx"
#include "util/RuntimeError.hxx"
#include <cinttypes>
#include <stdio.h>
namespace Ffmpeg {
@@ -36,9 +39,32 @@ RequireFilterByName(const char *name)
return *filter;
}
FilterContext
FilterContext::MakeAudioBufferSource(AudioFormat &audio_format,
AVFilterGraph &graph_ctx)
static AVFilterContext &
CreateFilter(const AVFilter &filt,
const char *name, const char *args, void *opaque,
AVFilterGraph &graph_ctx)
{
AVFilterContext *context = nullptr;
int err = avfilter_graph_create_filter(&context, &filt,
name, args, opaque,
&graph_ctx);
if (err < 0)
throw MakeFfmpegError(err, "avfilter_graph_create_filter() failed");
return *context;
}
static AVFilterContext &
CreateFilter(const AVFilter &filt,
const char *name,
AVFilterGraph &graph_ctx)
{
return CreateFilter(filt, name, nullptr, nullptr, graph_ctx);
}
AVFilterContext &
MakeAudioBufferSource(AudioFormat &audio_format,
AVFilterGraph &graph_ctx)
{
AVSampleFormat src_format = ToFfmpegSampleFormat(audio_format.format);
if (src_format == AV_SAMPLE_FMT_NONE) {
@@ -57,19 +83,72 @@ FilterContext::MakeAudioBufferSource(AudioFormat &audio_format,
char abuffer_args[256];
sprintf(abuffer_args,
"sample_rate=%u:sample_fmt=%s:channels=%u:time_base=1/%u",
"sample_rate=%u:sample_fmt=%s:channel_layout=0x%" PRIx64 ":time_base=1/%u",
audio_format.sample_rate,
av_get_sample_fmt_name(src_format),
audio_format.channels,
ToFfmpegChannelLayout(audio_format.channels),
audio_format.sample_rate);
return {RequireFilterByName("abuffer"), "abuffer", abuffer_args, nullptr, graph_ctx};
return CreateFilter(RequireFilterByName("abuffer"), "abuffer",
abuffer_args, nullptr, graph_ctx);
}
FilterContext
FilterContext::MakeAudioBufferSink(AVFilterGraph &graph_ctx)
AVFilterContext &
MakeAudioBufferSink(AVFilterGraph &graph_ctx)
{
return {RequireFilterByName("abuffersink"), "abuffersink", graph_ctx};
return CreateFilter(RequireFilterByName("abuffersink"), "abuffersink",
graph_ctx);
}
AVFilterContext &
MakeAformat(AudioFormat &audio_format,
AVFilterGraph &graph_ctx)
{
AVSampleFormat dest_format = ToFfmpegSampleFormat(audio_format.format);
if (dest_format == AV_SAMPLE_FMT_NONE) {
switch (audio_format.format) {
case SampleFormat::S24_P32:
audio_format.format = SampleFormat::S32;
dest_format = AV_SAMPLE_FMT_S32;
break;
default:
audio_format.format = SampleFormat::S16;
dest_format = AV_SAMPLE_FMT_S16;
break;
}
}
char args[256];
sprintf(args,
"sample_rates=%u:sample_fmts=%s:channel_layouts=0x%" PRIx64,
audio_format.sample_rate,
av_get_sample_fmt_name(dest_format),
ToFfmpegChannelLayout(audio_format.channels));
return CreateFilter(RequireFilterByName("aformat"), "aformat",
args, nullptr, graph_ctx);
}
AVFilterContext &
MakeAutoAformat(AVFilterGraph &graph_ctx)
{
return CreateFilter(RequireFilterByName("aformat"), "aformat",
"sample_fmts=flt|s32|s16",
nullptr, graph_ctx);
}
void
FilterGraph::ParseSingleInOut(const char *filters, AVFilterContext &in,
AVFilterContext &out)
{
auto [inputs, outputs] = Parse(filters, {"out", in}, {"in", out});
if (inputs.get() != nullptr)
throw std::runtime_error("FFmpeg filter has an open input");
if (outputs.get() != nullptr)
throw std::runtime_error("FFmpeg filter has an open output");
}
} // namespace Ffmpeg

@@ -77,63 +77,38 @@ public:
}
};
class FilterContext {
AVFilterContext *context = nullptr;
/**
* Create an "abuffer" filter.
*
* @param the input audio format; may be modified by the
* function to ask the caller to do format conversion
*/
AVFilterContext &
MakeAudioBufferSource(AudioFormat &audio_format,
AVFilterGraph &graph_ctx);
public:
FilterContext() = default;
/**
* Create an "abuffersink" filter.
*/
AVFilterContext &
MakeAudioBufferSink(AVFilterGraph &graph_ctx);
FilterContext(const AVFilter &filt,
const char *name, const char *args, void *opaque,
AVFilterGraph &graph_ctx) {
int err = avfilter_graph_create_filter(&context, &filt,
name, args, opaque,
&graph_ctx);
if (err < 0)
throw MakeFfmpegError(err, "avfilter_graph_create_filter() failed");
}
/**
* Create an "aformat" filter.
*
* @param the output audio format; may be modified by the function if
* the given format is not supported by libavfilter
*/
AVFilterContext &
MakeAformat(AudioFormat &audio_format,
AVFilterGraph &graph_ctx);
FilterContext(const AVFilter &filt,
const char *name,
AVFilterGraph &graph_ctx)
:FilterContext(filt, name, nullptr, nullptr, graph_ctx) {}
FilterContext(FilterContext &&src) noexcept
:context(std::exchange(src.context, nullptr)) {}
~FilterContext() noexcept {
if (context != nullptr)
avfilter_free(context);
}
FilterContext &operator=(FilterContext &&src) noexcept {
using std::swap;
swap(context, src.context);
return *this;
}
/**
* Create an "abuffer" filter.
*
* @param the input audio format; may be modified by the
* function to ask the caller to do format conversion
*/
static FilterContext MakeAudioBufferSource(AudioFormat &audio_format,
AVFilterGraph &graph_ctx);
/**
* Create an "abuffersink" filter.
*/
static FilterContext MakeAudioBufferSink(AVFilterGraph &graph_ctx);
auto &operator*() noexcept {
return *context;
}
auto *get() noexcept {
return context;
}
};
/**
* Create an "aformat" filter which automatically converts the output
* to a format supported by MPD.
*/
AVFilterContext &
MakeAutoAformat(AVFilterGraph &graph_ctx);
class FilterGraph {
AVFilterGraph *graph = nullptr;
@@ -180,6 +155,9 @@ public:
return std::make_pair(std::move(inputs), std::move(outputs));
}
void ParseSingleInOut(const char *filters, AVFilterContext &in,
AVFilterContext &out);
std::pair<FilterInOut, FilterInOut> Parse(const char *filters) {
AVFilterInOut *inputs, *outputs;
int err = avfilter_graph_parse2(graph, filters,

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

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

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

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

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

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

@@ -21,6 +21,7 @@
#include "Sticker.hxx"
#include "lib/sqlite/Util.hxx"
#include "fs/Path.hxx"
#include "fs/NarrowPath.hxx"
#include "Idle.hxx"
#include "util/StringCompare.hxx"
#include "util/ScopeExit.hxx"
@@ -82,7 +83,7 @@ static const char sticker_sql_create[] =
"";
StickerDatabase::StickerDatabase(Path path)
:db(path.c_str())
:db(NarrowPath(path))
{
assert(!path.IsNull());

@@ -36,10 +36,12 @@ TagBuilder::TagBuilder(const Tag &other) noexcept
{
items.reserve(other.num_items);
const std::lock_guard<Mutex> protect(tag_pool_lock);
for (unsigned i = 0, n = other.num_items; i != n; ++i)
items.push_back(tag_pool_dup_item(other.items[i]));
const std::size_t n = other.num_items;
if (n > 0) {
const std::lock_guard<Mutex> protect(tag_pool_lock);
for (std::size_t i = 0; i != n; ++i)
items.push_back(tag_pool_dup_item(other.items[i]));
}
}
TagBuilder::TagBuilder(Tag &&other) noexcept
@@ -63,12 +65,17 @@ TagBuilder::operator=(const TagBuilder &other) noexcept
/* copy all attributes */
duration = other.duration;
has_playlist = other.has_playlist;
items = other.items;
/* increment the tag pool refcounters */
const std::lock_guard<Mutex> protect(tag_pool_lock);
for (auto i : items)
tag_pool_dup_item(i);
RemoveAll();
if (!other.items.empty()) {
items = other.items;
/* increment the tag pool refcounters */
const std::lock_guard<Mutex> protect(tag_pool_lock);
for (auto &i : items)
i = tag_pool_dup_item(i);
}
return *this;
}
@@ -76,9 +83,14 @@ TagBuilder::operator=(const TagBuilder &other) noexcept
TagBuilder &
TagBuilder::operator=(TagBuilder &&other) noexcept
{
using std::swap;
duration = other.duration;
has_playlist = other.has_playlist;
items = std::move(other.items);
/* swap the two TagItem lists so we don't need to touch the
tag pool just yet */
swap(items, other.items);
return *this;
}
@@ -92,7 +104,7 @@ TagBuilder::operator=(Tag &&other) noexcept
/* move all TagItem pointers from the Tag object; we don't
need to contact the tag pool, because all we do is move
references */
items.clear();
RemoveAll();
items.reserve(other.num_items);
std::copy_n(other.items, other.num_items, std::back_inserter(items));
@@ -174,11 +186,14 @@ TagBuilder::Complement(const Tag &other) noexcept
items.reserve(items.size() + other.num_items);
const std::lock_guard<Mutex> protect(tag_pool_lock);
for (unsigned i = 0, n = other.num_items; i != n; ++i) {
TagItem *item = other.items[i];
if (!present[item->type])
items.push_back(tag_pool_dup_item(item));
const std::size_t n = other.num_items;
if (n > 0) {
const std::lock_guard<Mutex> protect(tag_pool_lock);
for (std::size_t i = 0; i != n; ++i) {
TagItem *item = other.items[i];
if (!present[item->type])
items.push_back(tag_pool_dup_item(item));
}
}
}
@@ -238,6 +253,11 @@ TagBuilder::AddEmptyItem(TagType type) noexcept
void
TagBuilder::RemoveAll() noexcept
{
if (items.empty())
/* don't acquire the tag_pool_lock if we're not going
to call tag_pool_put_item() anyway */
return;
{
const std::lock_guard<Mutex> protect(tag_pool_lock);
for (auto i : items)

@@ -28,9 +28,11 @@ extern Mutex tag_pool_lock;
struct TagItem;
struct StringView;
[[nodiscard]]
TagItem *
tag_pool_get_item(TagType type, StringView value) noexcept;
[[nodiscard]]
TagItem *
tag_pool_dup_item(TagItem *item) noexcept;

@@ -35,7 +35,7 @@
#include <stdio.h>
#ifdef __clang__
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic push
// TODO: fix this warning properly
#pragma GCC diagnostic ignored "-Wformat-security"
@@ -59,7 +59,7 @@ FormatInvalidArgument(const char *fmt, Args&&... args) noexcept
return std::invalid_argument(buffer);
}
#ifdef __clang__
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#endif

@@ -78,7 +78,7 @@ private:
};
struct AvahiTimeout final {
TimerEvent timer;
TimerEvent event;
const AvahiTimeoutCallback callback;
void *const userdata;
@@ -87,17 +87,17 @@ public:
AvahiTimeout(const struct timeval *tv,
AvahiTimeoutCallback _callback, void *_userdata,
EventLoop &_loop)
:timer(_loop, BIND_THIS_METHOD(OnTimeout)),
:event(_loop, BIND_THIS_METHOD(OnTimeout)),
callback(_callback), userdata(_userdata) {
if (tv != nullptr)
timer.Schedule(ToSteadyClockDuration(*tv));
Schedule(*tv);
}
static void TimeoutUpdate(AvahiTimeout *t, const struct timeval *tv) {
if (tv != nullptr)
t->timer.Schedule(ToSteadyClockDuration(*tv));
t->Schedule(*tv);
else
t->timer.Cancel();
t->event.Cancel();
}
static void TimeoutFree(AvahiTimeout *t) {
@@ -105,6 +105,30 @@ public:
}
private:
[[gnu::pure]]
Event::Duration AbsoluteToDuration(const struct timeval &tv) noexcept {
if (tv.tv_sec == 0)
/* schedule immediately */
return {};
struct timeval now;
if (gettimeofday(&now, nullptr) < 0)
/* shouldn't ever fail, but if it does, do
something reasonable */
return std::chrono::seconds(1);
auto d = ToSteadyClockDuration(tv)
- ToSteadyClockDuration(now);
if (d.count() < 0)
return {};
return d;
}
void Schedule(const struct timeval &tv) noexcept {
event.Schedule(AbsoluteToDuration(tv));
}
void OnTimeout() noexcept {
callback(this, userdata);
}

@@ -157,8 +157,6 @@ MyAvahiClientCallback(AvahiClient *c, AvahiClientState state,
FormatDebug(avahi_domain, "Client changed to state %d", state);
switch (state) {
int reason;
case AVAHI_CLIENT_S_RUNNING:
LogDebug(avahi_domain, "Client is RUNNING");
@@ -169,8 +167,8 @@ MyAvahiClientCallback(AvahiClient *c, AvahiClientState state,
break;
case AVAHI_CLIENT_FAILURE:
reason = avahi_client_errno(c);
if (reason == AVAHI_ERR_DISCONNECTED) {
if (int reason = avahi_client_errno(c);
reason == AVAHI_ERR_DISCONNECTED) {
LogNotice(avahi_domain,
"Client Disconnected, will reconnect shortly");
if (avahi_group != nullptr) {

@@ -1,3 +1,4 @@
/packagecache/
/googletest-*/
/sqlite-*/

12
subprojects/sqlite3.wrap Normal file

@@ -0,0 +1,12 @@
[wrap-file]
directory = sqlite-amalgamation-3340100
source_url = https://www.sqlite.org/2021/sqlite-amalgamation-3340100.zip
source_filename = sqlite-amalgamation-3340100.zip
source_hash = e0b1c0345fe4338b936e17da8e1bd88366cd210e576834546977f040c12a8f68
patch_url = https://wrapdb.mesonbuild.com/v1/projects/sqlite3/3.34.1/1/get_zip
patch_filename = sqlite3-3.34.1-1-wrap.zip
patch_hash = cba9e47bdb4c02f88fadaae8deab357218d32562c6b86ce7ba0c72f107044360
[provide]
sqlite3 = sqlite3_dep