Compare commits

..

39 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 #373
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 #360
2018-10-23 18:44:28 +02:00
Max Kellermann
61eff1cddf Makefile.am: install the SVG icon
Closes #366
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 #355
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
49 changed files with 1290 additions and 474 deletions

View File

@@ -276,7 +276,8 @@ libjava_a_SOURCES = \
noinst_LIBRARIES += libandroid.a noinst_LIBRARIES += libandroid.a
libandroid_a_SOURCES = \ libandroid_a_SOURCES = \
src/android/Context.cxx src/android/Context.hxx \ 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 libandroid_a_CPPFLAGS = $(AM_CPPFLAGS) -Iandroid/build/include
noinst_LIBRARIES += libmain.a noinst_LIBRARIES += libmain.a
@@ -291,6 +292,7 @@ clean-local:
rm -rf android/build rm -rf android/build
libmpd.so: $(filter %.a,$(src_mpd_LDADD)) libmain.a 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) $(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_BUILD_TOOLS_VERSION = 27.0.0
@@ -300,6 +302,7 @@ ANDROID_BUILD_TOOLS_DIR = $(ANDROID_SDK)/build-tools/$(ANDROID_SDK_BUILD_TOOLS_V
ANDROID_SDK_PLATFORM_DIR = $(ANDROID_SDK)/platforms/$(ANDROID_SDK_PLATFORM) ANDROID_SDK_PLATFORM_DIR = $(ANDROID_SDK)/platforms/$(ANDROID_SDK_PLATFORM)
JAVAC = javac JAVAC = javac
AIDL = $(ANDROID_BUILD_TOOLS_DIR)/aidl
AAPT = $(ANDROID_BUILD_TOOLS_DIR)/aapt AAPT = $(ANDROID_BUILD_TOOLS_DIR)/aapt
DX = $(ANDROID_BUILD_TOOLS_DIR)/dx DX = $(ANDROID_BUILD_TOOLS_DIR)/dx
ZIPALIGN = $(ANDROID_BUILD_TOOLS_DIR)/zipalign ZIPALIGN = $(ANDROID_BUILD_TOOLS_DIR)/zipalign
@@ -307,18 +310,30 @@ ZIPALIGN = $(ANDROID_BUILD_TOOLS_DIR)/zipalign
ANDROID_XML_RES := $(wildcard $(srcdir)/android/res/*/*.xml) ANDROID_XML_RES := $(wildcard $(srcdir)/android/res/*/*.xml)
ANDROID_XML_RES_COPIES := $(patsubst $(srcdir)/android/%,android/build/%,$(ANDROID_XML_RES)) 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_SOURCE_PATHS = $(addprefix $(srcdir)/android/src/,$(JAVA_SOURCE_NAMES))
JAVA_CLASSFILES_DIR = android/build/classes JAVA_CLASSFILES_DIR = android/build/classes
$(ANDROID_XML_RES_COPIES): $(ANDROID_XML_RES) AIDL_FILES = $(wildcard $(srcdir)/android/src/*.aidl)
@$(MKDIR_P) $(dir $@) AIDL_JAVA_FILES = $(patsubst $(srcdir)/android/src/%.aidl,android/build/src/org/musicpd/%.java,$(AIDL_FILES))
cp $(patsubst android/build/%,$(srcdir)/android/%,$@) $@
android/build/resources.apk: $(ANDROID_XML_RES_COPIES) android/build/res/drawable/icon.png android/build/src/org/musicpd/IMain.java: android/build/src/org/musicpd/IMainCallback.java
@$(MKDIR_P) android/build/gen
$(AAPT) package -f -m --auto-add-overlay \ $(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 \ --custom-package org.musicpd \
-M $(srcdir)/android/AndroidManifest.xml \ -M $(srcdir)/android/AndroidManifest.xml \
-S android/build/res \ -S android/build/res \
@@ -329,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 # R.java is generated by aapt, when resources.apk is generated
android/build/gen/org/musicpd/R.java: android/build/resources.apk 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 android/build/classes.dex: $(JAVA_SOURCE_PATHS) $(AIDL_JAVA_FILES) android/build/gen/org/musicpd/R.java
@$(MKDIR_P) $(JAVA_CLASSFILES_DIR) $(AM_V_GEN)
$(JAVAC) -source 1.6 -target 1.6 -Xlint:-options \ $(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) \ -cp $(ANDROID_SDK_PLATFORM_DIR)/android.jar:$(JAVA_CLASSFILES_DIR) \
-h android/build/include \
-d $(JAVA_CLASSFILES_DIR) $^ -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 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 BUILT_SOURCES = android/build/include/org_musicpd_Bridge.h
android/build/lib/$(ANDROID_ABI)/libmpd.so: libmpd.so android/build/lib/$(ANDROID_ABI)/libmpd.so: libmpd.so
mkdir -p $(@D) $(AM_V_GEN)
rm -f $@ $(AM_V_at)$(MKDIR_P) $(@D)
$(STRIP) -o $@ $< $(AM_V_at)rm -f $@
$(AM_V_at)$(STRIP) -o $@ $<
android/build/res/drawable/icon.png: mpd.svg android/build/res/drawable/icon.png: mpd.svg
mkdir -p $(@D) $(AM_V_GEN)
rsvg-convert --width=48 --height=48 $< -o $@ $(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 .DELETE_ON_ERROR: android/build/unsigned.apk
android/build/unsigned.apk: android/build/classes.dex android/build/resources.apk android/build/lib/$(ANDROID_ABI)/libmpd.so android/build/unsigned.apk: android/build/classes.dex android/build/resources.apk android/build/lib/$(ANDROID_ABI)/libmpd.so
cp android/build/resources.apk $@ $(AM_V_GEN)
cd $(dir $@) && zip -q -r $(notdir $@) classes.dex lib $(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 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 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 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 endif
@@ -951,13 +977,14 @@ libtag_a_SOURCES =\
src/tag/TagItem.hxx \ src/tag/TagItem.hxx \
src/tag/TagHandler.cxx src/tag/TagHandler.hxx \ src/tag/TagHandler.cxx src/tag/TagHandler.hxx \
src/tag/Mask.hxx \ src/tag/Mask.hxx \
src/tag/Fallback.hxx \
src/tag/VisitFallback.hxx \
src/tag/Settings.cxx src/tag/Settings.hxx \ src/tag/Settings.cxx src/tag/Settings.hxx \
src/tag/TagConfig.cxx src/tag/TagConfig.hxx \ src/tag/TagConfig.cxx src/tag/TagConfig.hxx \
src/tag/TagNames.c \ src/tag/TagNames.c \
src/tag/TagString.cxx src/tag/TagString.hxx \ src/tag/TagString.cxx src/tag/TagString.hxx \
src/tag/TagPool.cxx src/tag/TagPool.hxx \ src/tag/TagPool.cxx src/tag/TagPool.hxx \
src/tag/TagTable.cxx src/tag/TagTable.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/Format.cxx src/tag/Format.hxx \
src/tag/VorbisComment.cxx src/tag/VorbisComment.hxx \ src/tag/VorbisComment.cxx src/tag/VorbisComment.hxx \
src/tag/ReplayGain.cxx src/tag/ReplayGain.hxx \ src/tag/ReplayGain.cxx src/tag/ReplayGain.hxx \
@@ -1675,6 +1702,13 @@ FILTER_LIBS = \
$(PCM_LIBS) $(PCM_LIBS)
#
# Icon
#
iconsdir = $(datadir)/icons/hicolor/scalable/apps
icons_DATA = mpd.svg
# #
# systemd unit # systemd unit
# #
@@ -2444,6 +2478,7 @@ endif
# #
EXTRA_DIST = $(doc_DATA) autogen.sh \ EXTRA_DIST = $(doc_DATA) autogen.sh \
mpd.svg \
test/test_archive_bzip2.sh \ test/test_archive_bzip2.sh \
test/test_archive_iso9660.sh \ test/test_archive_iso9660.sh \
test/test_archive_zzip.sh \ test/test_archive_zzip.sh \

17
NEWS
View File

@@ -1,3 +1,20 @@
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) ver 0.20.21 (2018/08/17)
* database * database
- proxy: add "password" setting - proxy: add "password" setting

View File

@@ -2,23 +2,32 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd" package="org.musicpd"
android:installLocation="auto" android:installLocation="auto"
android:versionCode="20" android:versionCode="21"
android:versionName="0.20.21"> android:versionName="0.20.22">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21"/> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/>
<application android:icon="@drawable/icon" android:label="@string/app_name"> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<activity android:name=".Main" <uses-permission android:name="android.permission.WAKE_LOCK"/>
android:label="@string/app_name" <uses-permission android:name="android.permission.INTERNET"/>
android:launchMode="singleInstance"> <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> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </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> </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> </manifest>

View File

@@ -132,13 +132,15 @@ class AndroidNdkToolchain:
libcxx_path = os.path.join(ndk_path, 'sources/cxx-stl/llvm-libc++') libcxx_path = os.path.join(ndk_path, 'sources/cxx-stl/llvm-libc++')
libcxx_libs_path = os.path.join(libcxx_path, 'libs', android_abi) 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_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: if use_cxx:
self.cxxflags += ' ' + libstdcxx_cxxflags self.cxxflags += ' ' + libstdcxx_cxxflags
self.ldflags += ' ' + libstdcxx_ldflags self.ldflags += ' ' + libstdcxx_ldflags
self.libs += ' ' + libstdcxx_libs
self.env = dict(os.environ) self.env = dict(os.environ)

View File

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

View File

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

View File

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

View File

@@ -2,4 +2,10 @@
<resources> <resources>
<string name="app_name">MPD</string> <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> </resources>

View File

@@ -25,6 +25,12 @@ import android.content.Context;
* Bridge to native code. * Bridge to native code.
*/ */
public class Bridge { 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(); public static native void shutdown();
} }

12
android/src/IMain.aidl Normal file
View 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);
}

