Merge branch 'nitehu-fix/store-api-version' into develop

This commit is contained in:
Óscar García Amor 2020-10-25 10:30:08 +01:00
commit 5c637a2e9c
No known key found for this signature in database
GPG Key ID: E18B2370D3D566EE
17 changed files with 1443 additions and 1244 deletions

View File

@ -81,6 +81,7 @@ class SubsonicAPIClient(
private val jacksonMapper = ObjectMapper()
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
.registerModule(KotlinModule())
private val retrofit = Retrofit.Builder()

View File

@ -1,9 +1,9 @@
package org.moire.ultrasonic.api.subsonic.interceptors
import java.math.BigInteger
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.util.Locale
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.Response
@ -15,27 +15,33 @@ import okhttp3.Response
* and above.
*/
class PasswordMD5Interceptor(private val password: String) : Interceptor {
private val salt: String by lazy {
val secureRandom = SecureRandom()
BigInteger(130, secureRandom).toString(32)
}
private val passwordMD5Hash: String by lazy {
try {
val md5Digest = MessageDigest.getInstance("MD5")
md5Digest.digest("$password$salt".toByteArray()).toHexBytes().toLowerCase()
} catch (e: NoSuchAlgorithmException) {
throw IllegalStateException(e)
}
}
private val secureRandom = SecureRandom()
private val saltBytes = ByteArray(16)
override fun intercept(chain: Chain): Response {
val originalRequest = chain.request()
val salt = getSalt()
val updatedUrl = originalRequest.url().newBuilder()
.addQueryParameter("t", passwordMD5Hash)
.addQueryParameter("t", getPasswordMD5Hash(salt))
.addQueryParameter("s", salt)
.build()
return chain.proceed(originalRequest.newBuilder().url(updatedUrl).build())
}
private fun getSalt(): String {
secureRandom.nextBytes(saltBytes)
return saltBytes.toHexBytes()
}
private fun getPasswordMD5Hash(salt: String): String {
try {
val md5Digest = MessageDigest.getInstance("MD5")
return md5Digest.digest(
"$password$salt".toByteArray()
).toHexBytes().toLowerCase(Locale.getDefault())
} catch (e: NoSuchAlgorithmException) {
throw IllegalStateException(e)
}
}
}

View File

@ -1218,6 +1218,7 @@ public class DownloadActivity extends SubsonicTabActivity implements OnGestureLi
@Override
protected void error(final Throwable error)
{
Timber.e(error, "Exception has occurred in savePlaylistInBackground");
final String msg = String.format("%s %s", getResources().getString(R.string.download_playlist_error), getErrorMessage(error));
Util.toast(DownloadActivity.this, msg);
}

View File

@ -34,13 +34,11 @@ import android.widget.TextView;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.data.ServerSetting;
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.MergeAdapter;
import org.moire.ultrasonic.util.TabActivityBackgroundTask;
import org.moire.ultrasonic.util.Util;
import java.util.Collections;
@ -55,7 +53,7 @@ public class MainActivity extends SubsonicTabActivity
{
private static boolean infoDialogDisplayed;
private static boolean shouldUseId3;
private static int lastActiveServer;
private static String lastActiveServerProperties;
private Lazy<MediaPlayerLifecycleSupport> lifecycleSupport = inject(MediaPlayerLifecycleSupport.class);
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
@ -121,7 +119,7 @@ public class MainActivity extends SubsonicTabActivity
final View albumsAlphaByArtistButton = buttons.findViewById(R.id.main_albums_alphaByArtist);
final View videosButton = buttons.findViewById(R.id.main_videos);
lastActiveServer = ActiveServerProvider.Companion.getActiveServerId(this);
lastActiveServerProperties = getActiveServerProperties();
String name = activeServerProvider.getValue().getActiveServer().getName();
serverTextView.setText(name);
@ -152,10 +150,6 @@ public class MainActivity extends SubsonicTabActivity
adapter.addView(videosTitle, false);
adapter.addViews(Collections.singletonList(videosButton), true);
if (Util.isNetworkConnected(this)) {
new PingTask(this, false).execute();
}
}
list.setAdapter(adapter);
@ -249,7 +243,7 @@ public class MainActivity extends SubsonicTabActivity
{
final SharedPreferences.Editor editor = preferences.edit();
editor.putString(Constants.PREFERENCES_KEY_CACHE_LOCATION, FileUtil.getDefaultMusicDirectory(this).getPath());
editor.commit();
editor.apply();
}
}
@ -260,7 +254,7 @@ public class MainActivity extends SubsonicTabActivity
boolean shouldRestart = false;
boolean id3 = Util.getShouldUseId3Tags(MainActivity.this);
int currentActiveServer = ActiveServerProvider.Companion.getActiveServerId(MainActivity.this);
String currentActiveServerProperties = getActiveServerProperties();
if (id3 != shouldUseId3)
{
@ -268,9 +262,9 @@ public class MainActivity extends SubsonicTabActivity
shouldRestart = true;
}
if (currentActiveServer != lastActiveServer)
if (!currentActiveServerProperties.equals(lastActiveServerProperties))
{
lastActiveServer = currentActiveServer;
lastActiveServerProperties = currentActiveServerProperties;
shouldRestart = true;
}
@ -378,22 +372,11 @@ public class MainActivity extends SubsonicTabActivity
startActivityForResult(intent, 0);
}
/**
* Temporary task to make a ping to server to get it supported api version.
*/
private static class PingTask extends TabActivityBackgroundTask<Void> {
PingTask(SubsonicTabActivity activity, boolean changeProgress) {
super(activity, changeProgress);
}
@Override
protected Void doInBackground() throws Throwable {
final MusicService service = MusicServiceFactory.getMusicService(getActivity());
service.ping(getActivity(), null);
return null;
}
@Override
protected void done(Void result) {}
}
private String getActiveServerProperties()
{
ServerSetting currentSetting = activeServerProvider.getValue().getActiveServer();
return String.format("%s;%s;%s;%s;%s;%s", currentSetting.getUrl(), currentSetting.getUserName(),
currentSetting.getPassword(), currentSetting.getAllowSelfSignedCertificate(),
currentSetting.getLdapSupport(), currentSetting.getMinimumApiVersion());
}
}

