Compare commits

...

80 Commits

Author SHA1 Message Date
Max Kellermann
9274bc15bc release v0.20.22 2018-10-23 19:52:37 +02:00
Max Kellermann
751fff07fb input/Error: work around clang bug leading to crash
Closes 
2018-10-23 19:52:22 +02:00
Max Kellermann
f7d1408a1a android/build.py: improved libc++ linker flags (for NDK r18)
The flag `-stdlib=libc++` doesn't appear to work because it attempts
to load `libc++` and not `libc++_static`, and it omits `libc++abi`.
2018-10-23 19:32:25 +02:00
Max Kellermann
e4e14ef6b0 Makefile.am: include mpd.svg in source tarball 2018-10-23 18:48:01 +02:00
Max Kellermann
005e691339 decoder/fluidsynth: adapt to API change in version 2.0
Closes 
2018-10-23 18:44:28 +02:00
Max Kellermann
61eff1cddf Makefile.am: install the SVG icon
Closes 
2018-10-22 18:39:50 +02:00
Max Kellermann
c26703b7e6 SongFilter: check value.empty() after checking tag fallbacks
In this new order, a filter 'AlbumArtist ""' matches only on songs
which neither have `AlbumArtist` nor `Artist`.
2018-10-22 18:34:47 +02:00
Max Kellermann
db27bb76e2 db: fix broken command "list ... group"
Grouping in the "list" command was completely broken from the start,
unlike "count group".  I have no idea what I have been thinking when I
wrote commit ae178c77bd, but it didn't
make any sense.

This commit is a rewrite of the feature.

For clients to be able to detect this feature, this commit also
increments the protocol version.
2018-10-22 13:08:24 +02:00
Max Kellermann
7cfe929c36 db/Count: print empty group if song without grouped tag exists
Be consistent with "list" responses.
2018-10-22 12:42:18 +02:00
Max Kellermann
6c06244e83 db/Count: move code to tag/VisitCallback.hxx 2018-10-22 11:50:51 +02:00
Max Kellermann
53448e4633 tag/Fallback: add tag fallbacks for AlbumArtistSort, ArtistSort
Just like AlbumArtist falls back to Artist, AlbumArtistSort should
fall back tom AlbumArtist, ArtistSort and finally Artist.

Closes 
2018-10-22 10:52:42 +02:00
Max Kellermann
21adc78713 SongFilter: use ApplyTagFallback() 2018-10-22 10:46:26 +02:00
Max Kellermann
0340b01392 db/Count: use ApplyTagFallback() 2018-10-22 10:46:26 +02:00
Max Kellermann
94aed92e9a tag/Set: move code to ApplyTagWithFallback() 2018-10-22 10:10:43 +02:00
Max Kellermann
6b9966e969 tag/Set: include cleanup 2018-10-22 10:09:56 +02:00
Max Kellermann
4bc5333995 tag/Set: use TagBuilder::AddItemUnchecked()
This improves the workaround from commit
b5ba94f1de and actually gives a useful
result for "list" with a disabled tag.
2018-10-22 10:06:04 +02:00
Max Kellermann
ff58b8d255 tag/Builder: move code to AddItemUnchecked() 2018-10-22 10:03:47 +02:00
Max Kellermann
3f3f0af543 python/build/libs.py: upgrade CURL to 7.61.1 2018-10-22 08:53:30 +02:00
Max Kellermann
850d208b7b python/build/libs.py: upgrade Opus to 1.3 2018-10-22 08:44:11 +02:00
Max Kellermann
da563940b4 python/build/libs.py: upgrade libmpdclient to 2.16 2018-10-22 08:32:32 +02:00
Max Kellermann
282859a62a java/String: include cleanup 2018-08-28 13:07:28 +02:00
Max Kellermann
fbeb5eefdc java/Class: drop unnecessary namespace spec 2018-08-28 13:01:01 +02:00
Max Kellermann
85bada0505 java/Class: use DiscardException() in FindOptional()
Sometimes, the JVM returns a non-nullptr value with an exception
pending (seen on Android 1.6, maybe a Dalvik bug?).  Let's catch all
such cases.
2018-08-28 13:00:08 +02:00
Max Kellermann
cf96135125 android/Main: remove SDK_INT diversion
MPD has minSdkVersion=21 which is above all the checks here.
2018-08-20 11:25:47 +02:00
Max Kellermann
1ff97783ea Makefile.am: use $(AM_V_GEN) and $(AM_V_at) 2018-08-20 11:22:56 +02:00
Max Kellermann
2bc42c6445 Makefile.am: use $(MKDIR_P) instead of "mkdir -p" 2018-08-20 11:13:25 +02:00
Max Kellermann
49372a222f Makefile.am: use $(@D)/$(@F) instead of $(dir/notdir ...) 2018-08-20 11:04:35 +02:00
Thomas Guillem
9127afbf3f lib/nfs/Connection: use nfs_stat64_async
Since nfs_stat_async is deprecated.
2018-08-20 10:51:24 +02:00
Max Kellermann
f2caac595a configure.ac: specify minimum libnfs version 1.9.5
This is the version in Debian Jessie (oldstable), a reasonable "old
enough" version to keep support for.
2018-08-20 10:51:24 +02:00
Thomas Guillem
14d3a7ae83 android: use a gray notification icon 2018-08-20 00:07:35 +02:00
Thomas Guillem
f37ab5482b android: improve Settings UI and run mpd on boot
add 2 preferences to:
 - enable Wakelock when MPD is running (prevent suspend)
 - run MPD on boot

and display MPD logs
2018-08-20 00:07:18 +02:00
Thomas Guillem
ef38dbe5bf android: fix AndroidManifest.xml warnings
- <uses-permission> must be before <application>
 - specify allowBackup (default)
2018-08-20 00:07:18 +02:00
Thomas Guillem
54a5491b86 android: Main is now a service
- add Settings: Activity to start / stop MPD Service (Main).

- Main is a service that run in foreground with a notification. See
  Service.startForeground documentation for more details.

- Main.Client is used to control the service: start or stop it and also receive
  callbacks when service encounters an error, is killed, is started or is
  stopped.

- Main.start to start the service without any fallback.
2018-08-19 23:35:49 +02:00
Thomas Guillem
aff070bcbb android: add LogListener
A Java object to send logs on the android side.
2018-08-19 23:32:24 +02:00
Max Kellermann
5af2632d4f Makefile.am: use javac instead of javah to generate JNI header
javah is deprecated.
2018-08-19 23:27:12 +02:00
Max Kellermann
44a31357f4 android/AndroidManifest.xml: increase targetSdkVersion to 26 (required by Google Play) 2018-08-18 20:44:18 +02:00
Joshua Wise
29f78b18b1 storage/plugins/CurlStorage: URL-encode paths in CurlStorage::MapUTF8
When using a database that was not created with a WebDAV music_directory
(i.e., if using a remote database, on which updates happen locally) and
using the Curl storage plugin, MPD would previously send GET requests that
had unescaped spaces in them.  This change uses Curl's URL-encode API to
solve this.
2018-08-17 23:03:56 +02:00
Max Kellermann
147872fe97 lib/curl/Easy: add curl_easy_escape() wrapper 2018-08-17 23:02:49 +02:00
Max Kellermann
38edb58054 increment version number to 0.20.22 2018-08-17 23:02:13 +02:00
Max Kellermann
98afae2520 release v0.20.21 2018-08-17 19:50:59 +02:00
Max Kellermann
ddc85c620f configure.ac: make the GIT_COMMIT command worktree-safe
`$srcdir/.git` doesn't exist if `$srcdir` is a worktree.
2018-08-17 19:50:53 +02:00
Max Kellermann
12bc625fe1 android/build.py: add aarch64 support 2018-08-17 19:20:25 +02:00
Max Kellermann
6b407356b9 configure.ac: set ANDROID_ABI=x86 for the Android-x86 build
This was missing in commit 8266ab5588 for .
2018-08-17 19:18:29 +02:00
Max Kellermann
a4e0b52468 configure.ac, Makefile.am: add variable ANDROID_ABI 2018-08-17 19:01:37 +02:00
Max Kellermann
98efb4f6d5 android: raise minSdkVersion to 21
The number of MPD installs on Android < 5.0 is negligible, and that
API version introduces lots of useful features for MPD.
2018-08-17 19:01:37 +02:00
Max Kellermann
36edb4886c android/build.py: add variable "android_api_level" 2018-08-17 19:01:37 +02:00
Max Kellermann
76290f786d python/build/meson.py: set "needs_exe_wrapper=true"
Prevent Meson from running Android-x86 binaries.  That will fail
because the Android standard libraries are most likely not installed.
2018-08-17 19:00:42 +02:00
Max Kellermann
c6299c26b5 python/build/libs.py: disable libnfs utils/examples 2018-08-17 18:32:07 +02:00
Max Kellermann
fb5f9baf9c android/build.py: enable libexpat for the "curl" storage plugin 2018-08-17 17:15:05 +02:00
Max Kellermann
dee591d970 python/build/libs.py: disable expat documentation 2018-08-17 17:13:39 +02:00
Joshua Wise
a5cc13b0c5 build: Add libexpat to the crosscompile build on Windows.
The Curl plugin requires libexpat in order to work these days, so we should
download and build it in order to get the plugin enabled on Windows.
2018-08-17 17:13:39 +02:00
Max Kellermann
aaf588aeaa python/libs: upgrade Boost to 1.68.0 2018-08-17 17:13:39 +02:00
Max Kellermann
533a3def9f Makefile.am: add missing $(CURL_CFLAGS) and $(EXPAT_CFLAGS)
Fixes problems with the Windows build because `-DCURL_STATICLIB` was
missing, causing error messages like:

 "undefined reference to `__imp_curl_slist_free_all'"
2018-08-17 17:06:03 +02:00
Max Kellermann
fcf487f4e0 playlist/cue: support file type declaration "FLAC" (non-standard)
According to http://wiki.hydrogenaud.io/index.php?title=Cue_sheet FLAC
files should use the "WAVE" file type, but I recently encountered CUE
files declared as "FLAC" which could not be read by MPD.
2018-08-13 08:18:18 +02:00
Max Kellermann
906972973e case-insensitive URI scheme comparison
Required according to RFC 3986:

> An implementation should accept uppercase letters as equivalent to
> lowercase in scheme names

Closes 
2018-08-02 11:01:45 +02:00
Max Kellermann
116edf5fce util/ASCII: add StringStartsWithCaseASCII() 2018-08-02 10:42:28 +02:00
Max Kellermann
8581013911 configure.ac: default to --disable-daemon on Windows
This typo was present since the option was added in commit ed001e0cfb
2018-08-02 10:27:13 +02:00
Max Kellermann
b1e073bacd python/build/libs.py: upgrade FFmpeg to 4.0.2 2018-07-29 18:29:48 +02:00
skidoo23
501e48daba configure.ac: Do not link libsidplayfp against libresid-builder 2018-07-29 18:22:03 +02:00
skidoo23
643ecd1edd configure.ac: clarify sidplay related info 2018-07-29 18:21:42 +02:00
Max Kellermann
7393e1cba1 python/build/libs: upgrade libnfs to 3.0.0 2018-07-16 10:58:19 +02:00
Max Kellermann
ceee47fda8 python/build/libs: upgrade CURL to 7.61.0 2018-07-16 10:55:08 +02:00
Max Kellermann
6f3c0d0a60 AudioFormat: include cleanup 2018-07-06 19:35:31 +02:00
Max Kellermann
466625f7ad input/curl: use new class HttpStatusError
This way, IsFileNotFound() can detect status 404.
2018-07-06 19:26:11 +02:00
Max Kellermann
b8259e604a db/update/{Walk,ExcludeList}: use InputStream to read .mpdignore
Supports .mpdignore on NFS/SMB and others (closes ).
2018-07-06 19:19:04 +02:00
Max Kellermann
86e2075c63 lib/nfs/Connection: use new class NfsClientError
Allows callers to extract the NFS error code.
2018-07-06 19:17:34 +02:00
Max Kellermann
30900b2fe2 input/Error: new library providing IsFileNotFound() 2018-07-06 19:16:01 +02:00
Max Kellermann
fd7ae7ea4c input/Domain: remove obsolete variable 2018-07-06 19:13:53 +02:00
Max Kellermann
60d5bf0240 util/StringFormat: new utility library 2018-07-06 19:07:02 +02:00
Max Kellermann
41cdc4e14b input/Offset: add macro PRIoffset 2018-07-06 19:06:05 +02:00
Max Kellermann
87dfca0477 input/curl: remove obsolete Windows sprintf() fallback
See commit be137a191e
2018-07-06 19:05:09 +02:00
Max Kellermann
e1ee8e7812 util/FormatString: remove obsolete Windows fallback
Since 7d353bbe2a, _GNU_SOURCE is always
defined, which implies __USE_MINGW_ANSI_STDIO and thus switches to
the mingw implementations of the printf() family.  That's
standards-compliant, unlike Microsoft's CRT implementations.
2018-07-06 19:04:33 +02:00
Max Kellermann
63406efcd8 db/update/ExcludeList: allow comments only at start of line 2018-07-06 18:27:17 +02:00
Max Kellermann
d5c132fca0 db/update/ExcludeList: move code to ParseLine() 2018-07-06 18:25:27 +02:00
Max Kellermann
5f082a2739 output/httpd: remove broken DLNA support code
This code was added in 21851c0673 but
looks completely broken:

- the status code is "206 OK" but "206" would be "Partial Content"

- the "Content-Length" header has a bogus value

- the "Content-RangeX" parameter has different bogus values (why
  "Content-RangeX" anyway and not "Content-Range"?)

Apart from that, there are strange undocumented non-standard headers
which are probably there to work around bugs/expectations in one
broken proprietary client product.  But these days, MPD doesn't bend
over to support broken clients.  So let's kill this code.

Closes 
2018-07-06 17:28:01 +02:00
Max Kellermann
7d6a762845 python/build/libs.py: upgrade FFmpeg to 4.0.1 2018-06-22 22:35:27 +02:00
Max Kellermann
8dcb1f805d db/proxy: support tags "ArtistSort", "AlbumArtistSort", "AlbumSort"
Closes 
2018-05-28 20:14:07 +02:00
Max Kellermann
a8b9e5b9b9 db/proxy: add "password" setting
Closes 
2018-05-28 20:01:08 +02:00
Max Kellermann
04f928e2b0 doc/user.xml: remove copy&paste fallout 2018-05-28 20:01:08 +02:00
Max Kellermann
c7a803c922 increment version number to 0.20.21 2018-05-28 19:46:54 +02:00
99 changed files with 1863 additions and 743 deletions
Makefile.amNEWS
android
configure.ac
doc
python/build
src
win32

