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 
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
49 changed files with 1290 additions and 474 deletions

@@ -276,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
@@ -291,6 +292,7 @@ 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
@@ -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)
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
@@ -307,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 \
@@ -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
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/$(ANDROID_ABI)/libmpd.so: libmpd.so
mkdir -p $(@D)
rm -f $@
$(STRIP) -o $@ $<
$(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/$(ANDROID_ABI)/libmpd.so
cp android/build/resources.apk $@
cd $(dir $@) && zip -q -r $(notdir $@) classes.dex lib
$(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
@@ -951,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 \
@@ -1675,6 +1702,13 @@ FILTER_LIBS = \
$(PCM_LIBS)
#
# Icon
#
iconsdir = $(datadir)/icons/hicolor/scalable/apps
icons_DATA = mpd.svg
#
# systemd unit
#
@@ -2444,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 \

17
NEWS

@@ -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)
* database
- proxy: add "password" setting

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

@@ -132,13 +132,15 @@ class AndroidNdkToolchain:
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)

@@ -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" +
"SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\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.21, 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=21
VERSION_REVISION=22
VERSION_EXTRA=0
AC_CONFIG_SRCDIR([src/Main.cxx])
@@ -14,7 +14,7 @@ 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=`cd "$srcdir" && git describe --dirty --always 2>/dev/null`
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])
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 ------------------------------

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

@@ -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',
@@ -341,8 +341,8 @@ ffmpeg = FfmpegProject(
)
curl = AutotoolsProject(
'http://curl.haxx.se/download/curl-7.61.0.tar.xz',
'ef6e55192d04713673b4409ccbcb4cb6cd723137d6e10ca45b0c593a454e1720',
'http://curl.haxx.se/download/curl-7.61.1.tar.xz',
'3d5913d6a39bd22e68e34dff697fd6e4c3c81563f580c76fca2009315cd81891',
'lib/libcurl.a',
[
'--disable-shared', '--enable-static',

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

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

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

@@ -120,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;
@@ -334,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
@@ -799,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();
@@ -814,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;

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

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

@@ -30,7 +30,15 @@
* 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);

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

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

@@ -34,6 +34,7 @@
#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"
@@ -79,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 *

@@ -246,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 {
@@ -279,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);
}
};

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.
*

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