mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-05-18 16:26:36 +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"
|
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"
|
||||||
|
@ -435,13 +435,16 @@ 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 {
|
||||||
// This override is required by design when using setupActionBarWithNavController()
|
// This override is required by design when using setupActionBarWithNavController()
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
if (!state.isFinalState()) {
|
||||||
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
|
Timber.w(e, "CompletionHandler ${downloadTrack.pinnedFile}")
|
||||||
stateChangedCallback(downloadTrack, DownloadState.CANCELLED, null)
|
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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user