@@ -234,6 +234,7 @@ libmpd_a_SOURCES += \
endif
CURL_SOURCES = \
src/lib/curl/Error.hxx \
src/lib/curl/Version.cxx src/lib/curl/Version.hxx \
src/lib/curl/Global.cxx src/lib/curl/Global.hxx \
src/lib/curl/Request.cxx src/lib/curl/Request.hxx \
@@ -275,7 +276,8 @@ libjava_a_SOURCES = \
noinst_LIBRARIES += libandroid.a
libandroid_a_SOURCES = \
src/android/Context.cxx src/android/Context.hxx \
src/android/Environment.cxx src/android/Environment.hxx
src/android/Environment.cxx src/android/Environment.hxx \
src/android/LogListener.cxx src/android/LogListener.hxx
libandroid_a_CPPFLAGS = $(AM_CPPFLAGS) -Iandroid/build/include
noinst_LIBRARIES += libmain.a
@@ -290,15 +292,17 @@ clean-local:
rm -rf android/build
libmpd.so: $(filter %.a,$(src_mpd_LDADD)) libmain.a
$(AM_V_GEN)
$(AM_V_CXXLD)$(CXXLD) -shared -Wl,--no-undefined,-shared,-Bsymbolic -llog -lz -o $@ $(AM_CXXFLAGS) $(CXXFLAGS) $(LDFLAGS) src/libmain_a-Main.o $(src_mpd_LDADD) $(LIBS)
ANDROID_SDK_BUILD_TOOLS_VERSION = 27.0.0
ANDROID_SDK_PLATFORM = android-17
ANDROID_SDK_PLATFORM = android-21
ANDROID_BUILD_TOOLS_DIR = $(ANDROID_SDK)/build-tools/$(ANDROID_SDK_BUILD_TOOLS_VERSION)
ANDROID_SDK_PLATFORM_DIR = $(ANDROID_SDK)/platforms/$(ANDROID_SDK_PLATFORM)
JAVAC = javac
AIDL = $(ANDROID_BUILD_TOOLS_DIR)/aidl
AAPT = $(ANDROID_BUILD_TOOLS_DIR)/aapt
DX = $(ANDROID_BUILD_TOOLS_DIR)/dx
ZIPALIGN = $(ANDROID_BUILD_TOOLS_DIR)/zipalign
@@ -306,18 +310,30 @@ ZIPALIGN = $(ANDROID_BUILD_TOOLS_DIR)/zipalign
ANDROID_XML_RES := $(wildcard $(srcdir)/android/res/*/*.xml)
ANDROID_XML_RES_COPIES := $(patsubst $(srcdir)/android/%,android/build/%,$(ANDROID_XML_RES))
JAVA_SOURCE_NAMES = Bridge.java Loader.java Main.java
JAVA_SOURCE_NAMES = Bridge.java Loader.java Main.java Settings.java
JAVA_SOURCE_PATHS = $(addprefix $(srcdir)/android/src/,$(JAVA_SOURCE_NAMES))
JAVA_CLASSFILES_DIR = android/build/classes
$(ANDROID_XML_RES_COPIES): $(ANDROID_XML_RES)
@$(MKDIR_P) $(dir $@)
cp $(patsubst android/build/%,$(srcdir)/android/%,$@) $@
AIDL_FILES = $(wildcard $(srcdir)/android/src/*.aidl)
AIDL_JAVA_FILES = $(patsubst $(srcdir)/android/src/%.aidl,android/build/src/org/musicpd/%.java,$(AIDL_FILES))
android/build/resources.apk: $(ANDROID_XML_RES_COPIES) android/build/res/drawable/icon.png
@$(MKDIR_P) android/build/gen
$(AAPT) package -f -m --auto-add-overlay \
android/build/src/org/musicpd/IMain.java: android/build/src/org/musicpd/IMainCallback.java
$(AIDL_JAVA_FILES): android/build/src/org/musicpd/%.java: $(srcdir)/android/src/%.aidl
$(AM_V_GEN)
$(AM_V_at)$(MKDIR_P) $(@D)
$(AM_V_at)cp $< $(@D)/
$(AM_V_at)$(AIDL) -Iandroid/build/src -oandroid/build/src $(patsubst %.java,%.aidl,$@)
$(ANDROID_XML_RES_COPIES): $(ANDROID_XML_RES)
$(AM_V_at)$(MKDIR_P) $(@D)
$(AM_V_at)cp $(patsubst android/build/%,$(srcdir)/android/%,$@) $@
android/build/resources.apk: $(ANDROID_XML_RES_COPIES) android/build/res/drawable/icon.png android/build/res/drawable/notification_icon.png
$(AM_V_GEN)
$(AM_V_at)$(MKDIR_P) android/build/gen
$(AM_V_at)$(AAPT) package -f -m --auto-add-overlay \
--custom-package org.musicpd \
-M $(srcdir)/android/AndroidManifest.xml \
-S android/build/res \
@@ -328,40 +344,51 @@ android/build/resources.apk: $(ANDROID_XML_RES_COPIES) android/build/res/drawabl
# R.java is generated by aapt, when resources.apk is generated
android/build/gen/org/musicpd/R.java: android/build/resources.apk
android/build/classes.dex: $(JAVA_SOURCE_PATHS) android/build/gen/org/musicpd/R.java
@$(MKDIR_P) $(JAVA_CLASSFILES_DIR)
$(JAVAC) -source 1.6 -target 1.6 -Xlint:-options \
android/build/classes.dex: $(JAVA_SOURCE_PATHS) $(AIDL_JAVA_FILES) android/build/gen/org/musicpd/R.java
$(AM_V_GEN)
$(AM_V_at)$(MKDIR_P) $(JAVA_CLASSFILES_DIR)
$(AM_V_at)$(JAVAC) -source 1.6 -target 1.6 -Xlint:-options \
-cp $(ANDROID_SDK_PLATFORM_DIR)/android.jar:$(JAVA_CLASSFILES_DIR) \
-h android/build/include \
-d $(JAVA_CLASSFILES_DIR) $^
$(DX) --dex --output $@ $(JAVA_CLASSFILES_DIR)
$(AM_V_at)$(DX) --dex --output $@ $(JAVA_CLASSFILES_DIR)
android/build/include/org_musicpd_Bridge.h: android/build/classes.dex
javah -classpath $(ANDROID_SDK_PLATFORM_DIR)/android.jar:$(JAVA_CLASSFILES_DIR) -d $(@D) org.musicpd.Bridge
BUILT_SOURCES = android/build/include/org_musicpd_Bridge.h
android/build/lib/armeabi-v7a/libmpd.so: libmpd.so
mkdir -p $(@D)
rm -f $@
$(STRIP) -o $@ $<
android/build/lib/$(ANDROID_ABI)/libmpd.so: libmpd.so
$(AM_V_GEN)
$(AM_V_at)$(MKDIR_P) $(@D)
$(AM_V_at)rm -f $@
$(AM_V_at)$(STRIP) -o $@ $<
android/build/res/drawable/icon.png: mpd.svg
mkdir -p $(@D)
rsvg-convert --width=48 --height=48 $< -o $@
$(AM_V_GEN)
$(AM_V_at)$(MKDIR_P) $(@D)
$(AM_V_at)rsvg-convert --width=48 --height=48 $< -o $@
android/build/res/drawable/notification_icon.png: android/build/res/drawable/icon.png
$(AM_V_GEN)
$(AM_V_at)convert $< -colorspace Gray -gamma 2.2 $@
.DELETE_ON_ERROR: android/build/unsigned.apk
android/build/unsigned.apk: android/build/classes.dex android/build/resources.apk android/build/lib/armeabi-v7a/libmpd.so
cp android/build/resources.apk $@
cd $(dir $@) && zip -q -r $(notdir $@) classes.dex lib
android/build/unsigned.apk: android/build/classes.dex android/build/resources.apk android/build/lib/$(ANDROID_ABI)/libmpd.so
$(AM_V_GEN)
$(AM_V_at)cp android/build/resources.apk $@
$(AM_V_at)cd $(@D) && zip -q -r $(@F) classes.dex lib
android/build/$(APK_NAME)-debug.apk: android/build/unsigned.apk
jarsigner -keystore $(HOME)/.android/debug.keystore -storepass android -signedjar $@ $< androiddebugkey
$(AM_V_GEN)
$(AM_V_at)jarsigner -keystore $(HOME)/.android/debug.keystore -storepass android -signedjar $@ $< androiddebugkey
android/build/$(APK_NAME)-release-unaligned.apk: android/build/unsigned.apk
jarsigner -digestalg SHA1 -sigalg MD5withRSA -storepass:env ANDROID_KEYSTORE_PASS -keystore $(ANDROID_KEYSTORE) -signedjar $@ $< $(ANDROID_KEY_ALIAS)
$(AM_V_GEN)
$(AM_V_at)jarsigner -digestalg SHA1 -sigalg MD5withRSA -storepass:env ANDROID_KEYSTORE_PASS -keystore $(ANDROID_KEYSTORE) -signedjar $@ $< $(ANDROID_KEY_ALIAS)
android/build/$(APK_NAME).apk: android/build/$(APK_NAME)-release-unaligned.apk
$(ZIPALIGN) -f 4 $< $@
$(AM_V_GEN)
$(AM_V_at)$(ZIPALIGN) -f 4 $< $@
endif
@@ -444,6 +471,7 @@ libutil_a_SOURCES = \
src/util/NumberParser.hxx \
src/util/MimeType.cxx src/util/MimeType.hxx \
src/util/StringBuffer.hxx \
src/util/StringFormat.hxx \
src/util/StringPointer.hxx \
src/util/StringView.cxx src/util/StringView.hxx \
src/util/AllocatedString.cxx src/util/AllocatedString.hxx \
@@ -712,6 +740,7 @@ NFS_SOURCES = \
src/lib/nfs/Cancellable.hxx \
src/lib/nfs/Lease.hxx \
src/lib/nfs/Connection.cxx src/lib/nfs/Connection.hxx \
src/lib/nfs/Error.cxx src/lib/nfs/Error.hxx \
src/lib/nfs/Manager.cxx src/lib/nfs/Manager.hxx \
src/lib/nfs/Glue.cxx src/lib/nfs/Glue.hxx \
src/lib/nfs/Base.cxx src/lib/nfs/Base.hxx \
@@ -734,6 +763,8 @@ libstorage_a_SOURCES = \
src/storage/FileInfo.hxx
libstorage_a_CPPFLAGS = $(AM_CPPFLAGS) \
$(CURL_CFLAGS) \
$(EXPAT_CFLAGS) \
$(NFS_CFLAGS) \
$(SMBCLIENT_CFLAGS)
@@ -946,13 +977,14 @@ libtag_a_SOURCES =\
src/tag/TagItem.hxx \
src/tag/TagHandler.cxx src/tag/TagHandler.hxx \
src/tag/Mask.hxx \
src/tag/Fallback.hxx \
src/tag/VisitFallback.hxx \
src/tag/Settings.cxx src/tag/Settings.hxx \
src/tag/TagConfig.cxx src/tag/TagConfig.hxx \
src/tag/TagNames.c \
src/tag/TagString.cxx src/tag/TagString.hxx \
src/tag/TagPool.cxx src/tag/TagPool.hxx \
src/tag/TagTable.cxx src/tag/TagTable.hxx \
src/tag/Set.cxx src/tag/Set.hxx \
src/tag/Format.cxx src/tag/Format.hxx \
src/tag/VorbisComment.cxx src/tag/VorbisComment.hxx \
src/tag/ReplayGain.cxx src/tag/ReplayGain.hxx \
@@ -1292,7 +1324,7 @@ endif
#
libinput_a_SOURCES = \
src/input/Domain.cxx src/input/Domain.hxx \
src/input/Error.cxx src/input/Error.hxx \
src/input/Init.cxx src/input/Init.hxx \
src/input/Registry.cxx src/input/Registry.hxx \
src/input/Open.cxx \
@@ -1670,6 +1702,13 @@ FILTER_LIBS = \
$(PCM_LIBS)
#
# Icon
#
iconsdir = $(datadir)/icons/hicolor/scalable/apps
icons_DATA = mpd.svg
#
# systemd unit
#
@@ -2439,6 +2478,7 @@ endif
#
EXTRA_DIST = $(doc_DATA) autogen.sh \
mpd.svg \
test/test_archive_bzip2.sh \
test/test_archive_iso9660.sh \
test/test_archive_zzip.sh \

30
NEWS

@@ -1,3 +1,33 @@
ver 0.20.22 (2018/10/23)
* protocol
- add tag fallbacks for AlbumArtistSort, ArtistSort
- fix empty string filter on fallback tags
- "count group ..." can print an empty group
- fix broken command "list ... group"
* storage
- curl: URL-encode paths
* decoder
- fluidsynth: adapt to API change in version 2.0
* Android
- now runs as a service
- add button to start/stop MPD
- add option to auto-start on boot
* work around clang bug leading to crash
* install the SVG icon
ver 0.20.21 (2018/08/17)
* database
- proxy: add "password" setting
- proxy: support tags "ArtistSort", "AlbumArtistSort", "AlbumSort"
- simple: allow .mpdignore comments only at start of line
* output
- httpd: remove broken DLNA support code
* playlist
- cue: support file type declaration "FLAC" (non-standard)
* URI schemes are case insensitive
* Android, Windows
- enable the "curl" storage plugin
ver 0.20.20 (2018/05/22)
* protocol
- fix "modified-since" filter regression

@@ -2,23 +2,32 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
android:versionCode="19"
android:versionName="0.20.20">
android:versionCode="21"
android:versionName="0.20.22">
<uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17"/>
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/>
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".Main"
android:label="@string/app_name"
android:launchMode="singleInstance">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application android:allowBackup="true"
android:icon="@drawable/icon"
android:label="@string/app_name">
<activity android:name=".Settings"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".Receiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service android:name=".Main" android:process=":main"/>
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

@@ -29,6 +29,15 @@ android_abis = {
'cflags': '-march=armv7-a -mfpu=vfp -mfloat-abi=softfp',
},
'arm64-v8a': {
'android_api_level': '21',
'arch': 'aarch64-linux-android',
'ndk_arch': 'arm64',
'toolchain_arch': 'aarch64-linux-android',
'llvm_triple': 'aarch64-none-linux-android',
'cflags': '',
},
'x86': {
'arch': 'i686-linux-android',
'ndk_arch': 'x86',
@@ -65,7 +74,8 @@ class AndroidNdkToolchain:
self.build_path = build_path
ndk_arch = abi_info['ndk_arch']
ndk_platform = 'android-14'
android_api_level = '21'
ndk_platform = 'android-' + android_api_level
# select the NDK compiler
gcc_version = '4.9'
@@ -106,7 +116,7 @@ class AndroidNdkToolchain:
self.cppflags = '--sysroot=' + sysroot + \
' -isystem ' + os.path.join(install_prefix, 'include') + \
' -isystem ' + os.path.join(sysroot, 'usr', 'include', arch) + \
' -D__ANDROID_API__=14'
' -D__ANDROID_API__=' + android_api_level
self.ldflags = '--sysroot=' + sysroot + \
' -L' + os.path.join(install_prefix, 'lib') + \
' -L' + os.path.join(target_root, 'usr', 'lib') + \
@@ -116,18 +126,21 @@ class AndroidNdkToolchain:
self.is_arm = ndk_arch == 'arm'
self.is_armv7 = self.is_arm and 'armv7' in self.cflags
self.is_aarch64 = ndk_arch == 'arm64'
self.is_windows = False
libcxx_path = os.path.join(ndk_path, 'sources/cxx-stl/llvm-libc++')
libcxx_libs_path = os.path.join(libcxx_path, 'libs', android_abi)
libstdcxx_flags = '-stdlib=libc++'
libstdcxx_flags = ''
libstdcxx_cxxflags = libstdcxx_flags + ' -isystem ' + os.path.join(libcxx_path, 'include') + ' -isystem ' + os.path.join(ndk_path, 'sources/android/support/include')
libstdcxx_ldflags = libstdcxx_flags + ' -static-libstdc++ -L' + libcxx_libs_path
libstdcxx_ldflags = libstdcxx_flags + ' -L' + libcxx_libs_path
libstdcxx_libs = '-lc++_static -lc++abi'
if use_cxx:
self.cxxflags += ' ' + libstdcxx_cxxflags
self.ldflags += ' ' + libstdcxx_ldflags
self.libs += ' ' + libstdcxx_libs
self.env = dict(os.environ)
@@ -146,6 +159,7 @@ thirdparty_libs = [
libid3tag,
ffmpeg,
curl,
libexpat,
libnfs,
boost,
]

@@ -0,0 +1,22 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="10dp" >
<ImageView android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_alignParentLeft="true"
android:layout_marginRight="10dp" />
<TextView android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/image"
style="Custom Notification Title" />
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/image"
android:layout_below="@id/title"
style="Custom Notification Text" />
</RelativeLayout>

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:typeface="monospace" />

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<ToggleButton
android:id="@+id/run"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textOn="@string/toggle_button_run_on"
android:textOff="@string/toggle_button_run_off" />
<CheckBox
android:id="@+id/run_on_boot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/checkbox_run_on_boot" />
<CheckBox
android:id="@+id/wakelock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/checkbox_wakelock" />
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ListView
android:id="@+id/log_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dip" />
</LinearLayout>

@@ -2,4 +2,10 @@
<resources>
<string name="app_name">MPD</string>
<string name="notification_title_mpd_running">Music Player Daemon is running</string>
<string name="notification_text_mpd_running">Touch for MPD options.</string>
<string name="toggle_button_run_on">MPD is running</string>
<string name="toggle_button_run_off">MPD is not running</string>
<string name="checkbox_run_on_boot">Run MPD automatically on boot</string>
<string name="checkbox_wakelock">Prevent suspend when MPD is running (Wakelock)</string>
</resources>

@@ -25,6 +25,12 @@ import android.content.Context;
* Bridge to native code.
*/
public class Bridge {
public static native void run(Context context);
/* used by jni */
public interface LogListener {
public void onLog(int priority, String msg);
}
public static native void run(Context context, LogListener logListener);
public static native void shutdown();
}

12
android/src/IMain.aidl Normal file

@@ -0,0 +1,12 @@
package org.musicpd;
import org.musicpd.IMainCallback;
interface IMain
{
void start();
void stop();
void setWakelockEnabled(boolean enabled);
boolean isRunning();
void registerCallback(IMainCallback cb);
void unregisterCallback(IMainCallback cb);
}

@@ -0,0 +1,9 @@
package org.musicpd;
interface IMainCallback
{
void onStarted();
void onStopped();
void onError(String error);
void onLog(int priority, String msg);
}

@@ -19,57 +19,360 @@
package org.musicpd;
import android.app.Activity;
import android.os.Bundle;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.widget.TextView;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import android.widget.RemoteViews;
public class Main extends Activity implements Runnable {
private static final String TAG = "MPD";
public class Main extends Service implements Runnable {
private static final String TAG = "Main";
private static final String REMOTE_ERROR = "MPD process was killed";
private static final int MAIN_STATUS_ERROR = -1;
private static final int MAIN_STATUS_STOPPED = 0;
private static final int MAIN_STATUS_STARTED = 1;
Thread thread;
private static final int MSG_SEND_STATUS = 0;
private static final int MSG_SEND_LOG = 1;
TextView textView;
private Thread mThread = null;
private int mStatus = MAIN_STATUS_STOPPED;
private boolean mAbort = false;
private String mError = null;
private final RemoteCallbackList<IMainCallback> mCallbacks = new RemoteCallbackList<IMainCallback>();
private final IBinder mBinder = new MainStub(this);
private PowerManager.WakeLock mWakelock = null;
final Handler quitHandler = new Handler() {
public void handleMessage(Message msg) {
textView.setText("Music Player Daemon has quit");
static class MainStub extends IMain.Stub {
private Main mService;
MainStub(Main service) {
mService = service;
}
public void start() {
mService.start();
}
public void stop() {
mService.stop();
}
public void setWakelockEnabled(boolean enabled) {
mService.setWakelockEnabled(enabled);
}
public boolean isRunning() {
return mService.isRunning();
}
public void registerCallback(IMainCallback cb) {
mService.registerCallback(cb);
}
public void unregisterCallback(IMainCallback cb) {
mService.unregisterCallback(cb);
}
}
// TODO: what now? restart?
private synchronized void sendMessage(int what, int arg1, int arg2, Object obj) {
int i = mCallbacks.beginBroadcast();
while (i > 0) {
i--;
final IMainCallback cb = mCallbacks.getBroadcastItem(i);
try {
switch (what) {
case MSG_SEND_STATUS:
switch (arg1) {
case MAIN_STATUS_ERROR:
cb.onError((String)obj);
break;
case MAIN_STATUS_STOPPED:
cb.onStopped();
break;
case MAIN_STATUS_STARTED:
cb.onStarted();
break;
}
break;
case MSG_SEND_LOG:
cb.onLog(arg1, (String) obj);
break;
}
} catch (RemoteException e) {
}
}
mCallbacks.finishBroadcast();
}
private Bridge.LogListener mLogListener = new Bridge.LogListener() {
@Override
public void onLog(int priority, String msg) {
sendMessage(MSG_SEND_LOG, priority, 0, msg);
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
start();
if (intent != null && intent.getBooleanExtra("wakelock", false))
setWakelockEnabled(true);
return START_STICKY;
}
@Override
public void run() {
if (!Loader.loaded) {
final String error = "Failed to load the native MPD libary.\n" +
"Report this problem to us, and include the following information:\n" +
"SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" +
"PRODUCT=" + Build.PRODUCT + "\n" +
"FINGERPRINT=" + Build.FINGERPRINT + "\n" +
"error=" + Loader.error;
setStatus(MAIN_STATUS_ERROR, error);
stopSelf();
return;
}
synchronized (this) {
if (mAbort)
return;
setStatus(MAIN_STATUS_STARTED, null);
}
Bridge.run(this, mLogListener);
setStatus(MAIN_STATUS_STOPPED, null);
}
private synchronized void setStatus(int status, String error) {
mStatus = status;
mError = error;
sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
}
private void start() {
if (mThread != null)
return;
mThread = new Thread(this);
mThread.start();
final Intent mainIntent = new Intent(this, Settings.class);
mainIntent.setAction("android.intent.action.MAIN");
mainIntent.addCategory("android.intent.category.LAUNCHER");
final PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
mainIntent, PendingIntent.FLAG_CANCEL_CURRENT);
Notification notification = new Notification.Builder(this)
.setContentTitle(getText(R.string.notification_title_mpd_running))
.setContentText(getText(R.string.notification_text_mpd_running))
.setSmallIcon(R.drawable.notification_icon)
.setContentIntent(contentIntent)
.build();
startForeground(R.string.notification_title_mpd_running, notification);
startService(new Intent(this, Main.class));
}
private void stop() {
if (mThread != null) {
if (mThread.isAlive()) {
synchronized (this) {
if (mStatus == MAIN_STATUS_STARTED)
Bridge.shutdown();
else
mAbort = true;
}
}
try {
mThread.join();
mThread = null;
mAbort = false;
} catch (InterruptedException ie) {}
}
setWakelockEnabled(false);
stopForeground(true);
stopSelf();
}
private void setWakelockEnabled(boolean enabled) {
if (enabled && mWakelock == null) {
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
mWakelock.acquire();
Log.d(TAG, "Wakelock acquired");
} else if (!enabled && mWakelock != null) {
mWakelock.release();
mWakelock = null;
Log.d(TAG, "Wakelock released");
}
}
private boolean isRunning() {
return mThread != null && mThread.isAlive();
}
private void registerCallback(IMainCallback cb) {
if (cb != null) {
mCallbacks.register(cb);
sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
}
}
private void unregisterCallback(IMainCallback cb) {
if (cb != null) {
mCallbacks.unregister(cb);
}
}
/*
* Client that bind the Main Service in order to send commands and receive callback
*/
public static class Client {
public interface Callback {
public void onStarted();
public void onStopped();
public void onError(String error);
public void onLog(int priority, String msg);
}
private boolean mBound = false;
private final Context mContext;
private Callback mCallback;
private IMain mIMain = null;
private final IMainCallback.Stub mICallback = new IMainCallback.Stub() {
@Override
public void onStopped() throws RemoteException {
mCallback.onStopped();
}
@Override
public void onStarted() throws RemoteException {
mCallback.onStarted();
}
@Override
public void onError(String error) throws RemoteException {
mCallback.onError(error);
}
@Override
public void onLog(int priority, String msg) throws RemoteException {
mCallback.onLog(priority, msg);
}
};
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
private final ServiceConnection mServiceConnection = new ServiceConnection() {
if (!Loader.loaded) {
TextView tv = new TextView(this);
tv.setText("Failed to load the native MPD libary.\n" +
"Report this problem to us, and include the following information:\n" +
"ABI=" + Build.CPU_ABI + "\n" +
"PRODUCT=" + Build.PRODUCT + "\n" +
"FINGERPRINT=" + Build.FINGERPRINT + "\n" +
"error=" + Loader.error);
setContentView(tv);
return;
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (this) {
mIMain = IMain.Stub.asInterface(service);
try {
if (mCallback != null)
mIMain.registerCallback(mICallback);
} catch (RemoteException e) {
if (mCallback != null)
mCallback.onError(REMOTE_ERROR);
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mCallback != null)
mCallback.onError(REMOTE_ERROR);
}
};
public Client(Context context, Callback cb) throws IllegalArgumentException {
if (context == null)
throw new IllegalArgumentException("Context can't be null");
mContext = context;
mCallback = cb;
mBound = mContext.bindService(new Intent(mContext, Main.class), mServiceConnection, Context.BIND_AUTO_CREATE);
}
if (thread == null || !thread.isAlive()) {
thread = new Thread(this, "NativeMain");
thread.start();
public boolean start() {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.start();
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
textView = new TextView(this);
textView.setText("Music Player Daemon is running"
+ "\nCAUTION: this version is EXPERIMENTAL!");
setContentView(textView);
public boolean stop() {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.stop();
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean setWakelockEnabled(boolean enabled) {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.setWakelockEnabled(enabled);
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
public boolean isRunning() {
synchronized (this) {
if (mIMain != null) {
try {
return mIMain.isRunning();
} catch (RemoteException e) {
}
}
return false;
}
}
public void release() {
if (mBound) {
synchronized (this) {
if (mIMain != null && mICallback != null) {
try {
if (mCallback != null)
mIMain.unregisterCallback(mICallback);
} catch (RemoteException e) {
}
}
}
mBound = false;
mContext.unbindService(mServiceConnection);
}
}
}
@Override public void run() {
Bridge.run(this);
quitHandler.sendMessage(quitHandler.obtainMessage());
/*
* start Main service without any callback
*/
public static void start(Context context, boolean wakelock) {
context.startService(new Intent(context, Main.class).putExtra("wakelock", wakelock));
}
}

41
android/src/Receiver.java Normal file

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2003-2014 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.
*/
package org.musicpd;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d("Receiver", "onReceive: " + intent);
if (intent.getAction() == "android.intent.action.BOOT_COMPLETED") {
if (Settings.Preferences.getBoolean(context,
Settings.Preferences.KEY_RUN_ON_BOOT, false)) {
final boolean wakelock = Settings.Preferences.getBoolean(context,
Settings.Preferences.KEY_WAKELOCK, false);
Main.start(context, wakelock);
}
}
}
}

254
android/src/Settings.java Normal file

@@ -0,0 +1,254 @@
/*
* Copyright 2003-2018 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.
*/
package org.musicpd;
import java.util.LinkedList;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.ToggleButton;
public class Settings extends Activity {
private static final String TAG = "Settings";
private Main.Client mClient;
private TextView mTextStatus;
private ToggleButton mRunButton;
private boolean mFirstRun;
private LinkedList<String> mLogListArray = new LinkedList<String>();
private ListView mLogListView;
private ArrayAdapter<String> mLogListAdapter;
private static final int MAX_LOGS = 500;
private static final int MSG_ERROR = 0;
private static final int MSG_STOPPED = 1;
private static final int MSG_STARTED = 2;
private static final int MSG_LOG = 3;
public static class Preferences {
public static final String KEY_RUN_ON_BOOT ="run_on_boot";
public static final String KEY_WAKELOCK ="wakelock";
public static SharedPreferences get(Context context) {
return context.getSharedPreferences(TAG, MODE_PRIVATE);
}
public static void putBoolean(Context context, String key, boolean value) {
final SharedPreferences prefs = get(context);
if (prefs == null)
return;
final Editor editor = prefs.edit();
editor.putBoolean(key, value);
editor.apply();
}
public static boolean getBoolean(Context context, String key, boolean defValue) {
final SharedPreferences prefs = get(context);
return prefs != null ? prefs.getBoolean(key, defValue) : defValue;
}
}
private Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_ERROR:
Log.d(TAG, "onError");
mClient.release();
connectClient();
mRunButton.setEnabled(false);
mRunButton.setChecked(false);
mTextStatus.setText((String)msg.obj);
mFirstRun = true;
break;
case MSG_STOPPED:
Log.d(TAG, "onStopped");
mRunButton.setEnabled(true);
if (!mFirstRun && Preferences.getBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, false))
mRunButton.setChecked(true);
else
mRunButton.setChecked(false);
mFirstRun = true;
break;
case MSG_STARTED:
Log.d(TAG, "onStarted");
mRunButton.setChecked(true);
mFirstRun = true;
mTextStatus.setText("CAUTION: this version is EXPERIMENTAL!"); // XXX
break;
case MSG_LOG:
if (mLogListArray.size() > MAX_LOGS)
mLogListArray.remove(0);
String priority;
switch (msg.arg1) {
case Log.DEBUG:
priority = "D";
break;
case Log.ERROR:
priority = "E";
break;
case Log.INFO:
priority = "I";
break;
case Log.VERBOSE:
priority = "V";
break;
case Log.WARN:
priority = "W";
break;
default:
priority = "";
}
mLogListArray.add(priority + "/ " + (String)msg.obj);
mLogListAdapter.notifyDataSetChanged();
break;
}
return true;
}
});
private final OnCheckedChangeListener mOnRunChangeListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (mClient != null) {
if (isChecked) {
mClient.start();
if (Preferences.getBoolean(Settings.this,
Preferences.KEY_WAKELOCK, false))
mClient.setWakelockEnabled(true);
} else {
mClient.stop();
}
}
}
};
private final OnCheckedChangeListener mOnRunOnBootChangeListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Preferences.putBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, isChecked);
if (isChecked && mClient != null && !mRunButton.isChecked())
mRunButton.setChecked(true);
}
};
private final OnCheckedChangeListener mOnWakelockChangeListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Preferences.putBoolean(Settings.this, Preferences.KEY_WAKELOCK, isChecked);
if (mClient != null && mClient.isRunning())
mClient.setWakelockEnabled(isChecked);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.settings);
mRunButton = (ToggleButton) findViewById(R.id.run);
mRunButton.setOnCheckedChangeListener(mOnRunChangeListener);
mTextStatus = (TextView) findViewById(R.id.status);
mLogListAdapter = new ArrayAdapter<String>(this, R.layout.log_item, mLogListArray);
mLogListView = (ListView) findViewById(R.id.log_list);
mLogListView.setAdapter(mLogListAdapter);
mLogListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
CheckBox checkbox = (CheckBox) findViewById(R.id.run_on_boot);
checkbox.setOnCheckedChangeListener(mOnRunOnBootChangeListener);
if (Preferences.getBoolean(this, Preferences.KEY_RUN_ON_BOOT, false))
checkbox.setChecked(true);
checkbox = (CheckBox) findViewById(R.id.wakelock);
checkbox.setOnCheckedChangeListener(mOnWakelockChangeListener);
if (Preferences.getBoolean(this, Preferences.KEY_WAKELOCK, false))
checkbox.setChecked(true);
super.onCreate(savedInstanceState);
}
private void connectClient() {
mClient = new Main.Client(this, new Main.Client.Callback() {
private void removeMessages() {
/* don't remove log messages */
mHandler.removeMessages(MSG_STOPPED);
mHandler.removeMessages(MSG_STARTED);
mHandler.removeMessages(MSG_ERROR);
}
@Override
public void onStopped() {
removeMessages();
mHandler.sendEmptyMessage(MSG_STOPPED);
}
@Override
public void onStarted() {
removeMessages();
mHandler.sendEmptyMessage(MSG_STARTED);
}
@Override
public void onError(String error) {
removeMessages();
mHandler.sendMessage(Message.obtain(mHandler, MSG_ERROR, error));
}
@Override
public void onLog(int priority, String msg) {
mHandler.sendMessage(Message.obtain(mHandler, MSG_LOG, priority, 0, msg));
}
});
}
@Override
protected void onStart() {
mFirstRun = false;
connectClient();
super.onStart();
}
@Override
protected void onStop() {
mClient.release();
mClient = null;
super.onStop();
}
}