View File

@ -361,7 +361,16 @@ public class MediaPlayerControllerImpl implements MediaPlayerController
public synchronized void clear(boolean serialize)
{
MediaPlayerService mediaPlayerService = MediaPlayerService.getRunningInstance();
if (mediaPlayerService != null) mediaPlayerService.clear(serialize);
if (mediaPlayerService != null) {
mediaPlayerService.clear(serialize);
} else {
// If no MediaPlayerService is available, just empty the playlist
downloader.clear();
if (serialize) {
downloadQueueSerializer.serializeDownloadQueue(downloader.downloadList,
downloader.getCurrentPlayingIndex(), getPlayerPosition());
}
}
jukeboxMediaPlayer.getValue().updatePlaylist();
}

View File

@ -21,12 +21,14 @@ package org.moire.ultrasonic.service;
import android.content.Context;
import android.graphics.Bitmap;
import android.media.MediaMetadataRetriever;
import kotlin.Pair;
import timber.log.Timber;
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.data.ActiveServerProvider;
import org.moire.ultrasonic.domain.Artist;
import org.moire.ultrasonic.domain.Bookmark;
import org.moire.ultrasonic.domain.ChatMessage;
import org.moire.ultrasonic.domain.Genre;
import org.moire.ultrasonic.domain.Indexes;
import org.moire.ultrasonic.domain.JukeboxStatus;
@ -34,10 +36,12 @@ import org.moire.ultrasonic.domain.Lyrics;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.domain.MusicFolder;
import org.moire.ultrasonic.domain.Playlist;
import org.moire.ultrasonic.domain.PodcastsChannel;
import org.moire.ultrasonic.domain.SearchCriteria;
import org.moire.ultrasonic.domain.SearchResult;
import org.moire.ultrasonic.domain.Share;
import org.moire.ultrasonic.domain.UserInfo;
import org.moire.ultrasonic.util.CancellableTask;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.FileUtil;
import org.moire.ultrasonic.util.ProgressListener;
@ -48,6 +52,7 @@ import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
@ -68,22 +73,11 @@ import static org.koin.java.KoinJavaComponent.inject;
/**
* @author Sindre Mehus
*/
public class OfflineMusicService extends RESTMusicService
public class OfflineMusicService implements MusicService
{
private static final Pattern COMPILE = Pattern.compile(" ");
private Lazy<ActiveServerProvider> activeServerProvider = inject(ActiveServerProvider.class);
public OfflineMusicService(SubsonicAPIClient subsonicAPIClient, PermanentFileStorage storage) {
super(subsonicAPIClient, storage);
}
@Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) throws Exception
{
return true;
}
@Override
public Indexes getIndexes(String musicFolderId, boolean refresh, Context context, ProgressListener progressListener) throws Exception
{
@ -150,7 +144,7 @@ public class OfflineMusicService extends RESTMusicService
}
@Override
public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener) throws Exception
public MusicDirectory getMusicDirectory(String id, String artistName, boolean refresh, Context context, ProgressListener progressListener)
{
File dir = new File(id);
MusicDirectory result = new MusicDirectory();
@ -341,7 +335,7 @@ public class OfflineMusicService extends RESTMusicService
}
@Override
public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
public Bitmap getAvatar(Context context, String username, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener)
{
try
{
@ -355,7 +349,7 @@ public class OfflineMusicService extends RESTMusicService
}
@Override
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener) throws Exception
public Bitmap getCoverArt(Context context, MusicDirectory.Entry entry, int size, boolean saveToFile, boolean highQuality, ProgressListener progressListener)
{
try
{
@ -369,25 +363,7 @@ public class OfflineMusicService extends RESTMusicService
}
@Override
public void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
{
throw new OfflineException("Star not available in offline mode");
}
@Override
public void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
{
throw new OfflineException("UnStar not available in offline mode");
}
@Override
public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception
{
throw new OfflineException("Music folders not available in offline mode");
}
@Override
public SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener) throws Exception
public SearchResult search(SearchCriteria criteria, Context context, ProgressListener progressListener)
{
List<Artist> artists = new ArrayList<Artist>();
List<MusicDirectory.Entry> albums = new ArrayList<MusicDirectory.Entry>();
@ -531,7 +507,7 @@ public class OfflineMusicService extends RESTMusicService
}
@Override
public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener) throws Exception
public List<Playlist> getPlaylists(boolean refresh, Context context, ProgressListener progressListener)
{
List<Playlist> playlists = new ArrayList<Playlist>();
File root = FileUtil.getPlaylistDirectory(context);
@ -661,6 +637,45 @@ public class OfflineMusicService extends RESTMusicService
}
}
@Override
public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener)
{
File root = FileUtil.getMusicDirectory(context);
List<File> children = new LinkedList<File>();
listFilesRecursively(root, children);
MusicDirectory result = new MusicDirectory();
if (children.isEmpty())
{
return result;
}
Random random = new java.security.SecureRandom();
for (int i = 0; i < size; i++)
{
File file = children.get(random.nextInt(children.size()));
result.addChild(createEntry(context, file, getName(file)));
}
return result;
}
private static void listFilesRecursively(File parent, List<File> children)
{
for (File file : FileUtil.listMediaFiles(parent))
{
if (file.isFile())
{
children.add(file);
}
else
{
listFilesRecursively(file, children);
}
}
}
@Override
public void deletePlaylist(String id, Context context, ProgressListener progressListener) throws Exception
{
@ -691,12 +706,6 @@ public class OfflineMusicService extends RESTMusicService
throw new OfflineException("Album lists not available in offline mode");
}
@Override
public String getVideoUrl(Context context, String id, boolean useFlash)
{
return null;
}
@Override
public JukeboxStatus updateJukeboxPlaylist(List<String> ids, Context context, ProgressListener progressListener) throws Exception
{
@ -739,29 +748,6 @@ public class OfflineMusicService extends RESTMusicService
throw new OfflineException("Starred not available in offline mode");
}
@Override
public MusicDirectory getRandomSongs(int size, Context context, ProgressListener progressListener) throws Exception
{
File root = FileUtil.getMusicDirectory(context);
List<File> children = new LinkedList<File>();
listFilesRecursively(root, children);
MusicDirectory result = new MusicDirectory();
if (children.isEmpty())
{
return result;
}
Random random = new java.security.SecureRandom();
for (int i = 0; i < size; i++)
{
File file = children.get(random.nextInt(children.size()));
result.addChild(createEntry(context, file, getName(file)));
}
return result;
}
@Override
public MusicDirectory getSongsByGenre(String genre, int count, int offset, Context context, ProgressListener progressListener) throws Exception
{
@ -804,18 +790,121 @@ public class OfflineMusicService extends RESTMusicService
throw new OfflineException("Updating shares not available in offline mode");
}
private static void listFilesRecursively(File parent, List<File> children)
@Override
public void star(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
{
for (File file : FileUtil.listMediaFiles(parent))
{
if (file.isFile())
{
children.add(file);
}
else
{
listFilesRecursively(file, children);
}
}
throw new OfflineException("Star not available in offline mode");
}
@Override
public void unstar(String id, String albumId, String artistId, Context context, ProgressListener progressListener) throws Exception
{
throw new OfflineException("UnStar not available in offline mode");
}
@Override
public List<MusicFolder> getMusicFolders(boolean refresh, Context context, ProgressListener progressListener) throws Exception
{
throw new OfflineException("Music folders not available in offline mode");
}
@Override
public MusicDirectory getAlbumList2(String type, int size, int offset, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getAlbumList2 was called but it isn't available");
return null;
}
@Override
public String getVideoUrl(Context context, String id, boolean useFlash) {
Timber.w("OfflineMusicService.getVideoUrl was called but it isn't available");
return null;
}
@Override
public List<ChatMessage> getChatMessages(Long since, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getChatMessages was called but it isn't available");
return null;
}
@Override
public void addChatMessage(String message, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.addChatMessage was called but it isn't available");
}
@Override
public List<Bookmark> getBookmarks(Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getBookmarks was called but it isn't available");
return null;
}
@Override
public void deleteBookmark(String id, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.deleteBookmark was called but it isn't available");
}
@Override
public void createBookmark(String id, int position, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.createBookmark was called but it isn't available");
}
@Override
public MusicDirectory getVideos(boolean refresh, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getVideos was called but it isn't available");
return null;
}
@Override
public SearchResult getStarred2(Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getStarred2 was called but it isn't available");
return null;
}
@Override
public void ping(Context context, ProgressListener progressListener) {
}
@Override
public boolean isLicenseValid(Context context, ProgressListener progressListener) {
return true;
}
@Override
public Indexes getArtists(boolean refresh, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getArtists was called but it isn't available");
return null;
}
@Override
public MusicDirectory getArtist(String id, String name, boolean refresh, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getArtist was called but it isn't available");
return null;
}
@Override
public MusicDirectory getAlbum(String id, String name, boolean refresh, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getAlbum was called but it isn't available");
return null;
}
@Override
public MusicDirectory getPodcastEpisodes(String podcastChannelId, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getPodcastEpisodes was called but it isn't available");
return null;
}
@Override
public Pair<InputStream, Boolean> getDownloadInputStream(Context context, MusicDirectory.Entry song, long offset, int maxBitrate, CancellableTask task) {
Timber.w("OfflineMusicService.getDownloadInputStream was called but it isn't available");
return null;
}
@Override
public void setRating(String id, int rating, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.setRating was called but it isn't available");
}
@Override
public List<PodcastsChannel> getPodcastsChannels(boolean refresh, Context context, ProgressListener progressListener) {
Timber.w("OfflineMusicService.getPodcastsChannels was called but it isn't available");
return null;
}
}

