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" picasso = "2.8"
junit4 = "4.13.2" junit4 = "4.13.2"
junit5 = "5.12.1" junit5 = "5.12.2"
mockito = "5.17.0" mockito = "5.17.0"
mockitoKotlin = "5.4.0" mockitoKotlin = "5.4.0"
kluent = "1.73" kluent = "1.73"

View File

@ -435,12 +435,15 @@ class NavigationActivity : ScopeActivity() {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Skip android.R.id.home so the drawer button doesn't get wrongly routed val navController = findNavController(R.id.nav_host_fragment)
if (item.itemId == android.R.id.home) { // Check if this item ID exists in the nav graph
return super.onOptionsItemSelected(item) val destinationExists = navController.graph.findNode(item.itemId) != null
} return if (destinationExists) {
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) || item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
} else {
// Let the fragments handle their own menu items
super.onOptionsItemSelected(item) super.onOptionsItemSelected(item)
}
} }
override fun onSupportNavigateUp(): Boolean { 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) { fun setSong(song: Track, checkable: Boolean, draggable: Boolean, isSelected: Boolean = false) {
entry = song 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) val entryDescription = Util.readableEntryDescription(song)
artist.text = entryDescription.artist artist.text = entryDescription.artist
@ -121,31 +146,6 @@ class TrackViewHolder(val view: View) :
artist.isGone = true artist.isGone = true
progressIndicator.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 // This is called when the Holder is recycled and receives a new Song

View File

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

View File

@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
import androidx.room.Room import androidx.room.Room
import org.koin.android.ext.koin.androidContext 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.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
import org.moire.ultrasonic.data.AppDatabase import org.moire.ultrasonic.data.AppDatabase

View File

@ -70,7 +70,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
id = item.id, id = item.id,
name = item.name, name = item.name,
parentId = item.id, parentId = item.id,
isArtist = (item is Artist) isArtist = false
) )
} else { } else {
NavigationGraphDirections.toAlbumList( NavigationGraphDirections.toAlbumList(

View File

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

View File

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

View File

@ -725,7 +725,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
} }
override fun getTrackSelectionParameters(): TrackSelectionParameters { override fun getTrackSelectionParameters(): TrackSelectionParameters {
return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT return TrackSelectionParameters.DEFAULT
} }
override fun getMaxSeekToPreviousPosition(): Long { 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.C.USAGE_MEDIA
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.TrackSelectionParameters
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.datasource.ResolvingDataSource import androidx.media3.datasource.ResolvingDataSource
@ -292,6 +293,11 @@ class PlaybackService :
cacheNextSongs() 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) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
// Since we cannot update the metadata of the media item after creation, // Since we cannot update the metadata of the media item after creation,
// we cannot set change the rating on it // 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 files: MutableList<AbstractFile> = ArrayList()
val dirs: MutableList<AbstractFile> = ArrayList() val dirs: MutableList<AbstractFile> = ArrayList()
Timber.i("CacheCleaner backgroundSpaceCleanup running...")
findCandidatesForDeletion(musicDirectory, files, dirs) findCandidatesForDeletion(musicDirectory, files, dirs)
val bytesToDelete = getMinimumDelete(files) val bytesToDelete = getMinimumDelete(files)
@ -235,7 +236,8 @@ class CacheCleaner : CoroutineScope by CoroutineScope(Dispatchers.IO), KoinCompo
val filesToNotDelete: MutableSet<String> = HashSet(5) val filesToNotDelete: MutableSet<String> = HashSet(5)
// We just take the last published playlist from RX // 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) { for (track in playlist) {
filesToNotDelete.add(track.getPartialFile()) filesToNotDelete.add(track.getPartialFile())
filesToNotDelete.add(track.getCompleteFile()) filesToNotDelete.add(track.getCompleteFile())

View File

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