@@ -1,10 +1,10 @@
AC_PREREQ(2.60)
AC_INIT(mpd, 0.20.20, musicpd-dev-team@lists.sourceforge.net)
AC_INIT(mpd, 0.20.22, musicpd-dev-team@lists.sourceforge.net)
VERSION_MAJOR=0
VERSION_MINOR=20
VERSION_REVISION=20
VERSION_REVISION=22
VERSION_EXTRA=0
AC_CONFIG_SRCDIR([src/Main.cxx])
@@ -14,9 +14,9 @@ AM_SILENT_RULES
AC_CONFIG_HEADERS(config.h)
AC_CONFIG_MACRO_DIR([m4])
AC_DEFINE(PROTOCOL_VERSION, "0.20.0", [The MPD protocol version])
AC_DEFINE(PROTOCOL_VERSION, "0.20.22", [The MPD protocol version])
GIT_COMMIT=`GIT_DIR="$srcdir/.git" git describe --dirty --always 2>/dev/null`
GIT_COMMIT=`cd "$srcdir" && git describe --dirty --always 2>/dev/null`
if test x$GIT_COMMIT != x; then
AC_DEFINE_UNQUOTED(GIT_COMMIT, ["$GIT_COMMIT"], [The current git commit])
fi
@@ -186,6 +186,7 @@ AC_ARG_WITH([android-sdk],
[Directory for Android SDK]),
[], [with_android_sdk=no])
android_abi=""
if test x$host_is_android = xyes; then
if test x$with_android_sdk = xno; then
AC_MSG_ERROR([Android build requires option --with-android-sdk=DIR])
@@ -194,9 +195,15 @@ if test x$host_is_android = xyes; then
if ! test -x $with_android_sdk/tools/android; then
AC_MSG_ERROR([Android SDK not found in $with_android_sdk])
fi
AS_CASE([$host_cpu],
[i686], [android_abi="x86"],
[aarch64], [android_abi="arm64-v8a"],
[android_abi="armeabi-v7a"])
fi
AC_SUBST(ANDROID_SDK, [$with_android_sdk])
AC_SUBST(ANDROID_ABI, [$android_abi])
dnl ---------------------------------------------------------------------------
dnl Language Checks
@@ -315,7 +322,7 @@ else
fi
default_enable_daemon=yes
if test x$host_is_android = xyes || test x$host_is_android = xyes; then
if test x$host_is_android = xyes || test x$host_is_windows = xyes; then
default_enable_daemon=no
fi
AC_ARG_ENABLE(daemon,
@@ -402,7 +409,7 @@ AC_ARG_ENABLE(recorder-output,
AC_ARG_ENABLE(sidplay,
AS_HELP_STRING([--enable-sidplay],
[enable C64 SID support via libsidplay2]),,
[enable C64 SID support via libsidplayfp or libsidplay2]),,
enable_sidplay=auto)
AC_ARG_ENABLE(shout,
@@ -700,7 +707,7 @@ MPD_ENABLE_AUTO_PKG_LIB(smbclient, SMBCLIENT, [smbclient >= 0.2],
[smbclient input plugin], [libsmbclient not found])
dnl ----------------------------------- NFS -----------------------------
MPD_ENABLE_AUTO_PKG(nfs, NFS, [libnfs],
MPD_ENABLE_AUTO_PKG(nfs, NFS, [libnfs >= 1.9.5],
[NFS input plugin], [libnfs not found])
dnl --------------------------------- Soundcloud ------------------------------
@@ -1005,7 +1012,7 @@ if test x$enable_sidplay != xno && test x$found_sidplayfp = xno; then
[found_sidplay=no])
MPD_AUTO_PRE(sidplay, [sidplay decoder plugin],
[libsidplay2 not found])
[libsidplay2 or libsidutils not found])
fi
if test x$enable_sidplay != xno && test x$found_sidplayfp = xno; then
@@ -1017,10 +1024,11 @@ if test x$enable_sidplay != xno && test x$found_sidplayfp = xno; then
fi
if test x$enable_sidplay = xyes; then
SIDPLAY_LIBS="$SIDPLAY_LIBS -lresid-builder"
AC_DEFINE(ENABLE_SIDPLAY, 1, [Define for libsidplay2 support])
AC_DEFINE(ENABLE_SIDPLAY, 1, [Define for libsidplayfp or libsidplay2 support])
if test x$found_sidplayfp = xyes; then
AC_DEFINE(HAVE_SIDPLAYFP, 1, [Define if libsidplayfp is used instead of libsidplay2])
else
SIDPLAY_LIBS="$SIDPLAY_LIBS -lresid-builder"
fi
fi

@@ -1599,6 +1599,11 @@ OK
per-artist counts:
</para>
<programlisting>count group artist</programlisting>
<para>
A group with an empty value contains counts of matching
song which don't this group tag. It exists only if at
least one such song is found.
</para>
</listitem>
</varlistentry>

@@ -2087,13 +2087,6 @@ run</programlisting>
database.
</para>
<para>
Note that unless overridden by the below settings (e.g. by
setting them to a blank value), general curl configuration
from environment variables such as http_proxy or specified
in ~/.curlrc will be in effect.
</para>
<informaltable>
<tgroup cols="2">
<thead>
@@ -2121,6 +2114,15 @@ run</programlisting>
<application>MPD</application> instance.
</entry>
</row>
<row>
<entry>
<varname>password</varname>
</entry>
<entry>
The password used to log in to the "master"
<application>MPD</application> instance.
</entry>
</row>
<row>
<entry>
<varname>keepalive</varname>

@@ -21,6 +21,8 @@ class FfmpegProject(Project):
if toolchain.is_arm:
arch = 'arm'
elif toolchain.is_aarch64:
arch = 'aarch64'
else:
arch = 'x86'

@@ -9,8 +9,8 @@ from build.ffmpeg import FfmpegProject
from build.boost import BoostProject
libmpdclient = MesonProject(
'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.14.tar.xz',
'0a84e2791bfe3077cf22ee1784c805d5bb550803dffe56a39aa3690a38061372',
'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.16.tar.xz',
'fa6bdab67c0e0490302b38f00c27b4959735c3ec8aef7a88327adb1407654464',
'lib/libmpdclient.a',
)
@@ -38,8 +38,8 @@ libvorbis = AutotoolsProject(
)
opus = AutotoolsProject(
'https://archive.mozilla.org/pub/opus/opus-1.2.1.tar.gz',
'cfafd339ccd9c5ef8d6ab15d7e1a412c054bf4cb4ecbbbcc78c12ef2def70732',
'https://archive.mozilla.org/pub/opus/opus-1.3.tar.gz',
'4f3d69aefdf2dbaf9825408e452a8a414ffc60494c70633560700398820dc550',
'lib/libopus.a',
[
'--disable-shared', '--enable-static',
@@ -112,8 +112,8 @@ liblame = AutotoolsProject(
)
ffmpeg = FfmpegProject(
'http://ffmpeg.org/releases/ffmpeg-4.0.tar.xz',
'ed945daf40b124e77a685893cc025d086f638bc703183460aff49508edb3a43f',
'http://ffmpeg.org/releases/ffmpeg-4.0.2.tar.xz',
'a95c0cc9eb990e94031d2183f2e6e444cc61c99f6f182d1575c433d62afb2f97',
'lib/libavcodec.a',
[
'--disable-shared', '--enable-static',
@@ -341,8 +341,8 @@ ffmpeg = FfmpegProject(
)
curl = AutotoolsProject(
'http://curl.haxx.se/download/curl-7.60.0.tar.xz',
'8736ff8ded89ddf7e926eec7b16f82597d029fc1469f3a551f1fafaac164e6a0',
'http://curl.haxx.se/download/curl-7.61.1.tar.xz',
'3d5913d6a39bd22e68e34dff697fd6e4c3c81563f580c76fca2009315cd81891',
'lib/libcurl.a',
[
'--disable-shared', '--enable-static',
@@ -364,9 +364,19 @@ curl = AutotoolsProject(
patches='src/lib/curl/patches',
)
libexpat = AutotoolsProject(
'https://github.com/libexpat/libexpat/releases/download/R_2_2_6/expat-2.2.6.tar.bz2',
'17b43c2716d521369f82fc2dc70f359860e90fa440bea65b3b85f0b246ea81f2',
'lib/libexpat.a',
[
'--disable-shared', '--enable-static',
'--without-docbook',
],
)
libnfs = AutotoolsProject(
'https://github.com/sahlberg/libnfs/archive/libnfs-2.0.0.tar.gz',
'7ea6cd8fa6c461d01091e584d424d28e137d23ff4b65b95d01a3fd0ef95d120e',
'https://github.com/sahlberg/libnfs/archive/libnfs-3.0.0.tar.gz',
'445d92c5fc55e4a5b115e358e60486cf8f87ee50e0103d46a02e7fb4618566a5',
'lib/libnfs.a',
[
'--disable-shared', '--enable-static',
@@ -374,13 +384,15 @@ libnfs = AutotoolsProject(
# work around -Wtautological-compare
'--disable-werror',
'--disable-utils', '--disable-examples',
],
base='libnfs-libnfs-2.0.0',
base='libnfs-libnfs-3.0.0',
autoreconf=True,
)
boost = BoostProject(
'http://downloads.sourceforge.net/project/boost/boost/1.66.0/boost_1_66_0.tar.bz2',
'5721818253e6a0989583192f96782c4a98eb6204965316df9f5ad75819225ca9',
'http://downloads.sourceforge.net/project/boost/boost/1.68.0/boost_1_68_0.tar.bz2',
'7f6130bc3cf65f56a618888ce9d5ea704fa10b462be126ad053e80e553d6d8b7',
'include/boost/version.hpp',
)

@@ -20,6 +20,9 @@ class MesonProject(Project):
cpu = 'armv7'
else:
cpu = 'armv6'
elif toolchain.is_aarch64:
cpu_family = 'aarch64'
cpu = 'arm64-v8a'
else:
cpu_family = 'x86'
if 'x86_64' in toolchain.arch:
@@ -51,6 +54,9 @@ c_link_args = %s
cpp_args = %s
cpp_link_args = %s
# Keep Meson from executing Android-x86 test binariees
needs_exe_wrapper = true
[host_machine]
system = '%s'
cpu_family = '%s'

@@ -19,9 +19,9 @@
#include "AudioFormat.hxx"
#include "util/StringBuffer.hxx"
#include "util/StringFormat.hxx"
#include <assert.h>
#include <stdio.h>
void
AudioFormat::ApplyMask(AudioFormat mask) noexcept
@@ -44,21 +44,16 @@ AudioFormat::ApplyMask(AudioFormat mask) noexcept
StringBuffer<24>
ToString(const AudioFormat af) noexcept
{
StringBuffer<24> buffer;
if (af.format == SampleFormat::DSD && af.sample_rate > 0 &&
af.sample_rate % 44100 == 0) {
/* use shortcuts such as "dsd64" which implies the
sample rate */
snprintf(buffer.data(), buffer.capacity(), "dsd%u:%u",
af.sample_rate * 8 / 44100,
af.channels);
return buffer;
return StringFormat<24>("dsd%u:%u",
af.sample_rate * 8 / 44100,
af.channels);
}
snprintf(buffer.data(), buffer.capacity(), "%u:%s:%u",
af.sample_rate, sample_format_to_string(af.format),
af.channels);
return buffer;
return StringFormat<24>("%u:%s:%u",
af.sample_rate, sample_format_to_string(af.format),
af.channels);
}

@@ -23,7 +23,6 @@
#include "pcm/SampleFormat.hxx"
#include "Compiler.h"
#include <assert.h>
#include <stdint.h>
#include <stddef.h>

@@ -23,7 +23,7 @@
#include "fs/AllocatedPath.hxx"
#include "ls.hxx"
#include "util/UriUtil.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
#ifdef ENABLE_DATABASE
#include "storage/StorageInterface.hxx"
@@ -83,7 +83,7 @@ LocateUri(const char *uri, const Client *client
)
{
/* skip the obsolete "file://" prefix */
const char *path_utf8 = StringAfterPrefix(uri, "file://");
const char *path_utf8 = StringAfterPrefixCaseASCII(uri, "file://");
if (path_utf8 != nullptr) {
if (!PathTraitsUTF8::IsAbsolute(path_utf8))
throw std::runtime_error("Malformed file:// URI");

@@ -34,6 +34,8 @@
#ifdef ANDROID
#include <android/log.h>
#include "android/LogListener.hxx"
#include "Main.hxx"
static int
ToAndroidLogLevel(LogLevel log_level)
@@ -177,6 +179,9 @@ Log(const Domain &domain, LogLevel level, const char *msg)
#ifdef ANDROID
__android_log_print(ToAndroidLogLevel(level), "MPD",
"%s: %s", domain.GetName(), msg);
if (logListener != nullptr)
logListener->OnLog(Java::GetEnv(), ToAndroidLogLevel(level),
"%s: %s", domain.GetName(), msg);
#else
if (level < log_threshold)

@@ -92,6 +92,7 @@
#include "java/File.hxx"
#include "android/Environment.hxx"
#include "android/Context.hxx"
#include "android/LogListener.hxx"
#include "fs/StandardDirectory.hxx"
#include "fs/FileSystem.hxx"
#include "org_musicpd_Bridge.h"
@@ -128,6 +129,7 @@ static constexpr unsigned DEFAULT_BUFFER_BEFORE_PLAY = 10;
#ifdef ANDROID
Context *context;
LogListener *logListener;
#endif
Instance *instance;
@@ -676,16 +678,19 @@ try {
gcc_visibility_default
JNIEXPORT void JNICALL
Java_org_musicpd_Bridge_run(JNIEnv *env, jclass, jobject _context)
Java_org_musicpd_Bridge_run(JNIEnv *env, jclass, jobject _context, jobject _logListener)
{
Java::Init(env);
Java::File::Initialise(env);
Environment::Initialise(env);
context = new Context(env, _context);
if (_logListener != nullptr)
logListener = new LogListener(env, _logListener);
mpd_main(0, nullptr);
delete logListener;
delete context;
Environment::Deinitialise(env);
}

@@ -25,7 +25,10 @@ class Context;
struct Instance;
#ifdef ANDROID
#include "android/LogListener.hxx"
extern Context *context;
extern LogListener *logListener;
#endif
extern Instance *instance;

@@ -22,6 +22,7 @@
#include "db/LightSong.hxx"
#include "DetachedSong.hxx"
#include "tag/Tag.hxx"
#include "tag/Fallback.hxx"
#include "util/ConstBuffer.hxx"
#include "util/StringAPI.hxx"
#include "util/StringCompare.hxx"
@@ -109,6 +110,24 @@ SongFilter::Item::Match(const Tag &_tag) const noexcept
}
if (tag < TAG_NUM_OF_ITEM_TYPES && !visited_types[tag]) {
bool result = false;
if (ApplyTagFallback(TagType(tag),
[&](TagType tag2) {
if (!visited_types[tag2])
return false;
for (const auto &item : _tag) {
if (item.type == tag2 &&
StringMatch(item.value)) {
result = true;
break;
}
}
return true;
}))
return result;
/* If the search critieron was not visited during the
sweep through the song's tag, it means this field
is absent from the tag or empty. Thus, if the
@@ -117,15 +136,6 @@ SongFilter::Item::Match(const Tag &_tag) const noexcept
true. */
if (value.empty())
return true;
if (tag == TAG_ALBUM_ARTIST && visited_types[TAG_ARTIST]) {
/* if we're looking for "album artist", but
only "artist" exists, use that */
for (const auto &item : _tag)
if (item.type == TAG_ARTIST &&
StringMatch(item.value))
return true;
}
}
return false;

@@ -0,0 +1,46 @@
/*
* Copyright 2003-2018 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 "config.h"
#include "LogListener.hxx"
#include "java/Class.hxx"
#include "java/String.hxx"
#include "util/AllocatedString.hxx"
#include "util/FormatString.hxx"
void
LogListener::OnLog(JNIEnv *env, int priority, const char *fmt, ...) const
{
assert(env != nullptr);
Java::Class cls(env, env->GetObjectClass(Get()));
jmethodID method = env->GetMethodID(cls, "onLog",
"(ILjava/lang/String;)V");
assert(method);
va_list args;
va_start(args, fmt);
const auto log = FormatStringV(fmt, args);
va_end(args);
env->CallVoidMethod(Get(), method, priority,
Java::String(env, log.c_str()).Get());
}

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2017 The Music Player Daemon Project
* Copyright 2003-2018 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -17,11 +17,16 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef MPD_INPUT_DOMAIN_HXX
#define MPD_INPUT_DOMAIN_HXX
#ifndef MPD_ANDROID_LOG_LISTENER_HXX
#define MPD_ANDROID_LOG_LISTENER_HXX
class Domain;
#include "java/Object.hxx"
extern const Domain input_domain;
class LogListener : public Java::Object {
public:
LogListener(JNIEnv *env, jobject obj):Java::Object(env, obj) {}
void OnLog(JNIEnv *env, int priority, const char *fmt, ...) const;
};
#endif

@@ -191,7 +191,7 @@ handle_list(Client &client, Request args, Response &r)
}
std::unique_ptr<SongFilter> filter;
tag_mask_t group_mask = 0;
TagType group = TAG_NUM_OF_ITEM_TYPES;
if (args.size == 1) {
/* for compatibility with < 0.12.0 */
@@ -206,18 +206,16 @@ handle_list(Client &client, Request args, Response &r)
args.shift()));
}
while (args.size >= 2 &&
StringIsEqual(args[args.size - 2], "group")) {
if (args.size >= 2 &&
StringIsEqual(args[args.size - 2], "group")) {
const char *s = args[args.size - 1];
TagType gt = tag_name_parse_i(s);
if (gt == TAG_NUM_OF_ITEM_TYPES) {
group = tag_name_parse_i(s);
if (group == TAG_NUM_OF_ITEM_TYPES) {
r.FormatError(ACK_ERROR_ARG,
"Unknown tag type: %s", s);
return CommandResult::ERROR;
}
group_mask |= tag_mask_t(1) << unsigned(gt);
args.pop_back();
args.pop_back();
}
@@ -230,14 +228,13 @@ handle_list(Client &client, Request args, Response &r)
}
}
if (tagType < TAG_NUM_OF_ITEM_TYPES &&
group_mask & (tag_mask_t(1) << tagType)) {
if (tagType < TAG_NUM_OF_ITEM_TYPES && tagType == group) {
r.Error(ACK_ERROR_ARG, "Conflicting group");
return CommandResult::ERROR;
}
PrintUniqueTags(r, client.partition,
tagType, group_mask, filter.get());
tagType, group, filter.get());
return CommandResult::OK;
}

@@ -25,6 +25,7 @@
#include "client/Response.hxx"
#include "LightSong.hxx"
#include "tag/Tag.hxx"
#include "tag/VisitFallback.hxx"
#include <functional>
#include <map>
@@ -72,24 +73,15 @@ stats_visitor_song(SearchStats &stats, const LightSong &song)
stats.total_duration += duration;
}
static bool
CollectGroupCounts(TagCountMap &map, TagType group, const Tag &tag)
static void
CollectGroupCounts(TagCountMap &map, const Tag &tag,
const char *value) noexcept
{
bool found = false;
for (const auto &item : tag) {
if (item.type == group) {
auto r = map.insert(std::make_pair(item.value,
SearchStats()));
SearchStats &s = r.first->second;
++s.n_songs;
if (!tag.duration.IsNegative())
s.total_duration += tag.duration;
found = true;
}
}
return found;
auto r = map.insert(std::make_pair(value, SearchStats()));
SearchStats &s = r.first->second;
++s.n_songs;
if (!tag.duration.IsNegative())
s.total_duration += tag.duration;
}
static void
@@ -98,9 +90,10 @@ GroupCountVisitor(TagCountMap &map, TagType group, const LightSong &song)
assert(song.tag != nullptr);
const Tag &tag = *song.tag;
if (!CollectGroupCounts(map, group, tag) && group == TAG_ALBUM_ARTIST)
/* fall back to "Artist" if no "AlbumArtist" was found */
CollectGroupCounts(map, TAG_ARTIST, tag);
VisitTagWithFallbackOrEmpty(tag, group,
std::bind(CollectGroupCounts, std::ref(map),
std::cref(tag),
std::placeholders::_1));
}
void

@@ -187,22 +187,34 @@ PrintSongURIVisitor(Response &r, Partition &partition, const LightSong &song)
}
static void
PrintUniqueTag(Response &r, TagType tag_type,
const Tag &tag)
PrintUniqueTags(Response &r, TagType tag_type,
const std::set<std::string> &values)
{
const char *value = tag.GetValue(tag_type);
assert(value != nullptr);
r.Format("%s: %s\n", tag_item_names[tag_type], value);
const char *const name = tag_item_names[tag_type];
for (const auto &i : values)
r.Format("%s: %s\n", name, i.c_str());
}
for (const auto &item : tag)
if (item.type != tag_type)
r.Format("%s: %s\n",
tag_item_names[item.type], item.value);
static void
PrintGroupedUniqueTags(Response &r, TagType tag_type, TagType group,
const std::map<std::string, std::set<std::string>> &groups)
{
if (group == TAG_NUM_OF_ITEM_TYPES) {
for (const auto &i : groups)
PrintUniqueTags(r, tag_type, i.second);
return;
}
const char *const group_name = tag_item_names[group];
for (const auto &i : groups) {
r.Format("%s: %s\n", group_name, i.first.c_str());
PrintUniqueTags(r, tag_type, i.second);
}
}
void
PrintUniqueTags(Response &r, Partition &partition,
unsigned type, tag_mask_t group_mask,
unsigned type, TagType group,
const SongFilter *filter)
{
const Database &db = partition.GetDatabaseOrThrow();
@@ -217,10 +229,9 @@ PrintUniqueTags(Response &r, Partition &partition,
} else {
assert(type < TAG_NUM_OF_ITEM_TYPES);
using namespace std::placeholders;
const auto f = std::bind(PrintUniqueTag, std::ref(r),
(TagType)type, _1);
db.VisitUniqueTags(selection, (TagType)type,
group_mask, f);
PrintGroupedUniqueTags(r, TagType(type), group,
db.CollectUniqueTags(selection,
TagType(type),
group));
}
}

@@ -20,7 +20,7 @@
#ifndef MPD_DB_PRINT_H
#define MPD_DB_PRINT_H
#include "tag/Mask.hxx"
#include "tag/TagType.h"
class SongFilter;
struct DatabaseSelection;
@@ -44,7 +44,7 @@ db_selection_print(Response &r, Partition &partition,
void
PrintUniqueTags(Response &r, Partition &partition,
unsigned type, tag_mask_t group_mask,
unsigned type, TagType group,
const SongFilter *filter);
#endif

@@ -22,9 +22,12 @@
#include "Visitor.hxx"
#include "tag/TagType.h"
#include "tag/Mask.hxx"
#include "Compiler.h"
#include <map>
#include <set>
#include <string>
#include <time.h>
struct DatabasePlugin;
@@ -99,12 +102,9 @@ public:
return Visit(selection, VisitDirectory(), visit_song);
}
/**
* Visit all unique tag values.
*/
virtual void VisitUniqueTags(const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask,
VisitTag visit_tag) const = 0;
virtual std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group=TAG_NUM_OF_ITEM_TYPES) const = 0;
virtual DatabaseStats GetStats(const DatabaseSelection &selection) const = 0;

@@ -20,34 +20,42 @@
#include "UniqueTags.hxx"
#include "Interface.hxx"
#include "LightSong.hxx"
#include "tag/Set.hxx"
#include "tag/VisitFallback.hxx"
#include <functional>
#include <assert.h>
static void
CollectTags(TagSet &set, TagType tag_type, tag_mask_t group_mask,
const LightSong &song)
CollectTags(std::set<std::string> &result,
const Tag &tag,
TagType tag_type) noexcept
{
assert(song.tag != nullptr);
const Tag &tag = *song.tag;
set.InsertUnique(tag, tag_type, group_mask);
VisitTagWithFallbackOrEmpty(tag, tag_type, [&result](const char *value){
result.emplace(value);
});
}
void
VisitUniqueTags(const Database &db, const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask,
VisitTag visit_tag)
static void
CollectGroupTags(std::map<std::string, std::set<std::string>> &result,
const Tag &tag,
TagType tag_type,
TagType group) noexcept
{
TagSet set;
using namespace std::placeholders;
const auto f = std::bind(CollectTags, std::ref(set),
tag_type, group_mask, _1);
db.Visit(selection, f);
for (const auto &value : set)
visit_tag(value);
VisitTagWithFallbackOrEmpty(tag, group, [&](const char *group_name){
CollectTags(result[group_name], tag, tag_type);
});
}
std::map<std::string, std::set<std::string>>
CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
TagType tag_type, TagType group)
{
std::map<std::string, std::set<std::string>> result;
db.Visit(selection, [&result, tag_type, group](const LightSong &song){
CollectGroupTags(result, *song.tag, tag_type, group);
});
return result;
}

@@ -20,16 +20,19 @@
#ifndef MPD_DB_UNIQUE_TAGS_HXX
#define MPD_DB_UNIQUE_TAGS_HXX
#include "Visitor.hxx"
#include "tag/TagType.h"
#include "tag/Mask.hxx"
#include "Compiler.h"
#include <map>
#include <set>
#include <string>
class Database;
struct DatabaseSelection;
void
VisitUniqueTags(const Database &db, const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask,
VisitTag visit_tag);
gcc_pure
std::map<std::string, std::set<std::string>>
CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
TagType tag_type, TagType group);
#endif

