Merge branch 'feature/cache-fix' into 'develop'

Fixed cache space cleanup

Closes #1284

See merge request ultrasonic/ultrasonic!1206
This commit is contained in:
Nite 2025-04-14 16:47:12 +00:00
commit 1f9de0be7e
12 changed files with 71 additions and 56 deletions

View File

@ -10,7 +10,7 @@ ktlint = "1.0.1"
ktlintGradle = "12.2.0"
detekt = "1.23.8"
preferences = "1.2.1"
media3 = "1.6.0"
media3 = "1.6.1"
androidSupport = "1.9.1"
materialDesign = "1.12.0"
@ -31,7 +31,7 @@ koin = "4.0.4"
picasso = "2.8"
junit4 = "4.13.2"
junit5 = "5.12.1"
junit5 = "5.12.2"
mockito = "5.17.0"
mockitoKotlin = "5.4.0"
kluent = "1.73"

View File

@ -435,12 +435,15 @@ class NavigationActivity : ScopeActivity() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Skip android.R.id.home so the drawer button doesn't get wrongly routed
if (item.itemId == android.R.id.home) {
return super.onOptionsItemSelected(item)
}
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) ||
val navController = findNavController(R.id.nav_host_fragment)
// Check if this item ID exists in the nav graph
val destinationExists = navController.graph.findNode(item.itemId) != null
return if (destinationExists) {
item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
} else {
// Let the fragments handle their own menu items
super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {

View File

@ -83,6 +83,31 @@ class TrackViewHolder(val view: View) :
fun setSong(song: Track, checkable: Boolean, draggable: Boolean, isSelected: Boolean = false) {
entry = song
// Create new Disposable for the new Subscriptions
rxBusSubscription = CompositeDisposable()
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
}
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
if (it.id != song.id) return@subscribe
updateStatus(it.state, it.progress)
}
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
launch(Dispatchers.Main) {
// Ignore updates which are not for the current song
if (it.id != song.id) return@launch
if (it.rating is HeartRating) {
updateRatingDisplay(song.userRating, it.rating.isHeart)
} else if (it.rating is StarRating) {
updateRatingDisplay(it.rating.starRating.toInt(), song.starred)
}
}
}
val entryDescription = Util.readableEntryDescription(song)
artist.text = entryDescription.artist
@ -121,31 +146,6 @@ class TrackViewHolder(val view: View) :
artist.isGone = true
progressIndicator.isGone = true
}
// Create new Disposable for the new Subscriptions
rxBusSubscription = CompositeDisposable()
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
}
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
if (it.id != song.id) return@subscribe
updateStatus(it.state, it.progress)
}
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
launch(Dispatchers.Main) {
// Ignore updates which are not for the current song
if (it.id != song.id) return@launch
if (it.rating is HeartRating) {
updateRatingDisplay(song.userRating, it.rating.isHeart)
} else if (it.rating is StarRating) {
updateRatingDisplay(it.rating.starRating.toInt(), song.starred)
}
}
}
}
// This is called when the Holder is recycled and receives a new Song

View File

@ -159,7 +159,7 @@ class ActiveServerProvider(
METADATA_DB + serverId
)
.addMigrations(META_MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
.fallbackToDestructiveMigrationOnDowngrade(true)
.build()
}

View File

@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
import androidx.room.Room
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.module.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.data.AppDatabase

View File