View File

@ -9,7 +9,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputLayout
import java.io.IOException
import java.net.MalformedURLException
import java.net.URL
import org.koin.android.ext.android.inject
@ -19,16 +18,14 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.service.ApiCallResponseChecker.Companion.checkResponseSuccessful
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.service.SubsonicRESTException
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.ErrorDialog
import org.moire.ultrasonic.util.ModalBackgroundTask
import org.moire.ultrasonic.util.Util
import retrofit2.Response
import timber.log.Timber
/**
@ -87,6 +84,17 @@ internal class EditServerActivity : AppCompatActivity() {
if (t != null) {
currentServerSetting = t
setFields()
// Remove the minimum API version so it can be detected again
if (currentServerSetting?.minimumApiVersion != null) {
currentServerSetting!!.minimumApiVersion = null
serverSettingsModel.updateItem(currentServerSetting)
if (
activeServerProvider.getActiveServer().id ==
currentServerSetting!!.id
) {
MusicServiceFactory.resetMusicService()
}
}
}
}
)
@ -260,7 +268,18 @@ internal class EditServerActivity : AppCompatActivity() {
BuildConfig.DEBUG
)
val subsonicApiClient = SubsonicAPIClient(configuration)
val pingResponse = subsonicApiClient.api.ping().execute()
// Execute a ping to retrieve the API version.
// This is accepted to fail if the authentication is incorrect yet.
var pingResponse = subsonicApiClient.api.ping().execute()
if (pingResponse?.body() != null) {
val restApiVersion = pingResponse.body()!!.version.restApiVersion
currentServerSetting!!.minimumApiVersion = restApiVersion
Timber.i("Server minimum API version set to %s", restApiVersion)
}
// Execute a ping to check the authentication, now using the correct API version.
pingResponse = subsonicApiClient.api.ping().execute()
checkResponseSuccessful(pingResponse)
val licenseResponse = subsonicApiClient.api.getLicense().execute()
@ -292,28 +311,6 @@ internal class EditServerActivity : AppCompatActivity() {
task.execute()
}
/**
* Checks the Subsonic Response for application specific errors
*/
private fun checkResponseSuccessful(response: Response<out SubsonicResponse?>) {
if (
response.isSuccessful &&
response.body()!!.status === SubsonicResponse.Status.OK
) {
return
}
if (!response.isSuccessful) {
throw IOException("Server error, code: " + response.code())
} else if (
response.body()!!.status === SubsonicResponse.Status.ERROR &&
response.body()!!.error != null
) {
throw SubsonicRESTException(response.body()!!.error!!)
} else {
throw IOException("Failed to perform request: " + response.code())
}
}
/**
* Finishes the Activity, after confirmation from the user if needed
*/

