Fixed cache space cleanup

Fixed track status icons
Fixed checking if tracks should be downloaded
Fixed downloading tracks when the playlist changes
Fixed minor warnings and deprecations
This commit is contained in:
Nite 2025-04-14 16:20:43 +02:00
parent 006c554456
commit 9a31a00148
No known key found for this signature in database
GPG Key ID: 1D1AD59B1C6386C1
12 changed files with 70 additions and 54 deletions

View File

@ -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,13 +435,16 @@ 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 {
// This override is required by design when using setupActionBarWithNavController()

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

@ -70,7 +70,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) {
if (!state.isFinalState()) {
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
stateChangedCallback(downloadTrack, DownloadState.CANCELLED, null)
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)