@@ -82,6 +82,7 @@ class ProxyDatabase final : public Database, SocketMonitor, IdleMonitor {
DatabaseListener &listener;
const std::string host;
const std::string password;
const unsigned port;
const bool keepalive;
@@ -119,9 +120,9 @@ public:
VisitSong visit_song,
VisitPlaylist visit_playlist) const override;
void VisitUniqueTags(const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask,
VisitTag visit_tag) const override;
std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group) const override;
DatabaseStats GetStats(const DatabaseSelection &selection) const override;
@@ -169,6 +170,13 @@ static constexpr struct {
#if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
{ TAG_MUSICBRAINZ_RELEASETRACKID,
MPD_TAG_MUSICBRAINZ_RELEASETRACKID },
#endif
#if LIBMPDCLIENT_CHECK_VERSION(2,11,0)
{ TAG_ARTIST_SORT, MPD_TAG_ARTIST_SORT },
{ TAG_ALBUM_ARTIST_SORT, MPD_TAG_ALBUM_ARTIST_SORT },
#endif
#if LIBMPDCLIENT_CHECK_VERSION(2,12,0)
{ TAG_ALBUM_SORT, MPD_TAG_ALBUM_SORT },
#endif
{ TAG_NUM_OF_ITEM_TYPES, MPD_TAG_COUNT }
};
@@ -326,28 +334,19 @@ SendConstraints(mpd_connection *connection, const DatabaseSelection &selection)
}
static bool
SendGroupMask(mpd_connection *connection, tag_mask_t mask)
SendGroup(mpd_connection *connection, TagType group)
{
if (group == TAG_NUM_OF_ITEM_TYPES)
return true;
#if LIBMPDCLIENT_CHECK_VERSION(2,12,0)
for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) {
if ((mask & (tag_mask_t(1) << i)) == 0)
continue;
const auto tag = Convert(group);
if (tag == MPD_TAG_COUNT)
throw std::runtime_error("Unsupported tag");
const auto tag = Convert(TagType(i));
if (tag == MPD_TAG_COUNT)
throw std::runtime_error("Unsupported tag");
if (!mpd_search_add_group_tag(connection, tag))
return false;
}
return true;
return mpd_search_add_group_tag(connection, tag);
#else
(void)connection;
(void)mask;
if (mask != 0)
throw std::runtime_error("Grouping requires libmpdclient 2.12");
return true;
#endif
@@ -359,6 +358,7 @@ ProxyDatabase::ProxyDatabase(EventLoop &_loop, DatabaseListener &_listener,
SocketMonitor(_loop), IdleMonitor(_loop),
listener(_listener),
host(block.GetBlockValue("host", "")),
password(block.GetBlockValue("password", "")),
port(block.GetBlockValue("port", 0u)),
keepalive(block.GetBlockValue("keepalive", false))
{
@@ -402,6 +402,10 @@ ProxyDatabase::Connect()
try {
CheckError(connection);
if (!password.empty() &&
!mpd_run_password(connection, password.c_str()))
ThrowError(connection);
} catch (...) {
mpd_connection_free(connection);
connection = nullptr;
@@ -786,11 +790,9 @@ ProxyDatabase::Visit(const DatabaseSelection &selection,
visit_directory, visit_song, visit_playlist);
}
void
ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
tag_mask_t group_mask,
VisitTag visit_tag) const
std::map<std::string, std::set<std::string>>
ProxyDatabase::CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type, TagType group) const
try {
// TODO: eliminate the const_cast
const_cast<ProxyDatabase *>(this)->EnsureConnected();
@@ -801,54 +803,56 @@ try {
if (!mpd_search_db_tags(connection, tag_type2) ||
!SendConstraints(connection, selection) ||
!SendGroupMask(connection, group_mask))
!SendGroup(connection, group))
ThrowError(connection);
if (!mpd_search_commit(connection))
ThrowError(connection);
TagBuilder builder;
std::map<std::string, std::set<std::string>> result;
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
if (group == TAG_NUM_OF_ITEM_TYPES) {
auto &values = result[std::string()];
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
if (current_type == tag_type && !builder.IsEmpty()) {
try {
visit_tag(builder.Commit());
} catch (...) {
mpd_response_finish(connection);
throw;
}
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
if (current_type == tag_type)
values.emplace(pair->value);
}
} else {
std::set<std::string> *current_group = nullptr;
builder.AddItem(current_type, pair->value);
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
if (!builder.HasType(current_type))
/* if no tag item has been added, then the
given value was not acceptable
(e.g. empty); forcefully insert an empty
tag in this case, as the caller expects the
given tag type to be present */
builder.AddEmptyItem(current_type);
}
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
if (!builder.IsEmpty()) {
try {
visit_tag(builder.Commit());
} catch (...) {
mpd_response_finish(connection);
throw;
if (current_type == tag_type) {
if (current_group == nullptr)
current_group = &result[std::string()];
current_group->emplace(pair->value);
} else if (current_type == group) {
current_group = &result[pair->value];
}
}
}
if (!mpd_response_finish(connection))
ThrowError(connection);
return result;
} catch (...) {
if (connection != nullptr)
mpd_search_cancel(connection);

@@ -312,12 +312,11 @@ SimpleDatabase::Visit(const DatabaseSelection &selection,
"No such directory");
}
void
SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask,
VisitTag visit_tag) const
std::map<std::string, std::set<std::string>>
SimpleDatabase::CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type, TagType group) const
{
::VisitUniqueTags(*this, selection, tag_type, group_mask, visit_tag);
return ::CollectUniqueTags(*this, selection, tag_type, group);
}
DatabaseStats