View File

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

View File

@@ -19,57 +19,360 @@
package org.musicpd; package org.musicpd;
import android.app.Activity; import android.annotation.TargetApi;
import android.os.Bundle; 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.Build;
import android.os.Handler; import android.os.IBinder;
import android.os.Message; import android.os.PowerManager;
import android.widget.TextView; import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log; import android.util.Log;
import android.widget.RemoteViews;
public class Main extends Activity implements Runnable { public class Main extends Service implements Runnable {
private static final String TAG = "MPD"; 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() { static class MainStub extends IMain.Stub {
public void handleMessage(Message msg) { private Main mService;
textView.setText("Music Player Daemon has quit"); 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 protected void onCreate(Bundle savedInstanceState) { @Override
super.onCreate(savedInstanceState); 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) { if (!Loader.loaded) {
TextView tv = new TextView(this); final String error = "Failed to load the native MPD libary.\n" +
tv.setText("Failed to load the native MPD libary.\n" +
"Report this problem to us, and include the following information:\n" + "Report this problem to us, and include the following information:\n" +
"SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" + "SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" +
"PRODUCT=" + Build.PRODUCT + "\n" + "PRODUCT=" + Build.PRODUCT + "\n" +
"FINGERPRINT=" + Build.FINGERPRINT + "\n" + "FINGERPRINT=" + Build.FINGERPRINT + "\n" +
"error=" + Loader.error); "error=" + Loader.error;
setContentView(tv); setStatus(MAIN_STATUS_ERROR, error);
stopSelf();
return; return;
} }
synchronized (this) {
if (thread == null || !thread.isAlive()) { if (mAbort)
thread = new Thread(this, "NativeMain"); return;
thread.start(); setStatus(MAIN_STATUS_STARTED, null);
}
Bridge.run(this, mLogListener);
setStatus(MAIN_STATUS_STOPPED, null);
} }
textView = new TextView(this); private synchronized void setStatus(int status, String error) {
textView.setText("Music Player Daemon is running" mStatus = status;
+ "\nCAUTION: this version is EXPERIMENTAL!"); mError = error;
setContentView(textView); sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
} }
@Override public void run() { private void start() {
Bridge.run(this); if (mThread != null)
quitHandler.sendMessage(quitHandler.obtainMessage()); 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);
}
};
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@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);
}
public boolean start() {
synchronized (this) {
if (mIMain != null) {
try {
mIMain.start();
return true;
} catch (RemoteException e) {
}
}
return false;
}
}
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);
}
}
}
/*
* 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
View 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
View 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();
}
}

View File

@@ -1,10 +1,10 @@
AC_PREREQ(2.60) AC_PREREQ(2.60)
AC_INIT(mpd, 0.20.21, musicpd-dev-team@lists.sourceforge.net) AC_INIT(mpd, 0.20.22, musicpd-dev-team@lists.sourceforge.net)
VERSION_MAJOR=0 VERSION_MAJOR=0
VERSION_MINOR=20 VERSION_MINOR=20
VERSION_REVISION=21 VERSION_REVISION=22
VERSION_EXTRA=0 VERSION_EXTRA=0
AC_CONFIG_SRCDIR([src/Main.cxx]) AC_CONFIG_SRCDIR([src/Main.cxx])
@@ -14,7 +14,7 @@ AM_SILENT_RULES
AC_CONFIG_HEADERS(config.h) AC_CONFIG_HEADERS(config.h)
AC_CONFIG_MACRO_DIR([m4]) 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=`cd "$srcdir" && 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 if test x$GIT_COMMIT != x; then
@@ -707,7 +707,7 @@ MPD_ENABLE_AUTO_PKG_LIB(smbclient, SMBCLIENT, [smbclient >= 0.2],
[smbclient input plugin], [libsmbclient not found]) [smbclient input plugin], [libsmbclient not found])
dnl ----------------------------------- NFS ----------------------------- 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]) [NFS input plugin], [libnfs not found])
dnl --------------------------------- Soundcloud ------------------------------ dnl --------------------------------- Soundcloud ------------------------------

View File

@@ -1599,6 +1599,11 @@ OK
per-artist counts: per-artist counts:
</para> </para>
<programlisting>count group artist</programlisting> <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> </listitem>
</varlistentry> </varlistentry>

View File

@@ -9,8 +9,8 @@ from build.ffmpeg import FfmpegProject
from build.boost import BoostProject from build.boost import BoostProject
libmpdclient = MesonProject( libmpdclient = MesonProject(
'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.14.tar.xz', 'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.16.tar.xz',
'0a84e2791bfe3077cf22ee1784c805d5bb550803dffe56a39aa3690a38061372', 'fa6bdab67c0e0490302b38f00c27b4959735c3ec8aef7a88327adb1407654464',
'lib/libmpdclient.a', 'lib/libmpdclient.a',
) )
@@ -38,8 +38,8 @@ libvorbis = AutotoolsProject(
) )
opus = AutotoolsProject( opus = AutotoolsProject(
'https://archive.mozilla.org/pub/opus/opus-1.2.1.tar.gz', 'https://archive.mozilla.org/pub/opus/opus-1.3.tar.gz',
'cfafd339ccd9c5ef8d6ab15d7e1a412c054bf4cb4ecbbbcc78c12ef2def70732', '4f3d69aefdf2dbaf9825408e452a8a414ffc60494c70633560700398820dc550',
'lib/libopus.a', 'lib/libopus.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
@@ -341,8 +341,8 @@ ffmpeg = FfmpegProject(
) )
curl = AutotoolsProject( curl = AutotoolsProject(
'http://curl.haxx.se/download/curl-7.61.0.tar.xz', 'http://curl.haxx.se/download/curl-7.61.1.tar.xz',
'ef6e55192d04713673b4409ccbcb4cb6cd723137d6e10ca45b0c593a454e1720', '3d5913d6a39bd22e68e34dff697fd6e4c3c81563f580c76fca2009315cd81891',
'lib/libcurl.a', 'lib/libcurl.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',

View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@
#include "db/LightSong.hxx" #include "db/LightSong.hxx"
#include "DetachedSong.hxx" #include "DetachedSong.hxx"
#include "tag/Tag.hxx" #include "tag/Tag.hxx"
#include "tag/Fallback.hxx"
#include "util/ConstBuffer.hxx" #include "util/ConstBuffer.hxx"
#include "util/StringAPI.hxx" #include "util/StringAPI.hxx"
#include "util/StringCompare.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]) { 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 /* If the search critieron was not visited during the
sweep through the song's tag, it means this field sweep through the song's tag, it means this field
is absent from the tag or empty. Thus, if the is absent from the tag or empty. Thus, if the
@@ -117,15 +136,6 @@ SongFilter::Item::Match(const Tag &_tag) const noexcept
true. */ true. */
if (value.empty()) if (value.empty())
return true; 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; return false;