View File

@ -209,7 +209,8 @@ class ServerSettingsModel(
false
),
settings.getBoolean(PREFERENCES_KEY_LDAP_SUPPORT + preferenceId, false),
settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + preferenceId, null)
settings.getString(PREFERENCES_KEY_MUSIC_FOLDER_ID + preferenceId, null),
null
)
}

View File

@ -57,7 +57,8 @@ class ActiveServerProvider(
jukeboxByDefault = false,
allowSelfSignedCertificate = false,
ldapSupport = false,
musicFolderId = ""
musicFolderId = "",
minimumApiVersion = null
)
}
@ -79,6 +80,18 @@ class ActiveServerProvider(
}
}
/**
* Sets the minimum Subsonic API version of the current server.
*/
fun setMinimumApiVersion(apiVersion: String) {
GlobalScope.launch(Dispatchers.IO) {
if (cachedServer != null) {
cachedServer!!.minimumApiVersion = apiVersion
repository.update(cachedServer!!)
}
}
}
/**
* Invalidates the Active Server Setting cache
* This should be called when the Active Server or one of its properties changes

View File

@ -2,11 +2,13 @@ package org.moire.ultrasonic.data
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
/**
* Room Database to be used to store data for Ultrasonic
*/
@Database(entities = [ServerSetting::class], version = 1)
@Database(entities = [ServerSetting::class], version = 2)
abstract class AppDatabase : RoomDatabase() {
/**
@ -14,3 +16,11 @@ abstract class AppDatabase : RoomDatabase() {
*/
abstract fun serverSettingDao(): ServerSettingDao
}
val MIGRATION_1_2: Migration = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"ALTER TABLE ServerSetting ADD COLUMN minimumApiVersion TEXT"
)
}
}