@@ -119,9 +119,9 @@ public:
VisitSong visit_song,
VisitPlaylist visit_playlist) const override;
void VisitUniqueTags(const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask,
VisitTag visit_tag) const override;
std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group) const override;
DatabaseStats GetStats(const DatabaseSelection &selection) const override;

@@ -27,6 +27,7 @@
#include "util/UriUtil.hxx"
#include "util/RuntimeError.hxx"
#include "util/ScopeExit.hxx"
#include "util/StringFormat.hxx"
#include <stdio.h>
@@ -47,10 +48,6 @@ ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl,
unsigned &didreadp,
unsigned &totalp) const
{
// Create request
char ofbuf[100], cntbuf[100];
sprintf(ofbuf, "%u", offset);
sprintf(cntbuf, "%u", count);
// Some devices require an empty SortCriteria, else bad params
IXML_Document *request =
MakeActionHelper("Browse", m_serviceType.c_str(),
@@ -58,8 +55,10 @@ ContentDirectoryService::readDirSlice(UpnpClient_Handle hdl,
"BrowseFlag", "BrowseDirectChildren",
"Filter", "*",
"SortCriteria", "",
"StartingIndex", ofbuf,
"RequestedCount", cntbuf);
"StartingIndex",
StringFormat<32>("%u", offset).c_str(),
"RequestedCount",
StringFormat<32>("%u", count).c_str());
if (request == nullptr)
throw std::runtime_error("UpnpMakeAction() failed");
@@ -112,15 +111,13 @@ ContentDirectoryService::search(UpnpClient_Handle hdl,
unsigned offset = 0, total = -1, count;
do {
char ofbuf[100];
sprintf(ofbuf, "%d", offset);
UniqueIxmlDocument request(MakeActionHelper("Search", m_serviceType.c_str(),
"ContainerID", objectId,
"SearchCriteria", ss,
"Filter", "*",
"SortCriteria", "",
"StartingIndex", ofbuf,
"StartingIndex",
StringFormat<32>("%u", offset).c_str(),
"RequestedCount", "0")); // Setting a value here gets twonky into fits
if (!request)
throw std::runtime_error("UpnpMakeAction() failed");

@@ -87,9 +87,9 @@ public:
VisitSong visit_song,
VisitPlaylist visit_playlist) const override;
void VisitUniqueTags(const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask,
VisitTag visit_tag) const override;
std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type,
TagType group) const override;
DatabaseStats GetStats(const DatabaseSelection &selection) const override;
@@ -603,17 +603,15 @@ UpnpDatabase::Visit(const DatabaseSelection &selection,
visit_directory, visit_song, visit_playlist);
}
void
UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
TagType tag, gcc_unused tag_mask_t group_mask,
VisitTag visit_tag) const
std::map<std::string, std::set<std::string>>
UpnpDatabase::CollectUniqueTags(const DatabaseSelection &selection,
TagType tag, TagType group) const
{
// TODO: use group_mask
(void)group; // TODO: use group
if (!visit_tag)
return;
std::map<std::string, std::set<std::string>> result;
auto &values = result[std::string()];
std::set<std::string> values;
for (auto& server : discovery->GetDirectories()) {
const auto dirbuf = SearchSongs(server, rootid, selection);
@@ -633,11 +631,7 @@ UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
}
}
for (const auto& value : values) {
TagBuilder builder;
builder.AddItem(tag, value.c_str());
visit_tag(builder.Commit());
}
return result;
}
DatabaseStats

