mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-05 18:13:05 +03:00
Bunch of fixes 💐
This commit is contained in:
parent
5222e952aa
commit
548ecc6517
@ -155,11 +155,17 @@ class SubsonicAPIClient(
|
|||||||
return call.toStreamResponse()
|
return call.toStreamResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val isOffline by lazy {
|
||||||
|
config.baseUrl == OFFLINE_DB_URL
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val jacksonMapper: ObjectMapper = ObjectMapper()
|
val jacksonMapper: ObjectMapper = ObjectMapper()
|
||||||
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
|
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
|
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
|
||||||
.registerModule(KotlinModule.Builder().build())
|
.registerModule(KotlinModule.Builder().build())
|
||||||
|
|
||||||
|
const val OFFLINE_DB_URL = "http://localhost"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,17 +12,6 @@
|
|||||||
column="5"/>
|
column="5"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="AllowAllHostnameVerifier"
|
|
||||||
message="Using the `AllowAllHostnameVerifier` HostnameVerifier is unsafe because it always returns true, which could cause insecure network traffic due to trusting TLS/SSL server certificates for wrong hostnames"
|
|
||||||
errorLine1=" public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER = new AllowAllHostnameVerifier();"
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/java/org/moire/ultrasonic/service/ssl/SSLSocketFactory.java"
|
|
||||||
line="142"
|
|
||||||
column="73"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="TrustAllX509TrustManager"
|
id="TrustAllX509TrustManager"
|
||||||
message="`checkClientTrusted` is empty, which could cause insecure network traffic due to trusting arbitrary TLS/SSL certificates presented by peers">
|
message="`checkClientTrusted` is empty, which could cause insecure network traffic due to trusting arbitrary TLS/SSL certificates presented by peers">
|
||||||
@ -297,7 +286,7 @@
|
|||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/layout/share_details.xml"
|
file="src/main/res/layout/share_details.xml"
|
||||||
line="30"
|
line="29"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
@ -374,7 +363,7 @@
|
|||||||
errorLine2=" ~~~~~~~~">
|
errorLine2=" ~~~~~~~~">
|
||||||
<location
|
<location
|
||||||
file="src/main/res/layout/share_details.xml"
|
file="src/main/res/layout/share_details.xml"
|
||||||
line="30"
|
line="29"
|
||||||
column="10"/>
|
column="10"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
@ -400,17 +389,6 @@
|
|||||||
column="13"/>
|
column="13"/>
|
||||||
</issue>
|
</issue>
|
||||||
|
|
||||||
<issue
|
|
||||||
id="HardcodedText"
|
|
||||||
message="Hardcoded string "http://", should use `@string` resource"
|
|
||||||
errorLine1=" a:text="http://""
|
|
||||||
errorLine2=" ~~~~~~~~~~~~~~~~">
|
|
||||||
<location
|
|
||||||
file="src/main/res/layout/server_edit.xml"
|
|
||||||
line="43"
|
|
||||||
column="13"/>
|
|
||||||
</issue>
|
|
||||||
|
|
||||||
<issue
|
<issue
|
||||||
id="RelativeOverlap"
|
id="RelativeOverlap"
|
||||||
message="`@id/current_playing_duration` can overlap `@id/current_playing_position` if @string/util.no_time, @string/util.no_time grow due to localized text expansion"
|
message="`@id/current_playing_duration` can overlap `@id/current_playing_position` if @string/util.no_time, @string/util.no_time grow due to localized text expansion"
|
||||||
|
@ -127,12 +127,12 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
<provider
|
<provider
|
||||||
android:name=".provider.SearchSuggestionProvider"
|
android:name=".provider.SearchSuggestionProvider"
|
||||||
android:authorities="org.moire.ultrasonic.provider.SearchSuggestionProvider"
|
android:authorities="${applicationId}.provider.SearchSuggestionProvider"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".provider.AlbumArtContentProvider"
|
android:name=".provider.AlbumArtContentProvider"
|
||||||
android:authorities="org.moire.ultrasonic.provider.AlbumArtContentProvider"
|
android:authorities="${applicationId}.provider.AlbumArtContentProvider"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.view.GravityCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.drawerlayout.widget.DrawerLayout
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.Player.STATE_BUFFERING
|
import androidx.media3.common.Player.STATE_BUFFERING
|
||||||
import androidx.media3.common.Player.STATE_READY
|
import androidx.media3.common.Player.STATE_READY
|
||||||
@ -39,10 +40,11 @@ import androidx.navigation.ui.navigateUp
|
|||||||
import androidx.navigation.ui.onNavDestinationSelected
|
import androidx.navigation.ui.onNavDestinationSelected
|
||||||
import androidx.navigation.ui.setupActionBarWithNavController
|
import androidx.navigation.ui.setupActionBarWithNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.navigation.NavigationView
|
import com.google.android.material.navigation.NavigationView
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.moire.ultrasonic.NavigationGraphDirections
|
import org.moire.ultrasonic.NavigationGraphDirections
|
||||||
@ -100,7 +102,6 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
private val serverRepository: ServerSettingDao by inject()
|
private val serverRepository: ServerSettingDao by inject()
|
||||||
|
|
||||||
private var infoDialogDisplayed = false
|
|
||||||
private var currentFragmentId: Int = 0
|
private var currentFragmentId: Int = 0
|
||||||
private var cachedServerCount: Int = 0
|
private var cachedServerCount: Int = 0
|
||||||
|
|
||||||
@ -177,15 +178,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine if this is a first run
|
// Determine if this is a first run
|
||||||
val showWelcomeScreen = Util.isFirstRun()
|
val showWelcomeScreen = UApp.instance!!.isFirstRun
|
||||||
|
|
||||||
// Migrate Feature storage if needed
|
|
||||||
// TODO: Remove in December 2022
|
|
||||||
if (!Settings.hasKey(Constants.PREFERENCES_KEY_USE_FIVE_STAR_RATING)) {
|
|
||||||
Settings.migrateFeatureStorage()
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSettings()
|
|
||||||
|
|
||||||
// This is a first run with only the demo entry inside the database
|
// This is a first run with only the demo entry inside the database
|
||||||
// We set the active server to the demo one and show the welcome dialog
|
// We set the active server to the demo one and show the welcome dialog
|
||||||
@ -227,6 +220,11 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
Storage.reset()
|
Storage.reset()
|
||||||
|
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
Storage.ensureRootIsAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
setMenuForServerCapabilities()
|
setMenuForServerCapabilities()
|
||||||
|
|
||||||
// Lifecycle support's constructor registers some event receivers so it should be created early
|
// Lifecycle support's constructor registers some event receivers so it should be created early
|
||||||
@ -402,12 +400,12 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
*/
|
*/
|
||||||
override fun attachBaseContext(newBase: Context?) {
|
override fun attachBaseContext(newBase: Context?) {
|
||||||
val locale = Settings.overrideLanguage
|
val locale = Settings.overrideLanguage
|
||||||
val localeUpdatedContext: ContextWrapper = LocaleHelper.wrap(newBase, locale)
|
if (locale.isNotEmpty()) {
|
||||||
super.attachBaseContext(localeUpdatedContext)
|
val localeUpdatedContext: ContextWrapper = LocaleHelper.wrap(newBase, locale)
|
||||||
}
|
super.attachBaseContext(localeUpdatedContext)
|
||||||
|
} else {
|
||||||
private fun loadSettings() {
|
super.attachBaseContext(newBase)
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exit() {
|
private fun exit() {
|
||||||
@ -423,8 +421,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showWelcomeDialog() {
|
private fun showWelcomeDialog() {
|
||||||
if (!infoDialogDisplayed) {
|
if (!UApp.instance!!.setupDialogDisplayed) {
|
||||||
infoDialogDisplayed = true
|
|
||||||
|
|
||||||
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||||
|
|
||||||
@ -432,11 +429,13 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
.setTitle(R.string.main_welcome_title)
|
.setTitle(R.string.main_welcome_title)
|
||||||
.setMessage(R.string.main_welcome_text_demo)
|
.setMessage(R.string.main_welcome_text_demo)
|
||||||
.setNegativeButton(R.string.main_welcome_cancel) { dialog, _ ->
|
.setNegativeButton(R.string.main_welcome_cancel) { dialog, _ ->
|
||||||
|
UApp.instance!!.setupDialogDisplayed = true
|
||||||
// Go to the settings screen
|
// Go to the settings screen
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
findNavController(R.id.nav_host_fragment).navigate(R.id.serverSelectorFragment)
|
findNavController(R.id.nav_host_fragment).navigate(R.id.serverSelectorFragment)
|
||||||
}
|
}
|
||||||
.setPositiveButton(R.string.common_ok) { dialog, _ ->
|
.setPositiveButton(R.string.common_ok) { dialog, _ ->
|
||||||
|
UApp.instance!!.setupDialogDisplayed = true
|
||||||
// Add the demo server
|
// Add the demo server
|
||||||
val activeServerProvider: ActiveServerProvider by inject()
|
val activeServerProvider: ActiveServerProvider by inject()
|
||||||
val demoIndex = serverSettingsModel.addDemoServer()
|
val demoIndex = serverSettingsModel.addDemoServer()
|
||||||
|
@ -107,7 +107,7 @@ class ArtistRowBinder(
|
|||||||
if (name.isEmpty()) return SECTION_KEY_DEFAULT
|
if (name.isEmpty()) return SECTION_KEY_DEFAULT
|
||||||
val section = name.first().uppercaseChar()
|
val section = name.first().uppercaseChar()
|
||||||
if (!section.isLetter()) return SECTION_KEY_DEFAULT
|
if (!section.isLetter()) return SECTION_KEY_DEFAULT
|
||||||
return Util.stripAccents(section.toString())!!
|
return Util.stripAccents(section.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showArtistPicture(): Boolean {
|
private fun showArtistPicture(): Boolean {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
package org.moire.ultrasonic.app
|
package org.moire.ultrasonic.app
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
|
import android.os.StrictMode.ThreadPolicy
|
||||||
|
import android.os.StrictMode.VmPolicy
|
||||||
import androidx.multidex.MultiDexApplication
|
import androidx.multidex.MultiDexApplication
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -18,6 +21,7 @@ import org.moire.ultrasonic.di.musicServiceModule
|
|||||||
import org.moire.ultrasonic.log.FileLoggerTree
|
import org.moire.ultrasonic.log.FileLoggerTree
|
||||||
import org.moire.ultrasonic.log.TimberKoinLogger
|
import org.moire.ultrasonic.log.TimberKoinLogger
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import timber.log.Timber.DebugTree
|
import timber.log.Timber.DebugTree
|
||||||
|
|
||||||
@ -31,11 +35,15 @@ class UApp : MultiDexApplication() {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
instance = this
|
instance = this
|
||||||
if (BuildConfig.DEBUG)
|
if (BuildConfig.DEBUG) {
|
||||||
StrictMode.enableDefaults()
|
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())
|
||||||
|
StrictMode.setVmPolicy(VmPolicy.Builder().detectAllExceptSocket().penaltyLog().build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var initiated = false
|
var initiated = false
|
||||||
|
var isFirstRun = false
|
||||||
|
var setupDialogDisplayed = false
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
initiated = true
|
initiated = true
|
||||||
@ -52,6 +60,7 @@ class UApp : MultiDexApplication() {
|
|||||||
if (Settings.debugLogToFile) {
|
if (Settings.debugLogToFile) {
|
||||||
FileLoggerTree.plantToTimberForest()
|
FileLoggerTree.plantToTimberForest()
|
||||||
}
|
}
|
||||||
|
isFirstRun = Util.isFirstRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
startKoin()
|
startKoin()
|
||||||
@ -92,3 +101,23 @@ class UApp : MultiDexApplication() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder {
|
||||||
|
detectLeakedSqlLiteObjects()
|
||||||
|
detectActivityLeaks()
|
||||||
|
detectLeakedClosableObjects()
|
||||||
|
detectLeakedRegistrationObjects()
|
||||||
|
detectFileUriExposure()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
detectContentUriWithoutPermission()
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
detectCredentialProtectedWhileLocked()
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
detectUnsafeIntentLaunch()
|
||||||
|
detectIncorrectContextUse()
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
@ -11,6 +11,9 @@ import androidx.lifecycle.LiveData
|
|||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.util.FileUtil.deserialize
|
import org.moire.ultrasonic.util.FileUtil.deserialize
|
||||||
import org.moire.ultrasonic.util.FileUtil.serialize
|
import org.moire.ultrasonic.util.FileUtil.serialize
|
||||||
@ -21,7 +24,7 @@ import timber.log.Timber
|
|||||||
*
|
*
|
||||||
* TODO: Maybe store the settings in the DB?
|
* TODO: Maybe store the settings in the DB?
|
||||||
*/
|
*/
|
||||||
class EqualizerController {
|
class EqualizerController : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
var equalizer: Equalizer? = null
|
var equalizer: Equalizer? = null
|
||||||
@ -29,22 +32,30 @@ class EqualizerController {
|
|||||||
|
|
||||||
fun saveSettings() {
|
fun saveSettings() {
|
||||||
if (equalizer == null) return
|
if (equalizer == null) return
|
||||||
try {
|
launch {
|
||||||
serialize(UApp.applicationContext(), EqualizerSettings(equalizer!!), "equalizer.dat")
|
try {
|
||||||
} catch (all: Throwable) {
|
serialize(
|
||||||
Timber.w(all, "Failed to save equalizer settings.")
|
UApp.applicationContext(),
|
||||||
|
EqualizerSettings(equalizer!!),
|
||||||
|
"equalizer.dat"
|
||||||
|
)
|
||||||
|
} catch (all: Throwable) {
|
||||||
|
Timber.w(all, "Failed to save equalizer settings.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadSettings() {
|
fun loadSettings() {
|
||||||
if (equalizer == null) return
|
if (equalizer == null) return
|
||||||
try {
|
launch {
|
||||||
val settings = deserialize<EqualizerSettings>(
|
try {
|
||||||
UApp.applicationContext(), "equalizer.dat"
|
val settings = deserialize<EqualizerSettings>(
|
||||||
)
|
UApp.applicationContext(), "equalizer.dat"
|
||||||
settings?.apply(equalizer!!)
|
)
|
||||||
} catch (all: Throwable) {
|
settings?.apply(equalizer!!)
|
||||||
Timber.w(all, "Failed to load equalizer settings.")
|
} catch (all: Throwable) {
|
||||||
|
Timber.w(all, "Failed to load equalizer settings.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient.Companion.OFFLINE_DB_URL
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.di.DB_FILENAME
|
import org.moire.ultrasonic.di.DB_FILENAME
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
||||||
@ -202,7 +203,7 @@ class ActiveServerProvider(
|
|||||||
id = OFFLINE_DB_ID,
|
id = OFFLINE_DB_ID,
|
||||||
index = OFFLINE_DB_INDEX,
|
index = OFFLINE_DB_INDEX,
|
||||||
name = UApp.applicationContext().getString(R.string.main_offline),
|
name = UApp.applicationContext().getString(R.string.main_offline),
|
||||||
url = "http://localhost",
|
url = OFFLINE_DB_URL,
|
||||||
userName = "",
|
userName = "",
|
||||||
password = "",
|
password = "",
|
||||||
jukeboxByDefault = false,
|
jukeboxByDefault = false,
|
||||||
|
@ -61,6 +61,12 @@ interface AlbumDao : GenericDao<Album> {
|
|||||||
@Query("DELETE FROM albums WHERE artistId LIKE :id")
|
@Query("DELETE FROM albums WHERE artistId LIKE :id")
|
||||||
fun clearByArtist(id: String)
|
fun clearByArtist(id: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear albums by artist
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM albums WHERE id LIKE :id")
|
||||||
|
fun delete(id: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Make generic
|
* TODO: Make generic
|
||||||
* Upserts (insert or update) an object to the database
|
* Upserts (insert or update) an object to the database
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
@ -44,4 +45,19 @@ interface ArtistDao {
|
|||||||
*/
|
*/
|
||||||
@Query("SELECT * FROM artists WHERE id LIKE :id")
|
@Query("SELECT * FROM artists WHERE id LIKE :id")
|
||||||
fun get(id: String): Artist?
|
fun get(id: String): Artist?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an object from the database
|
||||||
|
*
|
||||||
|
* @param obj the object to be deleted
|
||||||
|
*/
|
||||||
|
@Delete
|
||||||
|
@JvmSuppressWildcards
|
||||||
|
fun delete(obj: Artist)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete artist by id
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM artists WHERE id LIKE :id")
|
||||||
|
fun delete(id: String)
|
||||||
}
|
}
|
||||||
|
@ -413,6 +413,7 @@ class SettingsFragment :
|
|||||||
// Clear download queue.
|
// Clear download queue.
|
||||||
mediaPlayerController.clear()
|
mediaPlayerController.clear()
|
||||||
Storage.reset()
|
Storage.reset()
|
||||||
|
Storage.ensureRootIsAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setDebugLogToFile(writeLog: Boolean) {
|
private fun setDebugLogToFile(writeLog: Boolean) {
|
||||||
|
@ -85,7 +85,6 @@ class BitmapUtils {
|
|||||||
} catch (expected: Exception) {
|
} catch (expected: Exception) {
|
||||||
Timber.e(expected, "Exception in BitmapFactory.decodeFile()")
|
Timber.e(expected, "Exception in BitmapFactory.decodeFile()")
|
||||||
}
|
}
|
||||||
Timber.i("getBitmapFromDisk %s", size.toString())
|
|
||||||
return bitmap1
|
return bitmap1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import com.squareup.picasso.RequestHandler
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
|
||||||
import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE
|
import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE
|
||||||
import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL
|
import org.moire.ultrasonic.util.FileUtil.SUFFIX_SMALL
|
||||||
|
|
||||||
@ -28,16 +27,24 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
|
|||||||
val size = request.uri.getQueryParameter(SIZE)?.toLong()
|
val size = request.uri.getQueryParameter(SIZE)?.toLong()
|
||||||
|
|
||||||
// Check if we have a hit in the disk cache
|
// Check if we have a hit in the disk cache
|
||||||
// Note: Currently we are only caching full size images on disk
|
// First check for a large and fallback to the small size.
|
||||||
// So we modify the key to query for the full size image,
|
|
||||||
// because scaling down a larger size image on the device is quicker than
|
// because scaling down a larger size image on the device is quicker than
|
||||||
// requesting the down-sized image from the network.
|
// requesting the down-sized image from the network.
|
||||||
val key = request.stableKey!!.replace(SUFFIX_SMALL, SUFFIX_LARGE)
|
val key = request.stableKey!!
|
||||||
val cache = BitmapUtils.getAlbumArtBitmapFromDisk(key, size?.toInt())
|
val largeKey = key.replace(SUFFIX_SMALL, SUFFIX_LARGE)
|
||||||
|
var cache = BitmapUtils.getAlbumArtBitmapFromDisk(largeKey, size?.toInt())
|
||||||
|
if (cache == null && key != largeKey) {
|
||||||
|
cache = BitmapUtils.getAlbumArtBitmapFromDisk(key, size?.toInt())
|
||||||
|
}
|
||||||
if (cache != null) {
|
if (cache != null) {
|
||||||
return Result(cache, DISK)
|
return Result(cache, DISK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel early if we are offline
|
||||||
|
if (client.isOffline) {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
// Try to fetch the image from the API
|
// Try to fetch the image from the API
|
||||||
// Inverted call order, because Mockito has problems with chained calls.
|
// Inverted call order, because Mockito has problems with chained calls.
|
||||||
val response = client.toStreamResponse(client.api.getCoverArt(id, size).execute())
|
val response = client.toStreamResponse(client.api.getCoverArt(id, size).execute())
|
||||||
|
@ -20,6 +20,7 @@ import org.moire.ultrasonic.R
|
|||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
||||||
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
||||||
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
@ -176,6 +177,13 @@ class ImageLoader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cacheArtistPicture(artist: Artist) {
|
||||||
|
if (artist.coverArt == null) return
|
||||||
|
val key = FileUtil.getArtistArtKey(artist.name, false)
|
||||||
|
val file = FileUtil.getAlbumArtFile(key)
|
||||||
|
cacheCoverArt(artist.coverArt!!, file)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a cover art file of a Track and cache it on disk
|
* Download a cover art file of a Track and cache it on disk
|
||||||
*/
|
*/
|
||||||
|
@ -24,6 +24,9 @@ import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|||||||
import androidx.media3.session.MediaLibraryService
|
import androidx.media3.session.MediaLibraryService
|
||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity
|
import org.moire.ultrasonic.activity.NavigationActivity
|
||||||
@ -44,7 +47,10 @@ import org.moire.ultrasonic.util.toTrack
|
|||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
class PlaybackService : MediaLibraryService(), KoinComponent {
|
class PlaybackService :
|
||||||
|
MediaLibraryService(),
|
||||||
|
KoinComponent,
|
||||||
|
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
private lateinit var player: ExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||||
private var equalizer: EqualizerController? = null
|
private var equalizer: EqualizerController? = null
|
||||||
@ -192,7 +198,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||||||
player.currentMediaItemIndex,
|
player.currentMediaItemIndex,
|
||||||
Settings.preloadCount
|
Settings.preloadCount
|
||||||
).map { it.toTrack() }
|
).map { it.toTrack() }
|
||||||
DownloadService.download(nextSongs, save = false, isHighPriority = true)
|
|
||||||
|
launch {
|
||||||
|
DownloadService.download(nextSongs, save = false, isHighPriority = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPendingIntentForContent(): PendingIntent {
|
private fun getPendingIntentForContent(): PendingIntent {
|
||||||
|
@ -30,6 +30,7 @@ import org.moire.ultrasonic.R
|
|||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.DownloadState.Companion.isFinalState
|
import org.moire.ultrasonic.service.DownloadState.Companion.isFinalState
|
||||||
|
import org.moire.ultrasonic.util.CacheCleaner
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
|
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
|
||||||
import org.moire.ultrasonic.util.FileUtil.getPartialFile
|
import org.moire.ultrasonic.util.FileUtil.getPartialFile
|
||||||
@ -365,6 +366,7 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
Storage.delete(track.getCompleteFile())
|
Storage.delete(track.getCompleteFile())
|
||||||
Storage.delete(track.getPinnedFile())
|
Storage.delete(track.getPinnedFile())
|
||||||
postState(track, DownloadState.IDLE)
|
postState(track, DownloadState.IDLE)
|
||||||
|
CacheCleaner().cleanDatabaseSelective(track)
|
||||||
Util.scanMedia(track.getPinnedFile())
|
Util.scanMedia(track.getPinnedFile())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +188,8 @@ class DownloadTask(
|
|||||||
val albumId: String? = if (albumId.isNullOrEmpty()) null else albumId
|
val albumId: String? = if (albumId.isNullOrEmpty()) null else albumId
|
||||||
|
|
||||||
var album: Album? = null
|
var album: Album? = null
|
||||||
|
var directArtist: Artist? = null
|
||||||
|
var compilationArtist: Artist? = null
|
||||||
|
|
||||||
// Sometime in compilation albums, the individual tracks won't have an Artist id
|
// Sometime in compilation albums, the individual tracks won't have an Artist id
|
||||||
// In this case, try to get the ArtistId of the album...
|
// In this case, try to get the ArtistId of the album...
|
||||||
@ -198,7 +200,7 @@ class DownloadTask(
|
|||||||
|
|
||||||
// Cache the artist
|
// Cache the artist
|
||||||
if (artistId != null)
|
if (artistId != null)
|
||||||
cacheArtist(onlineDB, offlineDB, artistId)
|
directArtist = cacheArtist(onlineDB, offlineDB, artistId)
|
||||||
|
|
||||||
// Now cache the album
|
// Now cache the album
|
||||||
if (albumId != null) {
|
if (albumId != null) {
|
||||||
@ -216,7 +218,7 @@ class DownloadTask(
|
|||||||
|
|
||||||
// If the album is a Compilation, also cache the Album artist
|
// If the album is a Compilation, also cache the Album artist
|
||||||
if (album.artistId != null && album.artistId != artistId)
|
if (album.artistId != null && album.artistId != artistId)
|
||||||
cacheArtist(onlineDB, offlineDB, album.artistId!!)
|
compilationArtist = cacheArtist(onlineDB, offlineDB, album.artistId!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,10 +226,20 @@ class DownloadTask(
|
|||||||
offlineDB.trackDao().insert(this)
|
offlineDB.trackDao().insert(this)
|
||||||
|
|
||||||
// Download the largest size that we can display in the UI
|
// Download the largest size that we can display in the UI
|
||||||
imageLoaderProvider.getImageLoader().cacheCoverArt(this)
|
val imageLoader = imageLoaderProvider.getImageLoader()
|
||||||
|
imageLoader.cacheCoverArt(this)
|
||||||
|
|
||||||
|
// Cache small copies of the Artist picture
|
||||||
|
|
||||||
|
directArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||||
|
compilationArtist?.let { imageLoader.cacheArtistPicture(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cacheArtist(onlineDB: MetaDatabase, offlineDB: MetaDatabase, artistId: String) {
|
private fun cacheArtist(
|
||||||
|
onlineDB: MetaDatabase,
|
||||||
|
offlineDB: MetaDatabase,
|
||||||
|
artistId: String
|
||||||
|
): Artist? {
|
||||||
var artist: Artist? = onlineDB.artistDao().get(artistId)
|
var artist: Artist? = onlineDB.artistDao().get(artistId)
|
||||||
|
|
||||||
// If we are downloading a new album, and the user has not visited the Artists list
|
// If we are downloading a new album, and the user has not visited the Artists list
|
||||||
@ -243,6 +255,8 @@ class DownloadTask(
|
|||||||
if (artist != null) {
|
if (artist != null) {
|
||||||
offlineDB.artistDao().insert(artist)
|
offlineDB.artistDao().insert(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return artist
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
@ -2,6 +2,9 @@ package org.moire.ultrasonic.subsonic
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
@ -15,7 +18,9 @@ import org.moire.ultrasonic.util.Util
|
|||||||
/**
|
/**
|
||||||
* Handles the lifetime of the Image Loader
|
* Handles the lifetime of the Image Loader
|
||||||
*/
|
*/
|
||||||
class ImageLoaderProvider(val context: Context) : KoinComponent {
|
class ImageLoaderProvider(val context: Context) :
|
||||||
|
KoinComponent,
|
||||||
|
CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||||
private var imageLoader: ImageLoader? = null
|
private var imageLoader: ImageLoader? = null
|
||||||
private var serverID: String = get(named("ServerID"))
|
private var serverID: String = get(named("ServerID"))
|
||||||
|
|
||||||
@ -32,6 +37,11 @@ class ImageLoaderProvider(val context: Context) : KoinComponent {
|
|||||||
imageLoader = get()
|
imageLoader = get()
|
||||||
serverID = currentID
|
serverID = currentID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
FileUtil.ensureAlbumArtDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
return imageLoader!!
|
return imageLoader!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,16 +8,17 @@
|
|||||||
package org.moire.ultrasonic.util
|
package org.moire.ultrasonic.util
|
||||||
|
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.HashSet
|
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.guava.future
|
import kotlinx.coroutines.guava.future
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import org.koin.java.KoinJavaComponent.inject
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.domain.Playlist
|
import org.moire.ultrasonic.domain.Playlist
|
||||||
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.MediaPlayerController
|
import org.moire.ultrasonic.service.MediaPlayerController
|
||||||
import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile
|
import org.moire.ultrasonic.util.FileUtil.getAlbumArtFile
|
||||||
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
|
import org.moire.ultrasonic.util.FileUtil.getCompleteFile
|
||||||
@ -35,9 +36,10 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* Responsible for cleaning up files from the offline download cache on the filesystem.
|
* Responsible for cleaning up files from the offline download cache on the filesystem.
|
||||||
*/
|
*/
|
||||||
class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinComponent {
|
||||||
|
|
||||||
private var mainScope = CoroutineScope(Dispatchers.Main)
|
private var mainScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
private val activeServerProvider by inject<ActiveServerProvider>()
|
||||||
|
|
||||||
private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
|
private fun exceptionHandler(tag: String): CoroutineExceptionHandler {
|
||||||
return CoroutineExceptionHandler { _, exception ->
|
return CoroutineExceptionHandler { _, exception ->
|
||||||
@ -54,6 +56,7 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
cleaning = true
|
cleaning = true
|
||||||
launch(exceptionHandler("clean")) {
|
launch(exceptionHandler("clean")) {
|
||||||
backgroundCleanup()
|
backgroundCleanup()
|
||||||
|
backgroundCleanWholeDatabase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,6 +83,94 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun backgroundCleanWholeDatabase() {
|
||||||
|
val offlineDB = activeServerProvider.offlineMetaDatabase
|
||||||
|
|
||||||
|
Timber.i("Starting Database cleanup")
|
||||||
|
|
||||||
|
// Get all tracks in the db
|
||||||
|
val tracks = offlineDB.trackDao().get().toMutableSet()
|
||||||
|
|
||||||
|
// Check all tracks if they have files
|
||||||
|
val orphanedTracks = tracks.filter {
|
||||||
|
!Storage.isPathExists(it.getPinnedFile()) && !Storage.isPathExists(it.getCompleteFile())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete orphaned tracks
|
||||||
|
orphanedTracks.forEach {
|
||||||
|
offlineDB.trackDao().delete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deleted tracks from our list
|
||||||
|
tracks -= orphanedTracks.toSet()
|
||||||
|
|
||||||
|
// Check for orphaned Albums
|
||||||
|
val usedAlbumIds = tracks.mapNotNull { it.albumId }.toSet()
|
||||||
|
val albums = offlineDB.albumDao().get().toMutableSet()
|
||||||
|
val orphanedAlbums = albums.filterNot {
|
||||||
|
usedAlbumIds.contains(it.id)
|
||||||
|
}.toSet()
|
||||||
|
|
||||||
|
// Delete orphaned Albums
|
||||||
|
orphanedAlbums.forEach {
|
||||||
|
offlineDB.albumDao().delete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
albums -= orphanedAlbums
|
||||||
|
|
||||||
|
// Check for orphaned Artists
|
||||||
|
val usedArtistsIds = tracks.mapNotNull { it.artistId } + albums.mapNotNull { it.artistId }
|
||||||
|
val artists = offlineDB.artistDao().get().toSet()
|
||||||
|
val orphanedArtists = artists.filterNot {
|
||||||
|
usedArtistsIds.contains(it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete orphaned Artists
|
||||||
|
orphanedArtists.forEach {
|
||||||
|
offlineDB.artistDao().delete(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.e("Database cleanup done")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cleanDatabaseSelective(trackToRemove: Track) {
|
||||||
|
launch(exceptionHandler("cleanDatabase")) {
|
||||||
|
backgroundDatabaseSelective(trackToRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backgroundDatabaseSelective(track: Track) {
|
||||||
|
val offlineDB = activeServerProvider.offlineMetaDatabase
|
||||||
|
|
||||||
|
// Delete track
|
||||||
|
offlineDB.trackDao().delete(track)
|
||||||
|
|
||||||
|
// Setup up artistList
|
||||||
|
val artistsToCheck = mutableListOf(track.artistId)
|
||||||
|
|
||||||
|
// Check if we have other tracks for the album
|
||||||
|
var albumToDelete: String? = null
|
||||||
|
if (track.albumId != null) {
|
||||||
|
val tracks = offlineDB.trackDao().byAlbum(track.albumId!!)
|
||||||
|
if (tracks.isEmpty()) albumToDelete = track.albumId!!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete empty album
|
||||||
|
if (albumToDelete != null) {
|
||||||
|
val album = offlineDB.albumDao().get(albumToDelete)
|
||||||
|
if (album != null) {
|
||||||
|
artistsToCheck.add(album.artistId)
|
||||||
|
offlineDB.albumDao().delete(album)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have an empty artist now..
|
||||||
|
artistsToCheck.filterNotNull().forEach {
|
||||||
|
val tracks = offlineDB.trackDao().byArtist(it)
|
||||||
|
if (tracks.isEmpty()) offlineDB.artistDao().delete(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun backgroundCleanup() {
|
private fun backgroundCleanup() {
|
||||||
try {
|
try {
|
||||||
val files: MutableList<AbstractFile> = ArrayList()
|
val files: MutableList<AbstractFile> = ArrayList()
|
||||||
|
@ -199,13 +199,17 @@ object FileUtil {
|
|||||||
return "$albumArtDir/$cacheKey"
|
return "$albumArtDir/$cacheKey"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the album art directory quickly, without checking that it exists.
|
||||||
|
*/
|
||||||
val albumArtDirectory: File
|
val albumArtDirectory: File
|
||||||
get() {
|
get() = File(ultrasonicDirectory, "artwork")
|
||||||
val albumArtDir = File(ultrasonicDirectory, "artwork")
|
|
||||||
ensureDirectoryExistsAndIsReadWritable(albumArtDir)
|
fun ensureAlbumArtDirectory() {
|
||||||
ensureDirectoryExistsAndIsReadWritable(File(albumArtDir, ".nomedia"))
|
val albumArtDir = albumArtDirectory
|
||||||
return albumArtDir
|
ensureDirectoryExistsAndIsReadWritable(albumArtDir)
|
||||||
}
|
ensureDirectoryExistsAndIsReadWritable(File(albumArtDir, ".nomedia"))
|
||||||
|
}
|
||||||
|
|
||||||
private fun getAlbumDirectory(entry: MusicDirectory.Child): String {
|
private fun getAlbumDirectory(entry: MusicDirectory.Child): String {
|
||||||
val dir: String
|
val dir: String
|
||||||
|
@ -29,6 +29,9 @@ object Storage {
|
|||||||
StorageFile.notExistingPathDictionary.clear()
|
StorageFile.notExistingPathDictionary.clear()
|
||||||
mediaRoot.reset()
|
mediaRoot.reset()
|
||||||
Timber.i("StorageFile caches were reset")
|
Timber.i("StorageFile caches were reset")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureRootIsAvailable() {
|
||||||
val root = getRoot()
|
val root = getRoot()
|
||||||
if (root == null) {
|
if (root == null) {
|
||||||
Settings.customCacheLocation = false
|
Settings.customCacheLocation = false
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
a:title="@string/settings.use_folder_for_album_artist"
|
a:title="@string/settings.use_folder_for_album_artist"
|
||||||
app:iconSpaceReserved="false"/>
|
app:iconSpaceReserved="false"/>
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
a:defaultValue="true"
|
a:defaultValue="false"
|
||||||
a:key="@string/setting_key.show_track_number"
|
a:key="@string/setting_key.show_track_number"
|
||||||
a:summary="@string/settings.show_track_number_summary"
|
a:summary="@string/settings.show_track_number_summary"
|
||||||
a:title="@string/settings.show_track_number"
|
a:title="@string/settings.show_track_number"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user