View File

@ -28,12 +28,13 @@ data class ServerSetting(
@ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean,
@ColumnInfo(name = "allowSelfSignedCertificate") var allowSelfSignedCertificate: Boolean,
@ColumnInfo(name = "ldapSupport") var ldapSupport: Boolean,
@ColumnInfo(name = "musicFolderId") var musicFolderId: String?
@ColumnInfo(name = "musicFolderId") var musicFolderId: String?,
@ColumnInfo(name = "minimumApiVersion") var minimumApiVersion: String?
) {
constructor() : this (
-1, 0, "", "", "", "", false, false, false, null
-1, 0, "", "", "", "", false, false, false, null, null
)
constructor(name: String, url: String) : this(
-1, 0, name, url, "", "", false, false, false, null
-1, 0, name, url, "", "", false, false, false, null, null
)
}

View File

@ -8,6 +8,7 @@ import org.koin.dsl.module
import org.moire.ultrasonic.activity.ServerSettingsModel
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.AppDatabase
import org.moire.ultrasonic.data.MIGRATION_1_2
import org.moire.ultrasonic.util.Util
const val SP_NAME = "Default_SP"
@ -20,7 +21,10 @@ val appPermanentStorage = module {
androidContext(),
AppDatabase::class.java,
"ultrasonic-database"
).build()
)
.addMigrations(MIGRATION_1_2)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
}
single { get<AppDatabase>().serverSettingDao() }