View File

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

View File

@@ -0,0 +1,32 @@
/*
* 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_ANDROID_LOG_LISTENER_HXX
#define MPD_ANDROID_LOG_LISTENER_HXX
#include "java/Object.hxx"
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -120,9 +120,9 @@ public:
VisitSong visit_song, VisitSong visit_song,
VisitPlaylist visit_playlist) const override; VisitPlaylist visit_playlist) const override;
void VisitUniqueTags(const DatabaseSelection &selection, std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type, tag_mask_t group_mask, TagType tag_type,
VisitTag visit_tag) const override; TagType group) const override;
DatabaseStats GetStats(const DatabaseSelection &selection) const override; DatabaseStats GetStats(const DatabaseSelection &selection) const override;
@@ -334,28 +334,19 @@ SendConstraints(mpd_connection *connection, const DatabaseSelection &selection)
} }
static bool static bool
SendGroupMask(mpd_connection *connection, tag_mask_t mask) SendGroup(mpd_connection *connection, TagType group)
{ {
#if LIBMPDCLIENT_CHECK_VERSION(2,12,0) if (group == TAG_NUM_OF_ITEM_TYPES)
for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) { return true;
if ((mask & (tag_mask_t(1) << i)) == 0)
continue;
const auto tag = Convert(TagType(i)); #if LIBMPDCLIENT_CHECK_VERSION(2,12,0)
const auto tag = Convert(group);
if (tag == MPD_TAG_COUNT) if (tag == MPD_TAG_COUNT)
throw std::runtime_error("Unsupported tag"); throw std::runtime_error("Unsupported tag");
if (!mpd_search_add_group_tag(connection, tag)) return mpd_search_add_group_tag(connection, tag);
return false;
}
return true;
#else #else
(void)connection; (void)connection;
(void)mask;
if (mask != 0)
throw std::runtime_error("Grouping requires libmpdclient 2.12");
return true; return true;
#endif #endif
@@ -799,11 +790,9 @@ ProxyDatabase::Visit(const DatabaseSelection &selection,
visit_directory, visit_song, visit_playlist); visit_directory, visit_song, visit_playlist);
} }
void std::map<std::string, std::set<std::string>>
ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection, ProxyDatabase::CollectUniqueTags(const DatabaseSelection &selection,
TagType tag_type, TagType tag_type, TagType group) const
tag_mask_t group_mask,
VisitTag visit_tag) const
try { try {
// TODO: eliminate the const_cast // TODO: eliminate the const_cast
const_cast<ProxyDatabase *>(this)->EnsureConnected(); const_cast<ProxyDatabase *>(this)->EnsureConnected();
@@ -814,13 +803,16 @@ try {
if (!mpd_search_db_tags(connection, tag_type2) || if (!mpd_search_db_tags(connection, tag_type2) ||
!SendConstraints(connection, selection) || !SendConstraints(connection, selection) ||
!SendGroupMask(connection, group_mask)) !SendGroup(connection, group))
ThrowError(connection); ThrowError(connection);
if (!mpd_search_commit(connection)) if (!mpd_search_commit(connection))
ThrowError(connection); ThrowError(connection);
TagBuilder builder; std::map<std::string, std::set<std::string>> result;
if (group == TAG_NUM_OF_ITEM_TYPES) {
auto &values = result[std::string()];
while (auto *pair = mpd_recv_pair(connection)) { while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) { AtScopeExit(this, pair) {
@@ -831,37 +823,36 @@ try {
if (current_type == TAG_NUM_OF_ITEM_TYPES) if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue; continue;
if (current_type == tag_type && !builder.IsEmpty()) { if (current_type == tag_type)
try { values.emplace(pair->value);
visit_tag(builder.Commit());
} catch (...) {
mpd_response_finish(connection);
throw;
} }
} else {
std::set<std::string> *current_group = nullptr;
while (auto *pair = mpd_recv_pair(connection)) {
AtScopeExit(this, pair) {
mpd_return_pair(connection, pair);
};
const auto current_type = tag_name_parse_i(pair->name);
if (current_type == TAG_NUM_OF_ITEM_TYPES)
continue;
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];
} }
builder.AddItem(current_type, pair->value);
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);
}
if (!builder.IsEmpty()) {
try {
visit_tag(builder.Commit());
} catch (...) {
mpd_response_finish(connection);
throw;
} }
} }
if (!mpd_response_finish(connection)) if (!mpd_response_finish(connection))
ThrowError(connection); ThrowError(connection);
return result;
} catch (...) { } catch (...) {
if (connection != nullptr) if (connection != nullptr)
mpd_search_cancel(connection); mpd_search_cancel(connection);

View File

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

View File

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

View File

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

View File

@@ -64,7 +64,12 @@ fluidsynth_level_to_mpd(enum fluid_log_level level)
* logging library. * logging library.
*/ */
static void 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, Log(fluidsynth_domain,
fluidsynth_level_to_mpd(fluid_log_level(level)), fluidsynth_level_to_mpd(fluid_log_level(level)),

View File

@@ -30,7 +30,15 @@
* exist? This function attempts to recognize exceptions thrown by * exist? This function attempts to recognize exceptions thrown by
* various input plugins. * 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 gcc_pure
#endif
bool bool
IsFileNotFound(std::exception_ptr e); IsFileNotFound(std::exception_ptr e);

View File

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

View File

@@ -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 * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions

View File

@@ -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 * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions
@@ -34,7 +34,6 @@
#include <jni.h> #include <jni.h>
#include <assert.h>
#include <stddef.h> #include <stddef.h>
namespace Java { namespace Java {

View File

@@ -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 * Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions * modification, are permitted provided that the following conditions
@@ -86,6 +86,10 @@ public:
if (code != CURLE_OK) if (code != CURLE_OK)
throw std::runtime_error(curl_easy_strerror(code)); 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 #endif

View File

@@ -47,9 +47,9 @@ NfsConnection::CancellableCallback::Stat(nfs_context *ctx,
{ {
assert(connection.GetEventLoop().IsInside()); 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) if (result < 0)
throw FormatRuntimeError("nfs_stat_async() failed: %s", throw FormatRuntimeError("nfs_stat64_async() failed: %s",
nfs_get_error(ctx)); nfs_get_error(ctx));
} }

View File

@@ -34,6 +34,7 @@
#include "thread/Mutex.hxx" #include "thread/Mutex.hxx"
#include "thread/Cond.hxx" #include "thread/Cond.hxx"
#include "util/ASCII.hxx" #include "util/ASCII.hxx"
#include "util/IterableSplitString.hxx"
#include "util/RuntimeError.hxx" #include "util/RuntimeError.hxx"
#include "util/StringCompare.hxx" #include "util/StringCompare.hxx"
#include "util/StringFormat.hxx" #include "util/StringFormat.hxx"
@@ -79,9 +80,18 @@ CurlStorage::MapUTF8(const char *uri_utf8) const noexcept
if (StringIsEmpty(uri_utf8)) if (StringIsEmpty(uri_utf8))
return base; 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 * const char *

View File

@@ -246,19 +246,19 @@ NfsStorage::MapToRelativeUTF8(const char *uri_utf8) const noexcept
} }
static void 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; info.type = StorageFileInfo::Type::REGULAR;
else if (S_ISDIR(st.st_mode)) else if (S_ISDIR(st.nfs_mode))
info.type = StorageFileInfo::Type::DIRECTORY; info.type = StorageFileInfo::Type::DIRECTORY;
else else
info.type = StorageFileInfo::Type::OTHER; info.type = StorageFileInfo::Type::OTHER;
info.size = st.st_size; info.size = st.nfs_size;
info.mtime = st.st_mtime; info.mtime = st.nfs_mtime;
info.device = st.st_dev; info.device = st.nfs_dev;
info.inode = st.st_ino; info.inode = st.nfs_ino;
} }
class NfsGetInfoOperation final : public BlockingNfsOperation { class NfsGetInfoOperation final : public BlockingNfsOperation {
@@ -279,7 +279,7 @@ protected:
} }
void HandleResult(gcc_unused unsigned status, void *data) override { void HandleResult(gcc_unused unsigned status, void *data) override {
Copy(info, *(const struct stat *)data); Copy(info, *(const struct nfs_stat_64 *)data);
} }
}; };

53
src/tag/Fallback.hxx Normal file
View 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

View File

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

View File

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

View File

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

View File

@@ -132,6 +132,12 @@ public:
*/ */
void Complement(const Tag &other); 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. * Appends a new tag item.
* *

60
src/tag/VisitFallback.hxx Normal file
View 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