Merge pull request #204 from ultrasonic/introduce-di

Introduce initial dependency injection setup.
This commit is contained in:
Yahor Berdnikau 2018-06-24 17:36:51 +02:00 committed by GitHub
commit 50157eb811
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 328 additions and 194 deletions

View File

@ -20,6 +20,7 @@ ext.versions = [
okhttp : "3.10.0",
semver : "1.0.0",
twitterSerial : "0.1.6",
koin : "0.9.3",
junit : "4.12",
mockito : "2.16.0",
@ -51,6 +52,8 @@ ext.other = [
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
semver : "net.swiftzer.semver:semver:$versions.semver",
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
koinCore : "org.koin:koin-core:$versions.koin",
koinAndroid : "org.koin:koin-android:$versions.koin"
]
ext.testing = [

View File

@ -13,6 +13,8 @@ dependencies {
api other.kotlinStdlib
api other.retrofit
api other.jacksonConverter
api other.koinCore
implementation(other.jacksonKotlin) {
exclude module: 'kotlin-reflect'
}
@ -36,7 +38,8 @@ jacoco {
ext {
// Excluding data classes
jacocoExclude = [
'**/models/**'
'**/models/**',
'**/di/**'
]
}

View File

@ -21,8 +21,14 @@ class GetStreamUrlTest {
@Before
fun setUp() {
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME, PASSWORD, V1_6_0, CLIENT_ID)
val config = SubsonicClientConfiguration(
mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME,
PASSWORD,
V1_6_0,
CLIENT_ID
)
client = SubsonicAPIClient(config)
val baseExpectedUrl = mockWebServerRule.mockWebServer.url("").toString()
expectedUrl = "$baseExpectedUrl/rest/stream.view?id=$id&u=$USERNAME" +
"&c=$CLIENT_ID&f=json&v=${V1_6_0.restApiVersion}&p=enc:${PASSWORD.toHexBytes()}"

View File

@ -10,11 +10,18 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
abstract class SubsonicAPIClientTest {
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
protected lateinit var config: SubsonicClientConfiguration
protected lateinit var client: SubsonicAPIClient
@Before
fun setUp() {
client = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME, PASSWORD, CLIENT_VERSION, CLIENT_ID)
config = SubsonicClientConfiguration(
mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME,
PASSWORD,
CLIENT_VERSION,
CLIENT_ID
)
client = SubsonicAPIClient(config)
}
}

View File

@ -10,8 +10,9 @@ import org.junit.Test
class SubsonicApiPasswordTest : SubsonicAPIClientTest() {
@Test
fun `Should pass PasswordMD5Interceptor in query params for api version 1 13 0`() {
val clientV12 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME, PASSWORD, SubsonicAPIVersions.V1_14_0, CLIENT_ID)
val clientV12 = SubsonicAPIClient(
config.copy(minimalProtocolVersion = SubsonicAPIVersions.V1_14_0)
)
mockWebServerRule.enqueueResponse("ping_ok.json")
clientV12.api.ping().execute()
@ -25,8 +26,9 @@ class SubsonicApiPasswordTest : SubsonicAPIClientTest() {
@Test
fun `Should pass PasswordHexInterceptor in query params for api version 1 12 0`() {
val clientV11 = SubsonicAPIClient(mockWebServerRule.mockWebServer.url("/").toString(),
USERNAME, PASSWORD, SubsonicAPIVersions.V1_12_0, CLIENT_ID)
val clientV11 = SubsonicAPIClient(
config.copy(minimalProtocolVersion = SubsonicAPIVersions.V1_12_0)
)
mockWebServerRule.enqueueResponse("ping_ok.json")
clientV11.api.ping().execute()

View File

@ -90,7 +90,15 @@ class SubsonicApiSSLTest {
assertResponseSuccessful(response)
}
private fun createSubsonicClient(allowSelfSignedCertificate: Boolean) = SubsonicAPIClient(
"https://$HOST:$PORT/", USERNAME, PASSWORD, CLIENT_VERSION, CLIENT_ID,
allowSelfSignedCertificate = allowSelfSignedCertificate)
private fun createSubsonicClient(allowSelfSignedCertificate: Boolean): SubsonicAPIClient {
val config = SubsonicClientConfiguration(
"https://$HOST:$PORT/",
USERNAME,
PASSWORD,
CLIENT_VERSION,
CLIENT_ID,
allowSelfSignedCertificate = allowSelfSignedCertificate
)
return SubsonicAPIClient(config)
}
}

View File

@ -36,44 +36,39 @@ private const val READ_TIMEOUT = 60_000L
* @author Yahor Berdnikau
*/
class SubsonicAPIClient(
baseUrl: String,
username: String,
password: String,
minimalProtocolVersion: SubsonicAPIVersions,
clientID: String,
allowSelfSignedCertificate: Boolean = false,
enableLdapUserSupport: Boolean = false,
debug: Boolean = false
config: SubsonicClientConfiguration,
baseOkClient: OkHttpClient = OkHttpClient.Builder().build()
) {
private val versionInterceptor = VersionInterceptor(minimalProtocolVersion) {
private val versionInterceptor = VersionInterceptor(config.minimalProtocolVersion) {
protocolVersion = it
}
private val proxyPasswordInterceptor = ProxyPasswordInterceptor(
minimalProtocolVersion,
PasswordHexInterceptor(password),
PasswordMD5Interceptor(password),
enableLdapUserSupport)
config.minimalProtocolVersion,
PasswordHexInterceptor(config.password),
PasswordMD5Interceptor(config.password),
config.enableLdapUserSupport
)
/**
* Get currently used protocol version.
*/
var protocolVersion = minimalProtocolVersion
var protocolVersion = config.minimalProtocolVersion
private set(value) {
field = value
proxyPasswordInterceptor.apiVersion = field
wrappedApi.currentApiVersion = field
}
private val okHttpClient = OkHttpClient.Builder()
private val okHttpClient = baseOkClient.newBuilder()
.readTimeout(READ_TIMEOUT, MILLISECONDS)
.apply { if (allowSelfSignedCertificate) allowSelfSignedCertificates() }
.apply { if (config.allowSelfSignedCertificate) allowSelfSignedCertificates() }
.addInterceptor { chain ->
// Adds default request params
val originalRequest = chain.request()
val newUrl = originalRequest.url().newBuilder()
.addQueryParameter("u", username)
.addQueryParameter("c", clientID)
.addQueryParameter("u", config.username)
.addQueryParameter("c", config.clientID)
.addQueryParameter("f", "json")
.build()
chain.proceed(originalRequest.newBuilder().url(newUrl).build())
@ -81,7 +76,7 @@ class SubsonicAPIClient(
.addInterceptor(versionInterceptor)
.addInterceptor(proxyPasswordInterceptor)
.addInterceptor(RangeHeaderInterceptor())
.apply { if (debug) addLogging() }
.apply { if (config.debug) addLogging() }
.build()
private val jacksonMapper = ObjectMapper()
@ -90,14 +85,15 @@ class SubsonicAPIClient(
.registerModule(KotlinModule())
private val retrofit = Retrofit.Builder()
.baseUrl("$baseUrl/rest/")
.baseUrl("${config.baseUrl}/rest/")
.client(okHttpClient)
.addConverterFactory(JacksonConverterFactory.create(jacksonMapper))
.build()
private val wrappedApi = ApiVersionCheckWrapper(
retrofit.create(SubsonicAPIDefinition::class.java),
minimalProtocolVersion)
config.minimalProtocolVersion
)
val api: SubsonicAPIDefinition get() = wrappedApi

View File

@ -0,0 +1,15 @@
package org.moire.ultrasonic.api.subsonic
/**
* Provides configuration for [SubsonicAPIClient].
*/
data class SubsonicClientConfiguration(
val baseUrl: String,
val username: String,
val password: String,
val minimalProtocolVersion: SubsonicAPIVersions,
val clientID: String,
val allowSelfSignedCertificate: Boolean = false,
val enableLdapUserSupport: Boolean = false,
val debug: Boolean = false
)

View File

@ -0,0 +1,10 @@
package org.moire.ultrasonic.api.subsonic.di
import org.koin.dsl.context.Context
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
const val SUBSONIC_API_CLIENT_CONTEXT = "SubsonicApiClientContext"
fun Context.subsonicApiModule() = context(SUBSONIC_API_CLIENT_CONTEXT) {
bean { return@bean SubsonicAPIClient(get(), get()) }
}

View File

@ -61,6 +61,7 @@ dependencies {
implementation androidSupport.design
implementation other.kotlinStdlib
implementation other.koinAndroid
testImplementation other.kotlinReflect
testImplementation testing.junit
@ -69,7 +70,7 @@ dependencies {
testImplementation testing.kluent
}
// Excluding all non-kotlin classes
// Excluding all java classes and stuff that should not be covered
ext {
jacocoExclude = [
'**/activity/**',
@ -83,7 +84,8 @@ ext {
'**/view/**',
'**/R$*.class',
'**/R.class',
'**/BuildConfig.class'
'**/BuildConfig.class',
'**/di/**'
]
}
jacocoAndroidUnitTestReport {

View File

@ -23,6 +23,7 @@
a:icon="@mipmap/ic_launcher"
a:roundIcon="@mipmap/ic_launcher_round"
a:theme="@style/Theme.AppCompat"
a:name=".app.UApp"
a:label="@string/common.appname">
<activity
a:name=".activity.MainActivity"

View File

@ -14,6 +14,7 @@ import android.view.View;
import org.moire.ultrasonic.BuildConfig;
import org.moire.ultrasonic.R;
import org.moire.ultrasonic.cache.Directories;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.service.MusicService;
import org.moire.ultrasonic.service.MusicServiceFactory;
@ -282,9 +283,10 @@ public class ServerSettingsFragment extends PreferenceFragment
.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0);
// Clear permanent storage
final String storageServerId = MusicServiceFactory.getServerId(sharedPreferences, serverId);
final String storageServerId = MusicServiceFactory.getServerId();
final Directories directories = MusicServiceFactory.getDirectories();
final PermanentFileStorage fileStorage = new PermanentFileStorage(
MusicServiceFactory.getDirectories(getActivity()),
directories,
storageServerId,
BuildConfig.DEBUG
);

View File

@ -1,150 +0,0 @@
/*
This file is part of Subsonic.
Subsonic 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 3 of the License, or
(at your option) any later version.
Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.service;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.jetbrains.annotations.NotNull;
import org.moire.ultrasonic.BuildConfig;
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions;
import org.moire.ultrasonic.cache.Directories;
import org.moire.ultrasonic.cache.PermanentFileStorage;
import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Util;
import java.io.File;
/**
* @author Sindre Mehus
* @version $Id$
*/
public class MusicServiceFactory {
private static final String LOG_TAG = MusicServiceFactory.class.getSimpleName();
private static MusicService REST_MUSIC_SERVICE = null;
private static MusicService OFFLINE_MUSIC_SERVICE = null;
public static MusicService getMusicService(Context context) {
if (Util.isOffline(context)) {
Log.d(LOG_TAG, "App is offline, returning offline music service.");
if (OFFLINE_MUSIC_SERVICE == null) {
synchronized (MusicServiceFactory.class) {
if (OFFLINE_MUSIC_SERVICE == null) {
Log.d(LOG_TAG, "Creating new offline music service");
OFFLINE_MUSIC_SERVICE = new OfflineMusicService(
createSubsonicApiClient(context),
getPermanentFileStorage(context));
}
}
}
return OFFLINE_MUSIC_SERVICE;
} else {
Log.d(LOG_TAG, "Returning rest music service");
if (REST_MUSIC_SERVICE == null) {
synchronized (MusicServiceFactory.class) {
if (REST_MUSIC_SERVICE == null) {
Log.d(LOG_TAG, "Creating new rest music service");
REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService(
createSubsonicApiClient(context),
getPermanentFileStorage(context)));
}
}
}
return REST_MUSIC_SERVICE;
}
}
/**
* Resets {@link MusicService} to initial state, so on next call to {@link #getMusicService(Context)}
* it will return updated instance of it.
*/
public static void resetMusicService() {
Log.d(LOG_TAG, "Resetting music service");
synchronized (MusicServiceFactory.class) {
REST_MUSIC_SERVICE = null;
OFFLINE_MUSIC_SERVICE = null;
}
}
private static SubsonicAPIClient createSubsonicApiClient(final Context context) {
final SharedPreferences preferences = Util.getPreferences(context);
int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
String serverUrl = preferences.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
String username = preferences.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null);
String password = preferences.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null);
boolean allowSelfSignedCertificate = preferences
.getBoolean(Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + instance, false);
boolean enableLdapUserSupport = preferences
.getBoolean(Constants.PREFERENCES_KEY_LDAP_SUPPORT + instance , false);
if (serverUrl == null ||
username == null ||
password == null) {
Log.i("MusicServiceFactory", "Server credentials is not available");
return new SubsonicAPIClient("http://localhost", "", "",
SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION),
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
enableLdapUserSupport, BuildConfig.DEBUG);
}
return new SubsonicAPIClient(serverUrl, username, password,
SubsonicAPIVersions.fromApiVersion(Constants.REST_PROTOCOL_VERSION),
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
enableLdapUserSupport, BuildConfig.DEBUG);
}
private static PermanentFileStorage getPermanentFileStorage(final Context context) {
final SharedPreferences preferences = Util.getPreferences(context);
int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
final String serverId = getServerId(preferences, instance);
return new PermanentFileStorage(getDirectories(context), serverId, BuildConfig.DEBUG);
}
public static String getServerId(final SharedPreferences sp, final int instance) {
String serverUrl = sp.getString(
Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
return String.valueOf(Math.abs((serverUrl + instance).hashCode()));
}
public static Directories getDirectories(final Context context) {
return new Directories() {
@NotNull
@Override
public File getInternalCacheDir() {
return context.getCacheDir();
}
@NotNull
@Override
public File getInternalDataDir() {
return context.getFilesDir();
}
@Override
public File getExternalCacheDir() {
return context.getExternalCacheDir();
}
};
}
}

View File

@ -171,14 +171,16 @@ public class Util extends DownloadActivity
return preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_LOCK_SCREEN_CONTROLS, false);
}
public static void setActiveServer(Context context, int instance)
{
public static void setActiveServer(
Context context,
int instance
) {
MusicServiceFactory.resetMusicService();
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
editor.commit();
}
SharedPreferences preferences = getPreferences(context);
SharedPreferences.Editor editor = preferences.edit();
editor.putInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, instance);
editor.apply();
}
public static int getActiveServer(Context context)
{

View File

@ -0,0 +1,21 @@
package org.moire.ultrasonic.app
import android.app.Application
import org.koin.android.ext.android.startKoin
import org.moire.ultrasonic.di.baseNetworkModule
import org.moire.ultrasonic.di.directoriesModule
import org.moire.ultrasonic.di.musicServiceModule
import org.moire.ultrasonic.util.Util
class UApp : Application() {
override fun onCreate() {
super.onCreate()
val sharedPreferences = Util.getPreferences(this)
startKoin(this, listOf(
directoriesModule,
baseNetworkModule,
musicServiceModule(sharedPreferences)
))
}
}

View File

@ -0,0 +1,17 @@
package org.moire.ultrasonic.cache
import android.content.Context
import java.io.File
/**
* Provides specific to Android implementation of [Directories].
*/
class AndroidDirectories(
private val context: Context
) : Directories {
override fun getInternalCacheDir(): File = context.cacheDir
override fun getInternalDataDir(): File = context.filesDir
override fun getExternalCacheDir(): File? = context.externalCacheDir
}

View File

@ -0,0 +1,11 @@
package org.moire.ultrasonic.di
import okhttp3.OkHttpClient
import org.koin.dsl.module.applicationContext
/**
* Provides base network dependencies.
*/
val baseNetworkModule = applicationContext {
bean { OkHttpClient.Builder().build() }
}

View File

@ -0,0 +1,9 @@
package org.moire.ultrasonic.di
import org.koin.dsl.module.applicationContext
import org.moire.ultrasonic.cache.AndroidDirectories
import org.moire.ultrasonic.cache.Directories
val directoriesModule = applicationContext {
bean { AndroidDirectories(get()) } bind Directories::class
}

View File

@ -0,0 +1,113 @@
@file:JvmName("MusicServiceModule")
package org.moire.ultrasonic.di
import android.content.SharedPreferences
import android.util.Log
import org.koin.dsl.module.applicationContext
import org.moire.ultrasonic.BuildConfig
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.di.subsonicApiModule
import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.service.CachedMusicService
import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.OfflineMusicService
import org.moire.ultrasonic.service.RESTMusicService
import org.moire.ultrasonic.util.Constants
import kotlin.math.abs
internal const val MUSIC_SERVICE_CONTEXT = "CurrentMusicService"
internal const val ONLINE_MUSIC_SERVICE = "OnlineMusicService"
internal const val OFFLINE_MUSIC_SERVICE = "OfflineMusicService"
private const val DEFAULT_SERVER_INSTANCE = 1
private const val UNKNOWN_SERVER_URL = "not-exists"
private const val LOG_TAG = "MusicServiceModule"
fun musicServiceModule(sp: SharedPreferences) = applicationContext {
context(MUSIC_SERVICE_CONTEXT) {
subsonicApiModule()
bean(name = "ServerInstance") {
return@bean sp.getInt(
Constants.PREFERENCES_KEY_SERVER_INSTANCE,
DEFAULT_SERVER_INSTANCE
)
}
bean(name = "ServerID") {
val serverInstance = get<Int>(name = "ServerInstance")
val serverUrl = sp.getString(
Constants.PREFERENCES_KEY_SERVER_URL + serverInstance,
null
)
return@bean if (serverUrl == null) {
UNKNOWN_SERVER_URL
} else {
abs("$serverUrl$serverInstance".hashCode()).toString()
}
}
bean {
val serverId = get<String>(name = "ServerID")
return@bean PermanentFileStorage(get(), serverId, BuildConfig.DEBUG)
}
bean {
val instance = get<Int>(name = "ServerInstance")
val serverUrl = sp.getString(Constants.PREFERENCES_KEY_SERVER_URL + instance, null)
val username = sp.getString(Constants.PREFERENCES_KEY_USERNAME + instance, null)
val password = sp.getString(Constants.PREFERENCES_KEY_PASSWORD + instance, null)
val allowSelfSignedCertificate = sp.getBoolean(
Constants.PREFERENCES_KEY_ALLOW_SELF_SIGNED_CERTIFICATE + instance,
false
)
val enableLdapUserSupport = sp.getBoolean(
Constants.PREFERENCES_KEY_LDAP_SUPPORT + instance,
false
)
if (serverUrl == null ||
username == null ||
password == null
) {
Log.i(LOG_TAG, "Server credentials is not available")
return@bean SubsonicClientConfiguration(
baseUrl = "http://localhost",
username = "",
password = "",
minimalProtocolVersion = SubsonicAPIVersions.fromApiVersion(
Constants.REST_PROTOCOL_VERSION
),
clientID = Constants.REST_CLIENT_ID,
allowSelfSignedCertificate = allowSelfSignedCertificate,
enableLdapUserSupport = enableLdapUserSupport,
debug = BuildConfig.DEBUG
)
} else {
return@bean SubsonicClientConfiguration(
baseUrl = serverUrl,
username = username,
password = password,
minimalProtocolVersion = SubsonicAPIVersions.fromApiVersion(
Constants.REST_PROTOCOL_VERSION
),
clientID = Constants.REST_CLIENT_ID,
allowSelfSignedCertificate = allowSelfSignedCertificate,
enableLdapUserSupport = enableLdapUserSupport,
debug = BuildConfig.DEBUG
)
}
}
bean { return@bean SubsonicAPIClient(get()) }
bean<MusicService>(name = ONLINE_MUSIC_SERVICE) {
return@bean CachedMusicService(RESTMusicService(get(), get()))
}
bean<MusicService>(name = OFFLINE_MUSIC_SERVICE) {
return@bean OfflineMusicService(get(), get())
}
}
}

View File

@ -0,0 +1,56 @@
/*
This file is part of Subsonic.
Subsonic 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 3 of the License, or
(at your option) any later version.
Subsonic 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 Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2009 (C) Sindre Mehus
*/
package org.moire.ultrasonic.service
import android.content.Context
import org.koin.standalone.KoinComponent
import org.koin.standalone.get
import org.koin.standalone.releaseContext
import org.moire.ultrasonic.cache.Directories
import org.moire.ultrasonic.di.MUSIC_SERVICE_CONTEXT
import org.moire.ultrasonic.di.OFFLINE_MUSIC_SERVICE
import org.moire.ultrasonic.di.ONLINE_MUSIC_SERVICE
import org.moire.ultrasonic.util.Util
@Deprecated("Use DI way to get MusicService")
object MusicServiceFactory : KoinComponent {
@JvmStatic
fun getMusicService(context: Context): MusicService {
return if (Util.isOffline(context)) {
get(OFFLINE_MUSIC_SERVICE)
} else {
get(ONLINE_MUSIC_SERVICE)
}
}
/**
* Resets [MusicService] to initial state, so on next call to [.getMusicService]
* it will return updated instance of it.
*/
@JvmStatic
fun resetMusicService() {
releaseContext(MUSIC_SERVICE_CONTEXT)
}
@JvmStatic
fun getServerId() = get<String>(name = "ServerID")
@JvmStatic
fun getDirectories() = get<Directories>()
}