View File

@ -13,6 +13,7 @@ import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.log.TimberOkHttpLogger
import org.moire.ultrasonic.service.ApiCallResponseChecker
import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService
@ -46,7 +47,8 @@ val musicServiceModule = module {
username = get<ActiveServerProvider>().getActiveServer().userName,
password = get<ActiveServerProvider>().getActiveServer().password,
minimalProtocolVersion = SubsonicAPIVersions.getClosestKnownClientApiVersion(
Constants.REST_PROTOCOL_VERSION
get<ActiveServerProvider>().getActiveServer().minimumApiVersion
?: Constants.REST_PROTOCOL_VERSION
),
clientID = Constants.REST_CLIENT_ID,
allowSelfSignedCertificate = get<ActiveServerProvider>()
@ -58,13 +60,14 @@ val musicServiceModule = module {
single<HttpLoggingInterceptor.Logger> { TimberOkHttpLogger() }
single { SubsonicAPIClient(get(), get()) }
single { ApiCallResponseChecker(get(), get()) }
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
CachedMusicService(RESTMusicService(get(), get()))
CachedMusicService(RESTMusicService(get(), get(), get(), get()))
}
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
OfflineMusicService(get(), get())
OfflineMusicService()
}
single { SubsonicImageLoader(androidContext(), get()) }

View File

@ -0,0 +1,66 @@
package org.moire.ultrasonic.service
import java.io.IOException
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.data.ActiveServerProvider
import retrofit2.Response
import timber.log.Timber
/**
* This call wraps Subsonic API calls so their results can be checked for errors, API version, etc
*/
class ApiCallResponseChecker(
private val subsonicAPIClient: SubsonicAPIClient,
private val activeServerProvider: ActiveServerProvider
) {
/**
* Executes a Subsonic API call with response check
*/
@Throws(SubsonicRESTException::class, IOException::class)
fun <T : Response<out SubsonicResponse>> callWithResponseCheck(
call: (SubsonicAPIDefinition) -> T
): T {
// Check for API version when first contacting the server
if (activeServerProvider.getActiveServer().minimumApiVersion == null) {
try {
val response = subsonicAPIClient.api.ping().execute()
if (response?.body() != null) {
val restApiVersion = response.body()!!.version.restApiVersion
Timber.i("Server minimum API version set to %s", restApiVersion)
activeServerProvider.setMinimumApiVersion(restApiVersion)
}
} catch (ignored: Exception) {
// This Ping is only used to get the API Version, if it fails, that's no problem.
}
}
// This call will be now executed with the correct API Version, so it shouldn't fail
val result = call.invoke(subsonicAPIClient.api)
checkResponseSuccessful(result)
return result
}
/**
* Creates Exceptions from the results returned by the Subsonic API
*/
companion object {
@Throws(SubsonicRESTException::class, IOException::class)
fun checkResponseSuccessful(response: Response<out SubsonicResponse>) {
if (response.isSuccessful && response.body()!!.status === SubsonicResponse.Status.OK) {
return
}
if (!response.isSuccessful) {
throw IOException("Server error, code: " + response.code())
} else if (
response.body()!!.status === SubsonicResponse.Status.ERROR &&
response.body()!!.error != null
) {
throw SubsonicRESTException(response.body()!!.error!!)
} else {
throw IOException("Failed to perform request: " + response.code())
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -309,9 +309,9 @@
<string name="settings.theme_black">Black</string>
<string name="settings.theme_title">Theme</string>
<string name="settings.title.allow_self_signed_certificate">Allow self-signed HTTPS certificate</string>
<string name="settings.title.enable_ldap_users_support">Enable support for LDAP users</string>
<string name="settings.summary.enable_ldap_users_support">This forces app to always send password in old-way,
because Subsonic api does not support new authorization for LDAP users.</string>
<string name="settings.title.enable_ldap_users_support">Force plain password authentication</string>
<string name="settings.summary.enable_ldap_users_support">This forces the app to always send the password unencrypted.
Useful if the Subsonic server does not support the new authentication API for the users.</string>
<string name="settings.use_folder_for_album_artist">Use Folders For Artist Name</string>
<string name="settings.use_folder_for_album_artist_summary">Assume top-level folder is the name of the album artist</string>
<string name="settings.use_id3">Browse Using ID3 Tags</string>