@@ -26,8 +26,8 @@
#include "ExcludeList.hxx"
#include "fs/Path.hxx"
#include "fs/NarrowPath.hxx"
#include "fs/io/TextFile.hxx"
#include "system/Error.hxx"
#include "input/TextInputStream.hxx"
#include "util/StringUtil.hxx"
#include "Log.hxx"
#include <stdexcept>
@@ -35,35 +35,29 @@
#include <assert.h>
#include <string.h>
inline void
ExcludeList::ParseLine(char *line) noexcept
{
char *p = Strip(line);
if (*p != 0 && *p != '#')
patterns.emplace_front(p);
}
bool
ExcludeList::LoadFile(Path path_fs) noexcept
try {
ExcludeList::Load(InputStreamPtr is)
{
#ifdef HAVE_CLASS_GLOB
TextFile file(path_fs);
TextInputStream tis(std::move(is));
char *line;
while ((line = file.ReadLine()) != nullptr) {
char *p = strchr(line, '#');
if (p != nullptr)
*p = 0;
p = Strip(line);
if (*p != 0)
patterns.emplace_front(p);
}
while ((line = tis.ReadLine()) != nullptr)
ParseLine(line);
#else
/* not implemented */
(void)path_fs;
#endif
return true;
} catch (const std::system_error &e) {
if (!IsFileNotFound(e))
LogError(e);
return false;
} catch (const std::exception &e) {
LogError(e);
return false;
}
bool

@@ -28,6 +28,7 @@
#include "check.h"
#include "Compiler.h"
#include "fs/Glob.hxx"
#include "input/Ptr.hxx"
#ifdef HAVE_CLASS_GLOB
#include <forward_list>
@@ -62,13 +63,16 @@ public:
/**
* Loads and parses a .mpdignore file.
*/
bool LoadFile(Path path_fs) noexcept;
bool Load(InputStreamPtr is);
/**
* Checks whether one of the patterns in the .mpdignore file matches
* the specified file name.
*/
bool Check(Path name_fs) const noexcept;
private:
void ParseLine(char *line) noexcept;
};