@ -17,7 +17,6 @@ import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.model.ArtistListModel
@ -70,7 +69,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
id = item.id,
name = item.name,
parentId = item.id,
isArtist = (item is Artist)
isArtist = false
)
} else {
NavigationGraphDirections.toAlbumList(

View File

@ -312,11 +312,11 @@ class DownloadService : Service(), KoinComponent {
completeFile?.let {
postState(track, DownloadState.DONE)
false
return@filter false
}
pinnedFile?.let {
postState(track, DownloadState.PINNED)
false
return@filter false
}
true
}

View File

@ -23,6 +23,7 @@ import org.moire.ultrasonic.data.MetaDatabase
import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.DownloadState.Companion.isFinalState
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.FileUtil.copyWithProgress
@ -48,11 +49,17 @@ class DownloadTask(
private var inputStream: InputStream? = null
private var outputStream: OutputStream? = null
private var lastPostTime: Long = 0
private var state: DownloadState = DownloadState.UNKNOWN
private fun setState(state: DownloadState, progress: Int?) {
this.state = state
stateChangedCallback(downloadTrack, state, progress)
}
private fun checkIfExists(): Boolean {
if (Storage.isPathExists(downloadTrack.pinnedFile)) {
Timber.i("%s already exists. Skipping.", downloadTrack.pinnedFile)
stateChangedCallback(downloadTrack, DownloadState.PINNED, null)
setState(DownloadState.PINNED, null)
return true
}
@ -77,7 +84,7 @@ class DownloadTask(
} catch (ignore: Exception) {
Timber.w(ignore)
}
stateChangedCallback(downloadTrack, newStatus, null)
setState(newStatus, null)
return true
}
@ -85,7 +92,7 @@ class DownloadTask(
}
fun download() {
stateChangedCallback(downloadTrack, DownloadState.DOWNLOADING, null)
setState(DownloadState.DOWNLOADING, null)
val fileLength = Storage.getFromPath(downloadTrack.partialFile)?.length ?: 0
@ -136,11 +143,7 @@ class DownloadTask(
(totalBytesCopied * 100 / (size)).toInt()
}
stateChangedCallback(
downloadTrack,
DownloadState.DOWNLOADING,
progress
)
setState(DownloadState.DOWNLOADING, progress)
}
}
@ -157,7 +160,7 @@ class DownloadTask(
downloadTrack.pinnedFile
)
Timber.i("Renamed file to ${downloadTrack.pinnedFile}")
stateChangedCallback(downloadTrack, DownloadState.PINNED, null)
setState(DownloadState.PINNED, null)
Util.scanMedia(downloadTrack.pinnedFile)
} else {
Storage.rename(
@ -165,20 +168,22 @@ class DownloadTask(
downloadTrack.completeFile
)
Timber.i("Renamed file to ${downloadTrack.completeFile}")
stateChangedCallback(downloadTrack, DownloadState.DONE, null)
setState(DownloadState.DONE, null)
}
}
private fun onCompletion(e: Throwable?) {
if (e is CancellationException) {
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
stateChangedCallback(downloadTrack, DownloadState.CANCELLED, null)
if (!state.isFinalState()) {
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
setState(DownloadState.CANCELLED, null)
}
} else if (e != null) {
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
if (downloadTrack.tryCount < MAX_RETRIES) {
stateChangedCallback(downloadTrack, DownloadState.RETRYING, null)
setState(DownloadState.RETRYING, null)
} else {
stateChangedCallback(downloadTrack, DownloadState.FAILED, null)
setState(DownloadState.FAILED, null)
}
}
inputStream.safeClose()

View File

@ -725,7 +725,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
}
override fun getTrackSelectionParameters(): TrackSelectionParameters {
return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
return TrackSelectionParameters.DEFAULT
}
override fun getMaxSeekToPreviousPosition(): Long {

View File

@ -19,6 +19,7 @@ import androidx.media3.common.C
import androidx.media3.common.C.USAGE_MEDIA
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.ResolvingDataSource
@ -292,6 +293,11 @@ class PlaybackService :
cacheNextSongs()
}
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
// Handles playlist changes, e.g. when tracks are reordered or deleted
cacheNextSongs()
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
// Since we cannot update the metadata of the media item after creation,
// we cannot set change the rating on it

View File

@ -192,6 +192,7 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
val files: MutableList<AbstractFile> = ArrayList()
val dirs: MutableList<AbstractFile> = ArrayList()
Timber.i("CacheCleaner backgroundSpaceCleanup running...")
findCandidatesForDeletion(musicDirectory, files, dirs)
val bytesToDelete = getMinimumDelete(files)
@ -235,7 +236,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
val filesToNotDelete: MutableSet<String> = HashSet(5)
// We just take the last published playlist from RX
val playlist = RxBus.playlistObservable.blockingLast()
val playlist = RxBus.playlistObservable.firstElement().blockingGet()
?: return filesToNotDelete
for (track in playlist) {
filesToNotDelete.add(track.getPartialFile())
filesToNotDelete.add(track.getCompleteFile())

View File

@ -740,7 +740,7 @@ object Util {
timeline: Timeline?,
isShuffled: Boolean,
firstIndex: Int? = null,
count: Int? = null
count: Int = Int.MAX_VALUE
): List<MediaItem> {
if (timeline == null) return emptyList()
if (timeline.windowCount < 1) return emptyList()
@ -749,7 +749,7 @@ object Util {
var i = firstIndex ?: timeline.getFirstWindowIndex(isShuffled)
if (i == C.INDEX_UNSET) return emptyList()
while (i != C.INDEX_UNSET && (count != playlist.count())) {
while (i != C.INDEX_UNSET && (count >= playlist.count())) {
val window = timeline.getWindow(i, Timeline.Window())
playlist.add(window.mediaItem)
i = timeline.getNextWindowIndex(i, Player.REPEAT_MODE_OFF, isShuffled)