diff --git a/src/contrib/websocket_remote/Constants.h b/src/contrib/websocket_remote/Constants.h index 306322f3d..9ad030703 100644 --- a/src/contrib/websocket_remote/Constants.h +++ b/src/contrib/websocket_remote/Constants.h @@ -76,6 +76,7 @@ namespace key { static const std::string playing_current_time = "playing_current_time"; static const std::string playing_track = "playing_track"; static const std::string title = "title"; + static const std::string external_id = "external_id"; static const std::string filename = "filename"; static const std::string artist = "artist"; static const std::string album = "album"; diff --git a/src/contrib/websocket_remote/HttpServer.cpp b/src/contrib/websocket_remote/HttpServer.cpp index 37affaecb..ad1326e98 100644 --- a/src/contrib/websocket_remote/HttpServer.cpp +++ b/src/contrib/websocket_remote/HttpServer.cpp @@ -288,15 +288,16 @@ int HttpServer::HandleRequest( if (parts.size() > 0) { if (parts.at(0) == fragment::audio && parts.size() == 3) { IRetainedTrack* track = nullptr; + bool byExternalId = (parts.at(1) == fragment::external_id); - if (parts.at(1) == fragment::id) { - uint64_t id = std::stoull(urlDecode(parts.at(2))); - track = server->context.dataProvider->QueryTrackById(id); - } - else if (parts.at(1) == fragment::external_id) { + if (byExternalId) { std::string externalId = urlDecode(parts.at(2)); track = server->context.dataProvider->QueryTrackByExternalId(externalId.c_str()); } + else if (parts.at(1) == fragment::id) { + uint64_t id = std::stoull(urlDecode(parts.at(2))); + track = server->context.dataProvider->QueryTrackById(id); + } if (track) { std::string filename = GetMetadataString(track, key::filename); @@ -360,6 +361,13 @@ int HttpServer::HandleRequest( MHD_add_response_header(response, "Accept-Ranges", "bytes"); } + if (byExternalId) { + /* if we're using an on-demand transcoder, ensure the client does not cache the + result because we have to guess the content length. */ + std::string value = isOnDemandTranscoder ? "no-cache" : "public, max-age=31536000"; + MHD_add_response_header(response, "Cache-Control", value.c_str()); + } + MHD_add_response_header(response, "Content-Type", contentType(filename).c_str()); MHD_add_response_header(response, "Server", "musikcube websocket_remote"); diff --git a/src/contrib/websocket_remote/WebSocketServer.cpp b/src/contrib/websocket_remote/WebSocketServer.cpp index e96899e25..0918112fc 100644 --- a/src/contrib/websocket_remote/WebSocketServer.cpp +++ b/src/contrib/websocket_remote/WebSocketServer.cpp @@ -727,6 +727,7 @@ void WebSocketServer::BroadcastPlayQueueChanged() { json WebSocketServer::WebSocketServer::ReadTrackMetadata(IRetainedTrack* track) { return { { key::id, track->GetId() }, + { key::external_id, GetMetadataString(track, key::external_id) }, { key::title, GetMetadataString(track, key::title) }, { key::album, GetMetadataString(track, key::album) }, { key::album_id, track->GetInt64(key::album_id.c_str()) }, diff --git a/src/core/library/Indexer.cpp b/src/core/library/Indexer.cpp index 37350a250..39d761f7c 100644 --- a/src/core/library/Indexer.cpp +++ b/src/core/library/Indexer.cpp @@ -65,7 +65,6 @@ static const std::string TAG = "Indexer"; static const int MAX_THREADS = 2; static const size_t TRANSACTION_INTERVAL = 300; -static std::atomic nextExternalId; using namespace musik::core; using namespace musik::core::sdk; @@ -246,16 +245,6 @@ void Indexer::Synchronize(const SyncContext& context, boost::asio::io_service* i /* process local files */ if (type == SyncType::All || type == SyncType::Local) { - /* resolve our next external id. we do this once before starting so - we don't need to make a bunch of additional queries while indexing. */ - { - db::Statement stmt("SELECT MAX(id) FROM tracks", this->dbConnection); - if (stmt.Step() == db::Row) { - auto id = std::max((int64_t) 1, stmt.ColumnInt64(0)); - nextExternalId.store(id); - } - } - std::vector paths; std::vector pathIds; @@ -376,9 +365,6 @@ void Indexer::ReadMetadataFromFile( /* write it to the db, if read successfully */ if (saveToDb) { - std::string externalId = "local://" + std::to_string(nextExternalId.fetch_add(1)); - track.SetValue("external_id", externalId.c_str()); - track.SetValue("path_id", pathId.c_str()); track.Save(this->dbConnection, this->libraryPath); diff --git a/src/core/library/LocalLibrary.cpp b/src/core/library/LocalLibrary.cpp index 0bd3b3e4f..557c2aeee 100644 --- a/src/core/library/LocalLibrary.cpp +++ b/src/core/library/LocalLibrary.cpp @@ -50,7 +50,7 @@ using namespace musik::core; using namespace musik::core::library; using namespace musik::core::runtime; -#define DATABASE_VERSION 3 +#define DATABASE_VERSION 4 #define VERBOSE_LOGGING 0 #define MESSAGE_QUERY_COMPLETED 5000 @@ -299,6 +299,11 @@ static void upgradeV2ToV3(db::Connection& db) { scheduleSyncDueToDbUpgrade = true; } +static void upgradeV3ToV4(db::Connection& db) { + db.Execute("DELETE from tracks"); + scheduleSyncDueToDbUpgrade = true; +} + static void setVersion(db::Connection& db, int version) { db.Execute("DELETE FROM version"); @@ -481,6 +486,10 @@ void LocalLibrary::CreateDatabase(db::Connection &db){ upgradeV2ToV3(db); } + if (lastVersion >= 1 && lastVersion < 4) { + upgradeV3ToV4(db); + } + /* ensure our version is set correctly */ setVersion(db, DATABASE_VERSION); diff --git a/src/core/library/track/IndexerTrack.cpp b/src/core/library/track/IndexerTrack.cpp index d4344850d..31d9888c8 100644 --- a/src/core/library/track/IndexerTrack.cpp +++ b/src/core/library/track/IndexerTrack.cpp @@ -43,7 +43,9 @@ #include #include - +#include +#include +#include #include using namespace musik::core; @@ -60,6 +62,7 @@ using namespace musik::core; static std::mutex trackWriteLock; static std::unordered_map metadataIdCache; +static auto uuids = boost::uuids::random_generator(); void IndexerTrack::ResetIdCache() { metadataIdCache.clear(); @@ -615,6 +618,10 @@ bool IndexerTrack::Save(db::Connection &dbConnection, std::string libraryDirecto this->SetValue("album_artist", this->GetValue("artist").c_str()); } + if (this->GetValue("external_id") == "") { + this->SetValue("external_id", boost::uuids::to_string(uuids()).c_str()); + } + /* remove existing relations -- we're going to update them with fresh data */ if (this->id != 0) { diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java index 3b7107845..ec1ec4a9f 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/ExoPlayerWrapper.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import java.io.File; +import java.util.HashMap; +import java.util.Map; import io.casey.musikcube.remote.Application; import io.casey.musikcube.remote.util.NetworkUtil; @@ -35,8 +37,21 @@ import okhttp3.Cache; import okhttp3.OkHttpClient; public class ExoPlayerWrapper extends PlayerWrapper { - private static OkHttpClient audioStreamClient = null; - private static boolean certValidationDisabled = false; + private static OkHttpClient audioStreamHttpClient = null; + + private static final long BYTES_PER_MEGABYTE = 1048576L; + private static final long BYTES_PER_GIGABYTE = 1073741824L; + private static final Map CACHE_SETTING_TO_BYTES; + + static { + CACHE_SETTING_TO_BYTES = new HashMap<>(); + CACHE_SETTING_TO_BYTES.put(0, BYTES_PER_MEGABYTE * 32); + CACHE_SETTING_TO_BYTES.put(1, BYTES_PER_GIGABYTE / 2); + CACHE_SETTING_TO_BYTES.put(2, BYTES_PER_GIGABYTE); + CACHE_SETTING_TO_BYTES.put(3, BYTES_PER_GIGABYTE * 2); + CACHE_SETTING_TO_BYTES.put(4, BYTES_PER_GIGABYTE * 3); + CACHE_SETTING_TO_BYTES.put(5, BYTES_PER_GIGABYTE * 4); + } private DefaultBandwidthMeter bandwidth; private DataSource.Factory datasources; @@ -47,6 +62,12 @@ public class ExoPlayerWrapper extends PlayerWrapper { private Context context; private SharedPreferences prefs; + public static void invalidateSettings() { + synchronized (ExoPlayerWrapper.class) { + audioStreamHttpClient = null; + } + } + public ExoPlayerWrapper() { this.context = Application.getInstance(); this.prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE); @@ -56,37 +77,34 @@ public class ExoPlayerWrapper extends PlayerWrapper { this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector); this.extractors = new DefaultExtractorsFactory(); this.player.addListener(eventListener); - - synchronized (ExoPlayerWrapper.class) { - final boolean disabled = this.prefs.getBoolean("cert_validation_disabled", false); - if (disabled != certValidationDisabled) { - audioStreamClient = null; - certValidationDisabled = disabled; - } - } } - private void initDataSourceFactory(final String uri) { + private void initHttpClient(final String uri) { final Context context = Application.getInstance(); synchronized (ExoPlayerWrapper.class) { - if (audioStreamClient == null) { + if (audioStreamHttpClient == null) { final File path = new File(context.getExternalCacheDir(), "audio"); - OkHttpClient.Builder builder = new OkHttpClient.Builder() - .cache(new Cache(path, 1048576 * 256)); /* 256 meg cache */ + int diskCacheIndex = this.prefs.getInt("disk_cache_size_index", 0); + if (diskCacheIndex < 0 || diskCacheIndex > CACHE_SETTING_TO_BYTES.size()) { + diskCacheIndex = 0; + } - if (certValidationDisabled) { + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .cache(new Cache(path, CACHE_SETTING_TO_BYTES.get(diskCacheIndex))); + + if (this.prefs.getBoolean("cert_validation_disabled", false)) { NetworkUtil.disableCertificateValidation(builder); } - audioStreamClient = builder.build(); + audioStreamHttpClient = builder.build(); } } if (uri.startsWith("http")) { this.datasources = new OkHttpDataSourceFactory( - audioStreamClient, + audioStreamHttpClient, Util.getUserAgent(context, "musikdroid"), bandwidth); } @@ -98,7 +116,7 @@ public class ExoPlayerWrapper extends PlayerWrapper { @Override public void play(String uri) { - initDataSourceFactory(uri); + initHttpClient(uri); this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null); this.player.setPlayWhenReady(true); this.player.prepare(this.source); @@ -108,7 +126,7 @@ public class ExoPlayerWrapper extends PlayerWrapper { @Override public void prefetch(String uri) { - initDataSourceFactory(uri); + initHttpClient(uri); this.prefetch = true; this.source = new ExtractorMediaSource(Uri.parse(uri), datasources, extractors, null, null); this.player.setPlayWhenReady(false); diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java index 2aed9d332..8b5263ede 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/playback/StreamingPlaybackService.java @@ -13,6 +13,7 @@ import android.util.Log; import org.json.JSONArray; import org.json.JSONObject; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; @@ -535,8 +536,8 @@ public class StreamingPlaybackService implements PlaybackService { private String getUri(final JSONObject track) { if (track != null) { - final long trackId = track.optLong("id", -1); - if (trackId != -1) { + final String externalId = track.optString("external_id", ""); + if (Strings.notEmpty(externalId)) { final String protocol = prefs.getBoolean("ssl_enabled", false) ? "https" : "http"; /* transcoding bitrate, if selected by the user */ @@ -553,11 +554,11 @@ public class StreamingPlaybackService implements PlaybackService { return String.format( Locale.ENGLISH, - "%s://%s:%d/audio/id/%d%s", + "%s://%s:%d/audio/external_id/%s%s", protocol, prefs.getString("address", "192.168.1.100"), prefs.getInt("http_port", 7906), - trackId, + URLEncoder.encode(externalId), bitrateQueryParam); } } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java index 07de55703..98dc52ded 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/activity/SettingsActivity.java @@ -10,8 +10,10 @@ import android.support.annotation.Nullable; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.EditText; @@ -20,6 +22,7 @@ import android.widget.Spinner; import java.util.Locale; import io.casey.musikcube.remote.R; +import io.casey.musikcube.remote.playback.ExoPlayerWrapper; import io.casey.musikcube.remote.playback.MediaPlayerWrapper; import io.casey.musikcube.remote.playback.PlaybackServiceFactory; import io.casey.musikcube.remote.ui.util.Views; @@ -29,8 +32,9 @@ public class SettingsActivity extends AppCompatActivity { private EditText addressText, portText, httpPortText, passwordText; private CheckBox albumArtCheckbox, messageCompressionCheckbox, softwareVolume; private CheckBox sslCheckbox, certCheckbox; - private Spinner playbackModeSpinner, bitrateSpinner; + private Spinner playbackModeSpinner, bitrateSpinner, cacheSpinner; private SharedPreferences prefs; + private boolean wasStreaming; public static Intent getStartIntent(final Context context) { return new Intent(context, SettingsActivity.class); @@ -42,16 +46,27 @@ public class SettingsActivity extends AppCompatActivity { prefs = this.getSharedPreferences("prefs", MODE_PRIVATE); setContentView(R.layout.activity_settings); setTitle(R.string.settings_title); + wasStreaming = isStreamingEnabled(); bindEventListeners(); rebindUi(); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.settings_menu, menu); + return true; + } + @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { finish(); return true; } + else if (item.getItemId() == R.id.action_save) { + save(); + return true; + } return super.onOptionsItemSelected(item); } @@ -76,6 +91,13 @@ public class SettingsActivity extends AppCompatActivity { bitrateSpinner.setAdapter(bitrates); bitrateSpinner.setSelection(prefs.getInt("transcoder_bitrate_index", 0)); + final ArrayAdapter cacheSizes = ArrayAdapter.createFromResource( + this, R.array.disk_cache_array, android.R.layout.simple_spinner_item); + + cacheSizes.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + cacheSpinner.setAdapter(cacheSizes); + cacheSpinner.setSelection(prefs.getInt("disk_cache_size_index", 0)); + this.albumArtCheckbox.setChecked(this.prefs.getBoolean("album_art_enabled", true)); this.messageCompressionCheckbox.setChecked(this.prefs.getBoolean("message_compression_enabled", true)); this.softwareVolume.setChecked(this.prefs.getBoolean("software_volume", false)); @@ -137,43 +159,58 @@ public class SettingsActivity extends AppCompatActivity { this.softwareVolume = (CheckBox) findViewById(R.id.software_volume); this.playbackModeSpinner = (Spinner) findViewById(R.id.playback_mode_spinner); this.bitrateSpinner = (Spinner) findViewById(R.id.transcoder_bitrate_spinner); + this.cacheSpinner = (Spinner) findViewById(R.id.streaming_disk_cache_spinner); this.sslCheckbox = (CheckBox) findViewById(R.id.ssl_checkbox); this.certCheckbox = (CheckBox) findViewById(R.id.cert_validation); - final boolean wasStreaming = isStreamingEnabled(); - - this.findViewById(R.id.button_connect).setOnClickListener((View v) -> { - final String addr = addressText.getText().toString(); - final String port = portText.getText().toString(); - final String httpPort = httpPortText.getText().toString(); - final String password = passwordText.getText().toString(); - - prefs.edit() - .putString("address", addr) - .putInt("port", (port.length() > 0) ? Integer.valueOf(port) : 0) - .putInt("http_port", (httpPort.length() > 0) ? Integer.valueOf(httpPort) : 0) - .putString("password", password) - .putBoolean("album_art_enabled", albumArtCheckbox.isChecked()) - .putBoolean("message_compression_enabled", messageCompressionCheckbox.isChecked()) - .putBoolean("streaming_playback", isStreamingSelected()) - .putBoolean("software_volume", softwareVolume.isChecked()) - .putBoolean("ssl_enabled", sslCheckbox.isChecked()) - .putBoolean("cert_validation_disabled", certCheckbox.isChecked()) - .putInt("transcoder_bitrate_index", bitrateSpinner.getSelectedItemPosition()) - .apply(); - - if (!softwareVolume.isChecked()) { - MediaPlayerWrapper.setGlobalVolume(1.0f); + this.playbackModeSpinner.setOnItemSelectedListener(new Spinner.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int selectedIndex, long l) { + final boolean streaming = (selectedIndex == 1); + bitrateSpinner.setEnabled(streaming); + cacheSpinner.setEnabled(streaming); } - if (wasStreaming && !isStreamingEnabled()) { - PlaybackServiceFactory.streaming(this).stop(); + @Override + public void onNothingSelected(AdapterView adapterView) { + } - - WebSocketService.getInstance(this).disconnect(); - - finish(); }); + } + + private void save() { + final String addr = addressText.getText().toString(); + final String port = portText.getText().toString(); + final String httpPort = httpPortText.getText().toString(); + final String password = passwordText.getText().toString(); + + prefs.edit() + .putString("address", addr) + .putInt("port", (port.length() > 0) ? Integer.valueOf(port) : 0) + .putInt("http_port", (httpPort.length() > 0) ? Integer.valueOf(httpPort) : 0) + .putString("password", password) + .putBoolean("album_art_enabled", albumArtCheckbox.isChecked()) + .putBoolean("message_compression_enabled", messageCompressionCheckbox.isChecked()) + .putBoolean("streaming_playback", isStreamingSelected()) + .putBoolean("software_volume", softwareVolume.isChecked()) + .putBoolean("ssl_enabled", sslCheckbox.isChecked()) + .putBoolean("cert_validation_disabled", certCheckbox.isChecked()) + .putInt("transcoder_bitrate_index", bitrateSpinner.getSelectedItemPosition()) + .putInt("disk_cache_size_index", cacheSpinner.getSelectedItemPosition()) + .apply(); + + if (!softwareVolume.isChecked()) { + MediaPlayerWrapper.setGlobalVolume(1.0f); + } + + if (wasStreaming && !isStreamingEnabled()) { + PlaybackServiceFactory.streaming(this).stop(); + } + + ExoPlayerWrapper.invalidateSettings(); + WebSocketService.getInstance(this).disconnect(); + + finish(); } public static class SslAlertDialog extends DialogFragment { diff --git a/src/musikdroid/app/src/main/res/layout/activity_settings.xml b/src/musikdroid/app/src/main/res/layout/activity_settings.xml index e4b372ec4..1efa15c5a 100644 --- a/src/musikdroid/app/src/main/res/layout/activity_settings.xml +++ b/src/musikdroid/app/src/main/res/layout/activity_settings.xml @@ -1,192 +1,193 @@ - + android:layout_height="wrap_content" + android:fillViewport="true"> - + android:layout_height="wrap_content" + android:padding="16dp"> - + + + android:layout_marginBottom="8dp" + android:layout_marginLeft="24dp"> - + android:maxLines="1" + android:hint="@string/edit_connection_hostname" + android:inputType="textNoSuggestions" /> - + + + + + android:maxLines="1" + android:hint="@string/edit_connection_port" + android:inputType="number" /> - + - + - + android:maxLines="1" + android:hint="@string/edit_connection_http_port" + android:inputType="number" /> - + - + - + android:maxLines="1" + android:hint="@string/edit_connection_password" + android:inputType="textPassword" /> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/menu/settings_menu.xml b/src/musikdroid/app/src/main/res/menu/settings_menu.xml new file mode 100644 index 000000000..c3f527672 --- /dev/null +++ b/src/musikdroid/app/src/main/res/menu/settings_menu.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/values/disk_cache.xml b/src/musikdroid/app/src/main/res/values/disk_cache.xml new file mode 100644 index 000000000..2a8aa9c22 --- /dev/null +++ b/src/musikdroid/app/src/main/res/values/disk_cache.xml @@ -0,0 +1,11 @@ + + + + disabled + 0.5 gb + 1 gb + 2 gb + 3 gb + 4 gb + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/values/strings.xml b/src/musikdroid/app/src/main/res/values/strings.xml index 9d5e5ea23..61220b2f2 100644 --- a/src/musikdroid/app/src/main/res/values/strings.xml +++ b/src/musikdroid/app/src/main/res/values/strings.xml @@ -53,8 +53,9 @@ playlists <unknown> playback mode: - transcoder bitrate: - general: + streaming downsampler bitrate: + streaming disk cache size: + advanced: use ssl for server connections enable album art (uses last.fm) enable message compression