@@ -36,6 +36,9 @@
#include "fs/Traits.hxx"
#include "fs/FileSystem.hxx"
#include "storage/FileInfo.hxx"
#include "input/InputStream.hxx"
#include "input/Error.hxx"
#include "thread/Cond.hxx"
#include "util/Alloc.hxx"
#include "util/StringCompare.hxx"
#include "util/UriUtil.hxx"
@@ -345,11 +348,16 @@ UpdateWalk::UpdateDirectory(Directory &directory,
ExcludeList child_exclude_list(exclude_list);
{
const auto exclude_path_fs =
storage.MapChildFS(directory.GetPath(), ".mpdignore");
if (!exclude_path_fs.IsNull())
child_exclude_list.LoadFile(exclude_path_fs);
try {
Mutex mutex;
Cond cond;
auto is = InputStream::OpenReady(PathTraitsUTF8::Build(storage.MapUTF8(directory.GetPath()).c_str(),
".mpdignore").c_str(),
mutex, cond);
child_exclude_list.Load(std::move(is));
} catch (...) {
if (!IsFileNotFound(std::current_exception()))
LogError(std::current_exception());
}
if (!child_exclude_list.IsEmpty())

@@ -64,7 +64,12 @@ fluidsynth_level_to_mpd(enum fluid_log_level level)
* logging library.
*/
static void
fluidsynth_mpd_log_function(int level, char *message, gcc_unused void *data)
fluidsynth_mpd_log_function(int level,
#if FLUIDSYNTH_VERSION_MAJOR >= 2
const
#endif
char *message,
void *)
{
Log(fluidsynth_domain,
fluidsynth_level_to_mpd(fluid_log_level(level)),

@@ -28,7 +28,7 @@
#include "fs/Path.hxx"
#include "fs/AllocatedPath.hxx"
#include "util/ScopeExit.hxx"
#include "util/FormatString.hxx"
#include "util/StringFormat.hxx"
#include "util/UriUtil.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
@@ -38,7 +38,6 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define SUBTUNE_PREFIX "tune_"
@@ -191,20 +190,17 @@ ScanGmeInfo(const gme_info_t &info, unsigned song_num, int track_count,
tag_handler_invoke_duration(handler, handler_ctx,
SongTime::FromMS(info.play_length));
if (track_count > 1) {
char track[16];
sprintf(track, "%u", song_num + 1);
tag_handler_invoke_tag(handler, handler_ctx, TAG_TRACK, track);
}
if (track_count > 1)
tag_handler_invoke_tag(handler, handler_ctx, TAG_TRACK,
StringFormat<16>("%u", song_num + 1));
if (info.song != nullptr) {
if (track_count > 1) {
/* start numbering subtunes from 1 */
char tag_title[1024];
snprintf(tag_title, sizeof(tag_title),
"%s (%u/%d)",
info.song, song_num + 1,
track_count);
const auto tag_title =
StringFormat<1024>("%s (%u/%d)",
info.song, song_num + 1,
track_count);
tag_handler_invoke_tag(handler, handler_ctx,
TAG_TITLE, tag_title);
} else
@@ -297,9 +293,9 @@ gme_container_scan(Path path_fs)
ScanMusicEmu(emu, i,
add_tag_handler, &tag_builder);
char track_name[64];
snprintf(track_name, sizeof(track_name),
SUBTUNE_PREFIX "%03u.%s", i+1, subtune_suffix);
const auto track_name =
StringFormat<64>(SUBTUNE_PREFIX "%03u.%s", i+1,
subtune_suffix);
tail = list.emplace_after(tail, track_name,
tag_builder.Commit());
}

@@ -26,7 +26,7 @@
#include "fs/Path.hxx"
#include "fs/AllocatedPath.hxx"
#include "util/Macros.hxx"
#include "util/FormatString.hxx"
#include "util/StringFormat.hxx"
#include "util/Domain.hxx"
#include "system/ByteOrder.hxx"
#include "Log.hxx"
@@ -413,10 +413,9 @@ ScanSidTuneInfo(const SidTuneInfo &info, unsigned track, unsigned n_tracks,
title = "";
if (n_tracks > 1) {
char tag_title[1024];
snprintf(tag_title, sizeof(tag_title),
"%s (%u/%u)",
title, track, n_tracks);
const auto tag_title =
StringFormat<1024>("%s (%u/%u)",
title, track, n_tracks);
tag_handler_invoke_tag(handler, handler_ctx,
TAG_TITLE, tag_title);
} else
@@ -435,9 +434,8 @@ ScanSidTuneInfo(const SidTuneInfo &info, unsigned track, unsigned n_tracks,
date);
/* track */
char track_buffer[16];
sprintf(track_buffer, "%d", track);
tag_handler_invoke_tag(handler, handler_ctx, TAG_TRACK, track_buffer);
tag_handler_invoke_tag(handler, handler_ctx, TAG_TRACK,
StringFormat<16>("%u", track));
}
static bool

@@ -20,6 +20,7 @@
#include "config.h"
#include "FileOutputStream.hxx"
#include "system/Error.hxx"
#include "util/StringFormat.hxx"
FileOutputStream::FileOutputStream(Path _path, Mode _mode)
:path(_path), mode(_mode)
@@ -212,10 +213,9 @@ FileOutputStream::Commit()
unlink(GetPath().c_str());
/* hard-link the temporary file to the final path */
char fd_path[64];
snprintf(fd_path, sizeof(fd_path), "/proc/self/fd/%d",
fd.Get());
if (linkat(AT_FDCWD, fd_path, AT_FDCWD, path.c_str(),
if (linkat(AT_FDCWD,
StringFormat<64>("/proc/self/fd/%d", fd.Get()),
AT_FDCWD, path.c_str(),
AT_SYMLINK_FOLLOW) < 0)
throw FormatErrno("Failed to commit %s",
path.c_str());

@@ -19,7 +19,6 @@
#include "config.h"
#include "AsyncInputStream.hxx"
#include "Domain.hxx"
#include "tag/Tag.hxx"
#include "thread/Cond.hxx"
#include "IOThread.hxx"

53
src/input/Error.cxx Normal file

@@ -0,0 +1,53 @@
/*
* Copyright 2003-2018 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 "config.h"
#include "Error.hxx"
#include "system/Error.hxx"
#ifdef ENABLE_CURL
#include "lib/curl/Error.hxx"
#endif
#ifdef ENABLE_NFS
#include "lib/nfs/Error.hxx"
#include <nfsc/libnfs-raw-nfs.h>
#endif
bool
IsFileNotFound(std::exception_ptr ep)
{
try {
std::rethrow_exception(ep);
} catch (const std::system_error &e) {
return IsFileNotFound(e);
#ifdef ENABLE_CURL
} catch (const HttpStatusError &e) {
return e.GetStatus() == 404;
#endif
#ifdef ENABLE_NFS
} catch (const NfsClientError &e) {
return e.GetCode() == NFS3ERR_NOENT;
#endif
} catch (...) {
}
return false;
}

45
src/input/Error.hxx Normal file

@@ -0,0 +1,45 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef INPUT_ERROR_HXX
#define INPUT_ERROR_HXX
#include "check.h"
#include "Compiler.h"
#include <exception>
/**
* Was this exception thrown because the requested file does not
* exist? This function attempts to recognize exceptions thrown by
* various input plugins.
*/
#ifndef __clang__
/* the "pure" attribute must be disabled because it triggers a clang
bug, wrongfully leading to std::terminate() even though the
function catches all exceptions thrown by std::rethrow_exception();
this can be reproduced with clang 7 from Android NDK r18b and on
clang 6 on FreeBSD
(https://github.com/MusicPlayerDaemon/MPD/issues/373) */
gcc_pure
#endif
bool
IsFileNotFound(std::exception_ptr e);
#endif

@@ -20,7 +20,7 @@
#include "config.h"
#include "InputStream.hxx"
#include "thread/Cond.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
#include <stdexcept>
@@ -77,8 +77,8 @@ gcc_pure
static bool
ExpensiveSeeking(const char *uri) noexcept
{
return StringStartsWith(uri, "http://") ||
StringStartsWith(uri, "https://");
return StringStartsWithCaseASCII(uri, "http://") ||
StringStartsWithCaseASCII(uri, "https://");
}
bool

@@ -29,4 +29,10 @@
*/
typedef uint64_t offset_type;
/**
* To format an offset_type with printf(). To use this, include
* <cinttypes>.
*/
#define PRIoffset PRIu64
#endif

@@ -22,7 +22,6 @@
#include "Registry.hxx"
#include "InputPlugin.hxx"
#include "LocalOpen.hxx"
#include "Domain.hxx"
#include "plugins/RewindInputPlugin.hxx"
#include "fs/Traits.hxx"
#include "fs/AllocatedPath.hxx"

@@ -33,7 +33,7 @@
#include "util/RuntimeError.hxx"
#include "util/StringCompare.hxx"
#include "util/ReusableArray.hxx"
#include "util/ASCII.hxx"
#include "Log.hxx"
#include "event/MultiSocketMonitor.hxx"
#include "event/DeferredMonitor.hxx"
@@ -147,7 +147,7 @@ private:
inline InputStream *
AlsaInputStream::Create(const char *uri, Mutex &mutex, Cond &cond)
{
const char *device = StringAfterPrefix(uri, "alsa://");
const char *device = StringAfterPrefixCaseASCII(uri, "alsa://");
if (device == nullptr)
return nullptr;

@@ -26,7 +26,7 @@
#include "../InputStream.hxx"
#include "../InputPlugin.hxx"
#include "util/StringUtil.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
#include "util/RuntimeError.hxx"
#include "util/Domain.hxx"
#include "system/ByteOrder.hxx"
@@ -128,7 +128,7 @@ struct cdio_uri {
static bool
parse_cdio_uri(struct cdio_uri *dest, const char *src)
{
if (!StringStartsWith(src, "cdda://"))
if (!StringStartsWithCaseASCII(src, "cdda://"))
return false;
src += 7;

@@ -19,6 +19,7 @@
#include "config.h"
#include "CurlInputPlugin.hxx"
#include "lib/curl/Error.hxx"
#include "lib/curl/Easy.hxx"
#include "lib/curl/Global.hxx"
#include "lib/curl/Request.hxx"
@@ -34,12 +35,15 @@
#include "IOThread.hxx"
#include "util/ASCII.hxx"
#include "util/StringUtil.hxx"
#include "util/StringFormat.hxx"
#include "util/NumberParser.hxx"
#include "util/RuntimeError.hxx"
#include "util/Domain.hxx"
#include "Log.hxx"
#include "PluginUnavailable.hxx"
#include <cinttypes>
#include <assert.h>
#include <string.h>
@@ -185,7 +189,9 @@ CurlInputStream::OnHeaders(unsigned status,
assert(!postponed_exception);
if (status < 200 || status >= 300)
throw FormatRuntimeError("got HTTP status %ld", status);
throw HttpStatusError(status,
StringFormat<40>("got HTTP status %u",
status).c_str());
const std::lock_guard<Mutex> protect(mutex);
@@ -371,13 +377,10 @@ CurlInputStream::InitEasy()
if (proxy_port > 0)
request->SetOption(CURLOPT_PROXYPORT, (long)proxy_port);
if (proxy_user != nullptr && proxy_password != nullptr) {
char proxy_auth_str[1024];
snprintf(proxy_auth_str, sizeof(proxy_auth_str),
"%s:%s",
proxy_user, proxy_password);
request->SetOption(CURLOPT_PROXYUSERPWD, proxy_auth_str);
}
if (proxy_user != nullptr && proxy_password != nullptr)
request->SetOption(CURLOPT_PROXYUSERPWD,
StringFormat<1024>("%s:%s", proxy_user,
proxy_password).c_str());
request->SetOption(CURLOPT_SSL_VERIFYPEER, verify_peer ? 1l : 0l);
request->SetOption(CURLOPT_SSL_VERIFYHOST, verify_host ? 2l : 0l);
@@ -414,16 +417,10 @@ CurlInputStream::SeekInternal(offset_type new_offset)
/* send the "Range" header */
if (offset > 0) {
char range[32];
#ifdef _WIN32
// TODO: what can we use on Windows to format 64 bit?
sprintf(range, "%lu-", (long)offset);
#else
sprintf(range, "%llu-", (unsigned long long)offset);
#endif
request->SetOption(CURLOPT_RANGE, range);
}
if (offset > 0)
request->SetOption(CURLOPT_RANGE,
StringFormat<40>("%" PRIoffset "-",
offset).c_str());
StartRequest();
}
@@ -461,8 +458,8 @@ CurlInputStream::Open(const char *url, Mutex &mutex, Cond &cond)
static InputStream *
input_curl_open(const char *url, Mutex &mutex, Cond &cond)
{
if (strncmp(url, "http://", 7) != 0 &&
strncmp(url, "https://", 8) != 0)
if (!StringStartsWithCaseASCII(url, "http://") &&
!StringStartsWithCaseASCII(url, "https://"))
return nullptr;
return CurlInputStream::Open(url, mutex, cond);

@@ -28,7 +28,7 @@
#include "../InputStream.hxx"
#include "../InputPlugin.hxx"
#include "PluginUnavailable.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
extern "C" {
#include <libavformat/avio.h>
@@ -85,12 +85,12 @@ static InputStream *
input_ffmpeg_open(const char *uri,
Mutex &mutex, Cond &cond)
{
if (!StringStartsWith(uri, "gopher://") &&
!StringStartsWith(uri, "rtp://") &&
!StringStartsWith(uri, "rtsp://") &&
!StringStartsWith(uri, "rtmp://") &&
!StringStartsWith(uri, "rtmpt://") &&
!StringStartsWith(uri, "rtmps://"))
if (!StringStartsWithCaseASCII(uri, "gopher://") &&
!StringStartsWithCaseASCII(uri, "rtp://") &&
!StringStartsWithCaseASCII(uri, "rtsp://") &&
!StringStartsWithCaseASCII(uri, "rtmp://") &&
!StringStartsWithCaseASCII(uri, "rtmpt://") &&
!StringStartsWithCaseASCII(uri, "rtmps://"))
return nullptr;
AVIOContext *h;

@@ -65,12 +65,9 @@ OpenFileInputStream(Path path,
throw FormatRuntimeError("Not a regular file: %s",
path.c_str());
#if !defined(__BIONIC__) || __ANDROID_API__ >= 21
/* posix_fadvise() requires Android API 21 */
#ifdef POSIX_FADV_SEQUENTIAL
posix_fadvise(reader.GetFD().Get(), (off_t)0, info.GetSize(),
POSIX_FADV_SEQUENTIAL);
#endif
#endif
return InputStreamPtr(new FileInputStream(path.ToUTF8().c_str(),

@@ -22,7 +22,7 @@
#include "input/ThreadInputStream.hxx"
#include "input/InputPlugin.hxx"
#include "system/Error.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
#include <libmms/mmsx.h>
@@ -74,10 +74,10 @@ static InputStream *
input_mms_open(const char *url,
Mutex &mutex, Cond &cond)
{
if (!StringStartsWith(url, "mms://") &&
!StringStartsWith(url, "mmsh://") &&
!StringStartsWith(url, "mmst://") &&
!StringStartsWith(url, "mmsu://"))
if (!StringStartsWithCaseASCII(url, "mms://") &&
!StringStartsWithCaseASCII(url, "mmsh://") &&
!StringStartsWithCaseASCII(url, "mmst://") &&
!StringStartsWithCaseASCII(url, "mmsu://"))
return nullptr;
auto m = new MmsInputStream(url, mutex, cond);

@@ -23,9 +23,7 @@
#include "../InputPlugin.hxx"
#include "lib/nfs/Glue.hxx"
#include "lib/nfs/FileReader.hxx"
#include "util/StringCompare.hxx"
#include <string.h>
#include "util/ASCII.hxx"
/**
* Do not buffer more than this number of bytes. It should be a
@@ -219,7 +217,7 @@ static InputStream *
input_nfs_open(const char *uri,
Mutex &mutex, Cond &cond)
{
if (!StringStartsWith(uri, "nfs://"))
if (!StringStartsWithCaseASCII(uri, "nfs://"))
return nullptr;
NfsInputStream *is = new NfsInputStream(uri, mutex, cond);

@@ -25,7 +25,7 @@
#include "../InputPlugin.hxx"
#include "PluginUnavailable.hxx"
#include "system/Error.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
#include <libsmbclient.h>
@@ -87,7 +87,7 @@ static InputStream *
input_smbclient_open(const char *uri,
Mutex &mutex, Cond &cond)
{
if (!StringStartsWith(uri, "smb://"))
if (!StringStartsWithCaseASCII(uri, "smb://"))
return nullptr;
const std::lock_guard<Mutex> protect(smbclient_mutex);

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2010-2011 Max Kellermann <max.kellermann@gmail.com>
* Copyright (C) 2010-2018 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -31,6 +31,7 @@
#define JAVA_CLASS_HXX
#include "Ref.hxx"
#include "Exception.hxx"
#include <assert.h>
@@ -38,7 +39,7 @@ namespace Java {
/**
* Wrapper for a local "jclass" reference.
*/
class Class : public Java::LocalRef<jclass> {
class Class : public LocalRef<jclass> {
public:
Class(JNIEnv *env, jclass cls)
:LocalRef<jclass>(env, cls) {}
@@ -68,10 +69,8 @@ namespace Java {
assert(name != nullptr);
jclass cls = env->FindClass(name);
if (cls == nullptr) {
env->ExceptionClear();
if (DiscardException(env))
return false;
}
Set(env, cls);
env->DeleteLocalRef(cls);

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2010-2011 Max Kellermann <max.kellermann@gmail.com>
* Copyright 2010-2018 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2010-2011 Max Kellermann <max.kellermann@gmail.com>
* Copyright 2010-2018 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -34,7 +34,6 @@
#include <jni.h>
#include <assert.h>
#include <stddef.h>
namespace Java {

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2016 Max Kellermann <max.kellermann@gmail.com>
* Copyright (C) 2016-2018 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -86,6 +86,10 @@ public:
if (code != CURLE_OK)
throw std::runtime_error(curl_easy_strerror(code));
}
char *Escape(const char *string, int length=0) const noexcept {
return curl_easy_escape(handle, string, length);
}
};
#endif

@@ -1,5 +1,5 @@
/*
* Copyright 2003-2017 The Music Player Daemon Project
* Copyright 2003-2018 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
@@ -17,8 +17,26 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "config.h"
#include "Domain.hxx"
#include "util/Domain.hxx"
#ifndef CURL_ERROR_HXX
#define CURL_ERROR_HXX
const Domain input_domain("input");
#include <stdexcept>
/**
* Thrown when an unsuccessful status was received from the HTTP
* server.
*/
class HttpStatusError : public std::runtime_error {
unsigned status;
public:
template<typename M>
explicit HttpStatusError(unsigned _status, M &&_msg) noexcept
:std::runtime_error(std::forward<M>(_msg)), status(_status) {}
unsigned GetStatus() const noexcept {
return status;
}
};
#endif

@@ -25,6 +25,7 @@
#include "Domain.hxx"
#include "LogV.hxx"
#include "util/Domain.hxx"
#include "util/StringFormat.hxx"
extern "C" {
#include <libavutil/log.h>
@@ -57,9 +58,10 @@ FfmpegLogCallback(gcc_unused void *ptr, int level, const char *fmt, va_list vl)
cls = *(const AVClass *const*)ptr;
if (cls != nullptr) {
char domain[64];
snprintf(domain, sizeof(domain), "%s/%s",
ffmpeg_domain.GetName(), cls->item_name(ptr));
const auto domain =
StringFormat<64>("%s/%s",
ffmpeg_domain.GetName(),
cls->item_name(ptr));
const Domain d(domain);
LogFormatV(d, FfmpegImportLogLevel(level), fmt, vl);
}

@@ -19,6 +19,7 @@
#include "config.h"
#include "Connection.hxx"
#include "Error.hxx"
#include "Lease.hxx"
#include "Callback.hxx"
#include "event/Loop.hxx"
@@ -46,9 +47,9 @@ NfsConnection::CancellableCallback::Stat(nfs_context *ctx,
{
assert(connection.GetEventLoop().IsInside());
int result = nfs_stat_async(ctx, path, Callback, this);
int result = nfs_stat64_async(ctx, path, Callback, this);
if (result < 0)
throw FormatRuntimeError("nfs_stat_async() failed: %s",
throw FormatRuntimeError("nfs_stat64_async() failed: %s",
nfs_get_error(ctx));
}
@@ -139,7 +140,7 @@ NfsConnection::CancellableCallback::Callback(int err, void *data)
if (err >= 0)
cb.OnNfsCallback((unsigned)err, data);
else
cb.OnNfsError(std::make_exception_ptr(std::runtime_error((const char *)data)));
cb.OnNfsError(std::make_exception_ptr(NfsClientError(-err, (const char *)data)));
} else {
if (open) {
/* a nfs_open_async() call was cancelled - to

76
src/lib/nfs/Error.cxx Normal file

@@ -0,0 +1,76 @@
/*
* Copyright 2007-2017 Content Management AG
* All rights reserved.
*
* author: Max Kellermann <mk@cm4all.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "Error.hxx"
#include "util/StringFormat.hxx"
extern "C" {
#include <nfsc/libnfs.h>
}
#include <assert.h>
#include <string.h>
static StringBuffer<256>
FormatNfsClientError(struct nfs_context *nfs, const char *msg) noexcept
{
assert(msg != nullptr);
const char *msg2 = nfs_get_error(nfs);
return StringFormat<256>("%s: %s", msg, msg2);
}
NfsClientError::NfsClientError(struct nfs_context *nfs, const char *msg) noexcept
:std::runtime_error(FormatNfsClientError(nfs, msg).c_str()),
code(0) {}
static StringBuffer<256>
FormatNfsClientError(int err, struct nfs_context *nfs, void *data,
const char *msg) noexcept
{
assert(msg != nullptr);
assert(err < 0);
const char *msg2 = (const char *)data;
if (data == nullptr || *(const char *)data == 0) {
msg2 = nfs_get_error(nfs);
if (msg2 == nullptr)
msg2 = strerror(-err);
}
return StringFormat<256>("%s: %s", msg, msg2);
}
NfsClientError::NfsClientError(int err, struct nfs_context *nfs, void *data,
const char *msg) noexcept
:std::runtime_error(FormatNfsClientError(err, nfs, data, msg).c_str()),
code(-err) {}

58
src/lib/nfs/Error.hxx Normal file

@@ -0,0 +1,58 @@
/*
* Copyright 2007-2017 Content Management AG
* All rights reserved.
*
* author: Max Kellermann <mk@cm4all.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef NFS_ERROR_HXX
#define NFS_ERROR_HXX
#include <stdexcept>
class NfsClientError : public std::runtime_error {
int code;
public:
explicit NfsClientError(const char *_msg) noexcept
:std::runtime_error(_msg), code(0) {}
NfsClientError(int _code, const char *_msg) noexcept
:std::runtime_error(_msg), code(_code) {}
NfsClientError(struct nfs_context *nfs, const char *msg) noexcept;
NfsClientError(int err, struct nfs_context *nfs, void *data,
const char *msg) noexcept;
int GetCode() const noexcept {
return code;
}
};
#endif

@@ -24,7 +24,7 @@
#include "Connection.hxx"
#include "event/Call.hxx"
#include "IOThread.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
#include <utility>
@@ -93,7 +93,7 @@ NfsFileReader::Open(const char *uri)
{
assert(state == State::INITIAL);
if (!StringStartsWith(uri, "nfs://"))
if (!StringStartsWithCaseASCII(uri, "nfs://"))
throw std::runtime_error("Malformed nfs:// URI");
uri += 6;

@@ -20,7 +20,7 @@
#include "config.h"
#include "ls.hxx"
#include "client/Response.hxx"
#include "util/StringCompare.hxx"
#include "util/ASCII.hxx"
#include "util/UriUtil.hxx"
#include <assert.h>
@@ -97,7 +97,7 @@ uri_supported_scheme(const char *uri) noexcept
assert(uri_has_scheme(uri));
while (*urlPrefixes) {
if (StringStartsWith(uri, *urlPrefixes))
if (StringStartsWithCaseASCII(uri, *urlPrefixes))
return true;
urlPrefixes++;
}

@@ -37,6 +37,7 @@
#include "config/ConfigGlobal.hxx"
#include "config/Block.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringFormat.hxx"
#include "Log.hxx"
#include <stdexcept>

@@ -36,6 +36,8 @@
#include <roaraudio.h>
#undef new
#include <assert.h>
class RoarOutput {
friend struct AudioOutputWrapper<RoarOutput>;

@@ -122,15 +122,6 @@ HttpdClient::HandleLine(const char *line)
return true;
}
if (StringEqualsCaseASCII(line, "transferMode.dlna.org: Streaming", 32)) {
/* Send as dlna */
dlna_streaming_requested = true;
/* metadata is not supported by dlna streaming, so disable it */
metadata_supported = false;
metadata_requested = false;
return true;
}
/* expect more request headers */
return true;
}
@@ -148,22 +139,7 @@ HttpdClient::SendResponse()
assert(state == RESPONSE);
if (dlna_streaming_requested) {
snprintf(buffer, sizeof(buffer),
"HTTP/1.1 206 OK\r\n"
"Content-Type: %s\r\n"
"Content-Length: 10000\r\n"
"Content-RangeX: 0-1000000/1000000\r\n"
"transferMode.dlna.org: Streaming\r\n"
"Accept-Ranges: bytes\r\n"
"Connection: close\r\n"
"realTimeInfo.dlna.org: DLNA.ORG_TLAG=*\r\n"
"contentFeatures.dlna.org: DLNA.ORG_OP=01;DLNA.ORG_CI=0\r\n"
"\r\n",
httpd.content_type);
response = buffer;
} else if (metadata_requested) {
if (metadata_requested) {
allocated =
icy_server_metadata_header(httpd.name, httpd.genre,
httpd.website,
@@ -202,7 +178,6 @@ HttpdClient::HttpdClient(HttpdOutput &_httpd, int _fd, EventLoop &_loop,
state(REQUEST),
queue_size(0),
head_method(false),
dlna_streaming_requested(false),
metadata_supported(_metadata_supported),
metadata_requested(false), metadata_sent(true),
metaint(8192), /*TODO: just a std value */

@@ -82,11 +82,6 @@ class HttpdClient final
*/
bool head_method;
/**
* If DLNA streaming was an option.
*/
bool dlna_streaming_requested;
/* ICY */
/**

@@ -35,6 +35,7 @@
#include "SoxrResampler.hxx"
#endif
#include <assert.h>
#include <string.h>
enum class SelectedResampler {

@@ -32,6 +32,8 @@
#include "PcmDop.hxx"
#endif
#include <assert.h>
void
PcmExport::Open(SampleFormat sample_format, unsigned _channels,
Params params)

@@ -202,6 +202,7 @@ CueParser::Feed2(char *p) noexcept
return;
if (strcmp(type, "WAVE") != 0 &&
strcmp(type, "FLAC") != 0 && /* non-standard */
strcmp(type, "MP3") != 0 &&
strcmp(type, "AIFF") != 0) {
state = IGNORE_FILE;

@@ -24,6 +24,7 @@
#include "config/Block.hxx"
#include "input/InputStream.hxx"
#include "tag/TagBuilder.hxx"
#include "util/ASCII.hxx"
#include "util/StringCompare.hxx"
#include "util/Alloc.hxx"
#include "util/Domain.hxx"
@@ -68,7 +69,7 @@ soundcloud_resolve(const char* uri)
{
char *u, *ru;
if (StringStartsWith(uri, "https://")) {
if (StringStartsWithCaseASCII(uri, "https://")) {
u = xstrdup(uri);
} else if (StringStartsWith(uri, "soundcloud.com")) {
u = xstrcatdup("https://", uri);
@@ -273,7 +274,7 @@ try {
static SongEnumerator *
soundcloud_open_uri(const char *uri, Mutex &mutex, Cond &cond)
{
assert(strncmp(uri, "soundcloud://", 13) == 0);
assert(StringEqualsCaseASCII(uri, "soundcloud://", 13));
uri += 13;
char *u = nullptr;

@@ -20,9 +20,9 @@
#ifndef MPD_ACK_H
#define MPD_ACK_H
#include <stdexcept>
#include "util/StringFormat.hxx"
#include <stdio.h>
#include <stdexcept>
class Domain;
@@ -60,9 +60,9 @@ template<typename... Args>
static inline ProtocolError
FormatProtocolError(enum ack code, const char *fmt, Args&&... args) noexcept
{
char buffer[256];
snprintf(buffer, sizeof(buffer), fmt, std::forward<Args>(args)...);
return ProtocolError(code, buffer);
return ProtocolError(code,
StringFormat<256>(fmt,
std::forward<Args>(args)...));
}
#endif

@@ -33,8 +33,11 @@
#include "event/DeferredMonitor.hxx"
#include "thread/Mutex.hxx"
#include "thread/Cond.hxx"
#include "util/ASCII.hxx"
#include "util/IterableSplitString.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringCompare.hxx"
#include "util/StringFormat.hxx"
#include "util/TimeParser.hxx"
#include "util/UriUtil.hxx"
@@ -77,9 +80,18 @@ CurlStorage::MapUTF8(const char *uri_utf8) const noexcept
if (StringIsEmpty(uri_utf8))
return base;
// TODO: escape the given URI
CurlEasy easy;
std::string path_esc;
return PathTraitsUTF8::Build(base.c_str(), uri_utf8);
for (auto elt: IterableSplitString(uri_utf8, '/')) {
char *elt_esc = easy.Escape(elt.data, elt.size);
if (!path_esc.empty())
path_esc.push_back('/');
path_esc += elt_esc;
curl_free(elt_esc);
}
return PathTraitsUTF8::Build(base.c_str(), path_esc.c_str());
}
const char *
@@ -296,9 +308,7 @@ public:
{
request.SetOption(CURLOPT_CUSTOMREQUEST, "PROPFIND");
char buffer[40];
sprintf(buffer, "depth: %u", depth);
request_headers.Append(buffer);
request_headers.Append(StringFormat<40>("depth: %u", depth));
request.SetOption(CURLOPT_HTTPHEADER, request_headers.Get());
@@ -591,8 +601,8 @@ CurlStorage::OpenDirectory(const char *uri_utf8)
static Storage *
CreateCurlStorageURI(EventLoop &event_loop, const char *uri)
{
if (strncmp(uri, "http://", 7) != 0 &&
strncmp(uri, "https://", 8) != 0)
if (!StringStartsWithCaseASCII(uri, "http://") &&
!StringStartsWithCaseASCII(uri, "https://"))
return nullptr;
return new CurlStorage(event_loop, uri);

@@ -35,6 +35,7 @@
#include "event/Call.hxx"
#include "event/DeferredMonitor.hxx"
#include "event/TimeoutMonitor.hxx"
#include "util/ASCII.hxx"
#include "util/StringCompare.hxx"
extern "C" {
@@ -245,19 +246,19 @@ NfsStorage::MapToRelativeUTF8(const char *uri_utf8) const noexcept
}
static void
Copy(StorageFileInfo &info, const struct stat &st)
Copy(StorageFileInfo &info, const struct nfs_stat_64 &st)
{
if (S_ISREG(st.st_mode))
if (S_ISREG(st.nfs_mode))
info.type = StorageFileInfo::Type::REGULAR;
else if (S_ISDIR(st.st_mode))
else if (S_ISDIR(st.nfs_mode))
info.type = StorageFileInfo::Type::DIRECTORY;
else
info.type = StorageFileInfo::Type::OTHER;
info.size = st.st_size;
info.mtime = st.st_mtime;
info.device = st.st_dev;
info.inode = st.st_ino;
info.size = st.nfs_size;
info.mtime = st.nfs_mtime;
info.device = st.nfs_dev;
info.inode = st.nfs_ino;
}
class NfsGetInfoOperation final : public BlockingNfsOperation {
@@ -278,7 +279,7 @@ protected:
}
void HandleResult(gcc_unused unsigned status, void *data) override {
Copy(info, *(const struct stat *)data);
Copy(info, *(const struct nfs_stat_64 *)data);
}
};
@@ -401,11 +402,10 @@ NfsStorage::OpenDirectory(const char *uri_utf8)
static Storage *
CreateNfsStorageURI(EventLoop &event_loop, const char *base)
{
if (strncmp(base, "nfs://", 6) != 0)
const char *p = StringAfterPrefixCaseASCII(base, "nfs://");
if (p == nullptr)
return nullptr;
const char *p = base + 6;
const char *mount = strchr(p, '/');
if (mount == nullptr)
throw std::runtime_error("Malformed nfs:// URI");

@@ -27,6 +27,7 @@
#include "fs/Traits.hxx"
#include "thread/Mutex.hxx"
#include "system/Error.hxx"
#include "util/ASCII.hxx"
#include "util/StringCompare.hxx"
#include "util/ScopeExit.hxx"
@@ -182,7 +183,7 @@ SmbclientDirectoryReader::GetInfo(gcc_unused bool follow)
static Storage *
CreateSmbclientStorageURI(gcc_unused EventLoop &event_loop, const char *base)
{
if (strncmp(base, "smb://", 6) != 0)
if (!StringStartsWithCaseASCII(base, "smb://"))
return nullptr;
SmbclientInit();

@@ -22,21 +22,6 @@
#include "EPollFD.hxx"
#include "FatalError.hxx"
#if defined(__BIONIC__) && __ANDROID_API__ < 21
#include <sys/syscall.h>
#include <fcntl.h>
#define EPOLL_CLOEXEC O_CLOEXEC
static inline int
epoll_create1(int flags)
{
return syscall(__NR_epoll_create1, flags);
}
#endif
EPollFD::EPollFD()
:fd(::epoll_create1(EPOLL_CLOEXEC))
{

53
src/tag/Fallback.hxx Normal file

@@ -0,0 +1,53 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef MPD_TAG_FALLBACK_HXX
#define MPD_TAG_FALLBACK_HXX
#include <utility>
template<typename F>
bool
ApplyTagFallback(TagType type, F &&f) noexcept
{
if (type == TAG_ALBUM_ARTIST_SORT) {
/* fall back to "AlbumArtist", "ArtistSort" and
"Artist" if no "AlbumArtistSort" was found */
if (f(TAG_ALBUM_ARTIST))
return true;
return ApplyTagFallback(TAG_ARTIST_SORT, std::forward<F>(f));
}
if (type == TAG_ALBUM_ARTIST || type == TAG_ARTIST_SORT)
/* fall back to "Artist" if no
"AlbumArtist"/"ArtistSort" was found */
return f(TAG_ARTIST);
return false;
}
template<typename F>
bool
ApplyTagWithFallback(TagType type, F &&f) noexcept
{
return f(type) || ApplyTagFallback(type, std::forward<F>(f));
}
#endif

@@ -1,117 +0,0 @@
/*
* Copyright 2003-2017 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 "Set.hxx"
#include "TagBuilder.hxx"
#include "Settings.hxx"
#include <assert.h>
/**
* Copy all tag items of the specified type.
*/
static bool
CopyTagItem(TagBuilder &dest, TagType dest_type,
const Tag &src, TagType src_type)
{
bool found = false;
for (const auto &item : src) {
if (item.type == src_type) {
dest.AddItem(dest_type, item.value);
found = true;
}
}
return found;
}
/**
* Copy all tag items of the specified type. Fall back to "Artist" if
* there is no "AlbumArtist".
*/
static void
CopyTagItem(TagBuilder &dest, const Tag &src, TagType type)
{
if (!CopyTagItem(dest, type, src, type) &&
type == TAG_ALBUM_ARTIST)
CopyTagItem(dest, type, src, TAG_ARTIST);
}
/**
* Copy all tag items of the types in the mask.
*/
static void
CopyTagMask(TagBuilder &dest, const Tag &src, tag_mask_t mask)
{
for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i)
if ((mask & (tag_mask_t(1) << i)) != 0)
CopyTagItem(dest, src, TagType(i));
}
void
TagSet::InsertUnique(const Tag &src, TagType type, const char *value,
tag_mask_t group_mask) noexcept
{
TagBuilder builder;
if (value == nullptr)
builder.AddEmptyItem(type);
else
builder.AddItem(type, value);
CopyTagMask(builder, src, group_mask);
#if CLANG_OR_GCC_VERSION(4,8)
emplace(builder.Commit());
#else
insert(builder.Commit());
#endif
}
bool
TagSet::CheckUnique(TagType dest_type,
const Tag &tag, TagType src_type,
tag_mask_t group_mask) noexcept
{
bool found = false;
for (const auto &item : tag) {
if (item.type == src_type) {
InsertUnique(tag, dest_type, item.value, group_mask);
found = true;
}
}
return found;
}
void
TagSet::InsertUnique(const Tag &tag,
TagType type, tag_mask_t group_mask) noexcept
{
static_assert(sizeof(group_mask) * 8 >= TAG_NUM_OF_ITEM_TYPES,
"Mask is too small");
assert((group_mask & (tag_mask_t(1) << unsigned(type))) == 0);
if (!CheckUnique(type, tag, type, group_mask) &&
(type != TAG_ALBUM_ARTIST ||
!IsTagEnabled(TAG_ALBUM_ARTIST) ||
/* fall back to "Artist" if no "AlbumArtist" was found */
!CheckUnique(type, tag, TAG_ARTIST, group_mask)))
InsertUnique(tag, type, nullptr, group_mask);
}

@@ -1,73 +0,0 @@
/*
* Copyright 2003-2017 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef MPD_TAG_SET_HXX
#define MPD_TAG_SET_HXX
#include "Compiler.h"
#include "Tag.hxx"
#include "Mask.hxx"
#include <set>
#include <string.h>
/**
* Helper class for #TagSet which compares two #Tag objects.
*/
struct TagLess {
gcc_pure
bool operator()(const Tag &a, const Tag &b) const noexcept {
if (a.num_items != b.num_items)
return a.num_items < b.num_items;
const unsigned n = a.num_items;
for (unsigned i = 0; i < n; ++i) {
const TagItem &ai = *a.items[i];
const TagItem &bi = *b.items[i];
if (ai.type != bi.type)
return unsigned(ai.type) < unsigned(bi.type);
const int cmp = strcmp(ai.value, bi.value);
if (cmp != 0)
return cmp < 0;
}
return false;
}
};
/**
* A set of #Tag objects.
*/
class TagSet : public std::set<Tag, TagLess> {
public:
void InsertUnique(const Tag &tag,
TagType type, tag_mask_t group_mask) noexcept;
private:
void InsertUnique(const Tag &src, TagType type, const char *value,
tag_mask_t group_mask) noexcept;
bool CheckUnique(TagType dest_type,
const Tag &tag, TagType src_type,
tag_mask_t group_mask) noexcept;
};
#endif

@@ -188,6 +188,16 @@ TagBuilder::Complement(const Tag &other)
tag_pool_lock.unlock();
}
void
TagBuilder::AddItemUnchecked(TagType type, StringView value) noexcept
{
tag_pool_lock.lock();
auto i = tag_pool_get_item(type, value);
tag_pool_lock.unlock();
items.push_back(i);
}
inline void
TagBuilder::AddItemInternal(TagType type, StringView value)
{
@@ -197,13 +207,9 @@ TagBuilder::AddItemInternal(TagType type, StringView value)
if (!f.IsNull())
value = { f.data, f.size };
tag_pool_lock.lock();
auto i = tag_pool_get_item(type, value);
tag_pool_lock.unlock();
AddItemUnchecked(type, value);
free(f.data);
items.push_back(i);
}
void
@@ -229,11 +235,7 @@ TagBuilder::AddItem(TagType type, const char *value)
void
TagBuilder::AddEmptyItem(TagType type)
{
tag_pool_lock.lock();
auto i = tag_pool_get_item(type, StringView::Empty());
tag_pool_lock.unlock();
items.push_back(i);
AddItemUnchecked(type, StringView::Empty());
}
void

@@ -132,6 +132,12 @@ public:
*/
void Complement(const Tag &other);
/**
* A variant of AddItem() which does not attempt to fix up the
* value and does not check whether the tag type is disabled.
*/
void AddItemUnchecked(TagType type, StringView value) noexcept;
/**
* Appends a new tag item.
*

@@ -21,8 +21,8 @@
#include "TagHandler.hxx"
#include "TagBuilder.hxx"
#include "util/ASCII.hxx"
#include "util/StringFormat.hxx"
#include <stdio.h>
#include <stdlib.h>
static void
@@ -42,11 +42,8 @@ add_tag_tag(TagType type, const char *value, void *ctx)
/* filter out this extra data and leading zeroes */
char *end;
unsigned n = strtoul(value, &end, 10);
if (value != end) {
char s[21];
if (snprintf(s, 21, "%u", n) > 0)
tag.AddItem(type, s);
}
if (value != end)
tag.AddItem(type, StringFormat<21>("%u", n));
} else
tag.AddItem(type, value);
}

60
src/tag/VisitFallback.hxx Normal file

@@ -0,0 +1,60 @@
/*
* Copyright 2003-2018 The Music Player Daemon Project
* http://www.musicpd.org
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef MPD_TAG_VISIT_FALLBACK_HXX
#define MPD_TAG_VISIT_FALLBACK_HXX
#include "Fallback.hxx"
#include "Tag.hxx"
template<typename F>
bool
VisitTagType(const Tag &tag, TagType type, F &&f) noexcept
{
bool found = false;
for (const auto &item : tag) {
if (item.type == type) {
found = true;
f(item.value);
}
}
return found;
}
template<typename F>
bool
VisitTagWithFallback(const Tag &tag, TagType type, F &&f) noexcept
{
return ApplyTagWithFallback(type,
[&](TagType type2) {
return VisitTagType(tag, type2, f);
});
}
template<typename F>
void
VisitTagWithFallbackOrEmpty(const Tag &tag, TagType type, F &&f) noexcept
{
if (!VisitTagWithFallback(tag, type, f))
f("");
}
#endif

@@ -31,7 +31,7 @@
#endif
#ifdef HAVE_THREAD_NAME
# include <stdio.h>
#include "util/StringFormat.hxx"
#endif
static inline void
@@ -59,9 +59,7 @@ static inline void
FormatThreadName(const char *fmt, gcc_unused Args&&... args)
{
#ifdef HAVE_THREAD_NAME
char buffer[16];
snprintf(buffer, sizeof(buffer), fmt, args...);
SetThreadName(buffer);
SetThreadName(StringFormat<16>(fmt, args...));
#else
(void)fmt;
#endif

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2013 Max Kellermann <max.kellermann@gmail.com>
* Copyright (C) 2013-2018 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -30,6 +30,7 @@
#ifndef ASCII_HXX
#define ASCII_HXX
#include "StringView.hxx"
#include "Compiler.h"
#include <assert.h>
@@ -69,4 +70,20 @@ StringEqualsCaseASCII(const char *a, const char *b, size_t n) noexcept
return strncasecmp(a, b, n) == 0;
}
gcc_pure gcc_nonnull_all
static inline bool
StringStartsWithCaseASCII(const char *haystack, StringView needle) noexcept
{
return StringEqualsCaseASCII(haystack, needle.data, needle.size);
}
gcc_pure gcc_nonnull_all
static inline const char *
StringAfterPrefixCaseASCII(const char *haystack, StringView needle) noexcept
{
return StringStartsWithCaseASCII(haystack, needle)
? haystack + needle.size
: nullptr;
}
#endif

@@ -23,14 +23,9 @@
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <string.h>
#endif
AllocatedString<>
FormatStringV(const char *fmt, va_list args)
{
#ifndef _WIN32
va_list tmp;
va_copy(tmp, args);
const int length = vsnprintf(NULL, 0, fmt, tmp);
@@ -43,22 +38,6 @@ FormatStringV(const char *fmt, va_list args)
char *buffer = new char[length + 1];
vsnprintf(buffer, length + 1, fmt, args);
return AllocatedString<>::Donate(buffer);
#else
/* On mingw32, snprintf() expects a 64 bit integer instead of
a "long int" for "%li". This is not consistent with our
expectation, so we're using plain sprintf() here, hoping
the static buffer is large enough. Sorry for this hack,
but WIN32 development is so painful, I'm not in the mood to
do it properly now. */
char buffer[16384];
vsprintf(buffer, fmt, args);
const size_t length = strlen(buffer);
char *p = new char[length + 1];
memcpy(p, buffer, length + 1);
return AllocatedString<>::Donate(p);
#endif
}
AllocatedString<>

69
src/util/StringFormat.hxx Normal file

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2010-2015 Max Kellermann <max.kellermann@gmail.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the
* distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef STRING_FORMAT_HXX
#define STRING_FORMAT_HXX
#include "StringBuffer.hxx"
#include <stdio.h>
template<typename... Args>
static inline void
StringFormat(char *buffer, size_t size,
const char *fmt, Args&&... args) noexcept
{
snprintf(buffer, size, fmt, args...);
}
template<size_t CAPACITY, typename... Args>
static inline void
StringFormat(StringBuffer<CAPACITY> &buffer,
const char *fmt, Args&&... args) noexcept
{
StringFormat(buffer.data(), buffer.capacity(), fmt, args...);
}
template<size_t CAPACITY, typename... Args>
static inline StringBuffer<CAPACITY>
StringFormat(const char *fmt, Args&&... args) noexcept
{
StringBuffer<CAPACITY> result;
StringFormat(result, fmt, args...);
return result;
}
template<typename... Args>
static inline void
StringFormatUnsafe(char *buffer, const char *fmt, Args&&... args) noexcept
{
sprintf(buffer, fmt, args...);
}
#endif

@@ -18,7 +18,7 @@
*/
#include "UriUtil.hxx"
#include "StringCompare.hxx"
#include "ASCII.hxx"
#include "CharUtil.hxx"
#include <assert.h>
@@ -169,7 +169,7 @@ SkipUriScheme(const char *uri) noexcept
{
const char *const schemes[] = { "http://", "https://", "ftp://" };
for (auto scheme : schemes) {
auto result = StringAfterPrefix(uri, scheme);
auto result = StringAfterPrefixCaseASCII(uri, scheme);
if (result != nullptr)
return result;
}

@@ -65,6 +65,7 @@ class CrossGccToolchain:
self.is_arm = arch.startswith('arm')
self.is_armv7 = self.is_arm and 'armv7' in self.cflags
self.is_aarch64 = arch == 'aarch64'
self.is_windows = 'mingw32' in arch
self.env = dict(os.environ)
@@ -86,6 +87,7 @@ thirdparty_libs = [
liblame,
ffmpeg,
curl,
libexpat,
libnfs,
boost,
]