mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-05-18 00:06:35 +03:00
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:
parent
006c554456
commit
9a31a00148
@ -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"
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -159,7 +159,7 @@ class ActiveServerProvider(
|
||||
METADATA_DB + serverId
|
||||
)
|
||||
.addMigrations(META_MIGRATION_2_3)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.fallbackToDestructiveMigrationOnDowngrade(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -725,7 +725,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
|
||||
override fun getTrackSelectionParameters(): TrackSelectionParameters {
|
||||
return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
|
||||
return TrackSelectionParameters.DEFAULT
|
||||
}
|
||||
|
||||
override fun getMaxSeekToPreviousPosition(): Long {
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user