ultrasonic-app-subsonic-and.../ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/Downloader.kt

450 lines
14 KiB
Kotlin
Raw Normal View History

package org.moire.ultrasonic.service
import android.net.wifi.WifiManager
import androidx.lifecycle.MutableLiveData
2021-08-28 00:02:50 +02:00
import java.util.ArrayList
import java.util.PriorityQueue
import java.util.concurrent.Executors
import java.util.concurrent.RejectedExecutionException
2021-08-28 00:02:50 +02:00
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.domain.MusicDirectory
2021-08-28 00:02:50 +02:00
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.util.LRUCache
import org.moire.ultrasonic.util.Settings
2021-08-28 00:02:50 +02:00
import org.moire.ultrasonic.util.ShufflePlayBuffer
import org.moire.ultrasonic.util.Util
import timber.log.Timber
2020-06-23 18:40:44 +02:00
/**
* This class is responsible for maintaining the playlist and downloading
* its items from the network to the filesystem.
2021-08-28 11:29:39 +02:00
*
* TODO: Move away from managing the queue with scheduled checks, instead use callbacks when
* Downloads are finished
*/
class Downloader(
private val shufflePlayBuffer: ShufflePlayBuffer,
private val externalStorageMonitor: ExternalStorageMonitor,
private val localMediaPlayer: LocalMediaPlayer
2021-08-28 00:02:50 +02:00
) : KoinComponent {
val playlist: MutableList<DownloadFile> = ArrayList()
var started: Boolean = false
private val downloadQueue: PriorityQueue<DownloadFile> = PriorityQueue<DownloadFile>()
private val activelyDownloading: MutableList<DownloadFile> = ArrayList()
val observableList: MutableLiveData<List<DownloadFile>> = MutableLiveData<List<DownloadFile>>()
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
2021-08-28 00:02:50 +02:00
private val downloadFileCache = LRUCache<MusicDirectory.Entry, DownloadFile>(100)
private var executorService: ScheduledExecutorService? = null
private var wifiLock: WifiManager.WifiLock? = null
2021-08-28 00:02:50 +02:00
var playlistUpdateRevision: Long = 0
private set
val downloadChecker = Runnable {
try {
Timber.w("Checking Downloads")
checkDownloadsInternal()
} catch (all: Exception) {
Timber.e(all, "checkDownloads() failed.")
}
2020-06-23 18:40:44 +02:00
}
fun onDestroy() {
stop()
clearPlaylist()
clearBackground()
observableList.value = listOf()
Timber.i("Downloader destroyed")
2020-06-23 18:40:44 +02:00
}
fun start() {
started = true
if (executorService == null) {
executorService = Executors.newSingleThreadScheduledExecutor()
executorService!!.scheduleWithFixedDelay(
downloadChecker, 0L, CHECK_INTERVAL, TimeUnit.SECONDS
)
Timber.i("Downloader started")
}
if (wifiLock == null) {
wifiLock = Util.createWifiLock(toString())
wifiLock?.acquire()
}
}
fun stop() {
started = false
executorService?.shutdown()
executorService = null
wifiLock?.release()
wifiLock = null
MediaPlayerService.runningInstance?.notifyDownloaderStopped()
Timber.i("Downloader stopped")
2020-06-23 18:40:44 +02:00
}
fun checkDownloads() {
if (
executorService == null ||
executorService!!.isTerminated ||
executorService!!.isShutdown
) {
start()
} else {
try {
executorService?.execute(downloadChecker)
} catch (exception: RejectedExecutionException) {
Timber.w(
exception,
"checkDownloads() can't run, maybe the Downloader is shutting down..."
)
}
}
}
@Synchronized
@Suppress("ComplexMethod")
fun checkDownloadsInternal() {
if (
!Util.isExternalStoragePresent() ||
!externalStorageMonitor.isExternalStorageAvailable
) {
return
2020-06-23 18:40:44 +02:00
}
if (shufflePlayBuffer.isEnabled) {
checkShufflePlay()
2020-06-23 18:40:44 +02:00
}
if (jukeboxMediaPlayer.isEnabled || !Util.isNetworkConnected()) {
return
2020-06-23 18:40:44 +02:00
}
2021-08-28 00:02:50 +02:00
// Check the active downloads for failures or completions and remove them
// Store the result in a flag to know if changes have occurred
var listChanged = cleanupActiveDownloads()
2020-06-23 18:40:44 +02:00
// Check if need to preload more from playlist
val preloadCount = Settings.preloadCount
2020-06-23 18:40:44 +02:00
// Start preloading at the current playing song
2021-08-28 00:02:50 +02:00
var start = currentPlayingIndex
if (start == -1) start = 0
2020-06-23 18:40:44 +02:00
2021-08-28 00:02:50 +02:00
val end = (start + preloadCount).coerceAtMost(playlist.size)
2020-06-23 18:40:44 +02:00
for (i in start until end) {
2021-08-28 00:02:50 +02:00
val download = playlist[i]
2020-06-23 18:40:44 +02:00
// Set correct priority (the lower the number, the higher the priority)
download.priority = i
2020-06-23 18:40:44 +02:00
// Add file to queue if not in one of the queues already.
2021-08-28 00:02:50 +02:00
if (!download.isWorkDone &&
!activelyDownloading.contains(download) &&
!downloadQueue.contains(download)
) {
listChanged = true
downloadQueue.add(download)
2020-06-23 18:40:44 +02:00
}
}
// Fill up active List with waiting tasks
2021-08-28 00:02:50 +02:00
while (activelyDownloading.size < PARALLEL_DOWNLOADS && downloadQueue.size > 0) {
val task = downloadQueue.remove()
activelyDownloading.add(task)
2021-08-30 10:08:27 +02:00
startDownloadOnService(task)
2020-06-23 18:40:44 +02:00
// The next file on the playlist is currently downloading
2021-08-28 00:02:50 +02:00
if (playlist.indexOf(task) == 1) {
localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING)
}
listChanged = true
}
// Stop Executor service when done downloading
if (activelyDownloading.size == 0) {
stop()
}
if (listChanged) {
updateLiveData()
}
}
2020-06-23 18:40:44 +02:00
private fun updateLiveData() {
observableList.postValue(downloads)
}
2021-08-30 10:08:27 +02:00
private fun startDownloadOnService(task: DownloadFile) {
MediaPlayerService.executeOnStartedMediaPlayerService {
task.download()
}
}
/**
* Return true if modifications were made
*/
private fun cleanupActiveDownloads(): Boolean {
val oldSize = activelyDownloading.size
2021-08-28 00:02:50 +02:00
activelyDownloading.retainAll {
when {
it.isDownloading -> true
it.isFailed && it.shouldRetry() -> {
// Add it back to queue
downloadQueue.add(it)
false
}
else -> {
it.cleanup()
false
}
}
}
return (oldSize != activelyDownloading.size)
2021-08-28 00:02:50 +02:00
}
@get:Synchronized
val currentPlayingIndex: Int
2021-08-28 00:02:50 +02:00
get() = playlist.indexOf(localMediaPlayer.currentPlaying)
@get:Synchronized
val downloadListDuration: Long
get() {
var totalDuration: Long = 0
2021-08-28 00:02:50 +02:00
for (downloadFile in playlist) {
val song = downloadFile.song
if (!song.isDirectory) {
if (song.artist != null) {
if (song.duration != null) {
totalDuration += song.duration!!.toLong()
}
2020-06-23 18:40:44 +02:00
}
}
}
return totalDuration
2020-06-23 18:40:44 +02:00
}
@get:Synchronized
val all: List<DownloadFile>
get() {
val temp: MutableList<DownloadFile> = ArrayList()
temp.addAll(activelyDownloading)
temp.addAll(downloadQueue)
temp.addAll(playlist)
return temp.distinct().sorted()
}
2020-06-23 18:40:44 +02:00
/*
* Returns a list of all DownloadFiles that are currently downloading or waiting for download,
* including undownloaded files from the playlist.
*/
@get:Synchronized
val downloads: List<DownloadFile>
get() {
val temp: MutableList<DownloadFile> = ArrayList()
temp.addAll(activelyDownloading)
temp.addAll(downloadQueue)
temp.addAll(
playlist.filter {
when (it.status.value) {
DownloadStatus.DOWNLOADING -> true
else -> false
}
}
)
return temp.distinct().sorted()
}
@Synchronized
fun clearPlaylist() {
2021-08-28 00:02:50 +02:00
playlist.clear()
2020-06-23 18:40:44 +02:00
// Cancel all active downloads with a high priority
for (download in activelyDownloading) {
if (download.priority < 100) {
download.cancelDownload()
activelyDownloading.remove(download)
}
}
2021-08-28 00:02:50 +02:00
playlistUpdateRevision++
updateLiveData()
2020-06-23 18:40:44 +02:00
}
@Synchronized
private fun clearBackground() {
// Clear the pending queue
downloadQueue.clear()
// Cancel all active downloads with a low priority
for (download in activelyDownloading) {
if (download.priority >= 100) {
download.cancelDownload()
activelyDownloading.remove(download)
}
2020-06-23 18:40:44 +02:00
}
}
@Synchronized
fun clearActiveDownloads() {
// Cancel all active downloads
for (download in activelyDownloading) {
download.cancelDownload()
2020-06-23 18:40:44 +02:00
}
activelyDownloading.clear()
updateLiveData()
2020-06-23 18:40:44 +02:00
}
@Synchronized
fun removeFromPlaylist(downloadFile: DownloadFile) {
if (activelyDownloading.contains(downloadFile)) {
downloadFile.cancelDownload()
2020-06-23 18:40:44 +02:00
}
2021-08-28 00:02:50 +02:00
playlist.remove(downloadFile)
playlistUpdateRevision++
checkDownloads()
2020-06-23 18:40:44 +02:00
}
@Synchronized
fun addToPlaylist(
songs: List<MusicDirectory.Entry?>,
save: Boolean,
autoPlay: Boolean,
playNext: Boolean,
newPlaylist: Boolean
) {
shufflePlayBuffer.isEnabled = false
var offset = 1
if (songs.isEmpty()) {
return
2020-06-23 18:40:44 +02:00
}
if (newPlaylist) {
2021-08-28 00:02:50 +02:00
playlist.clear()
2020-06-23 18:40:44 +02:00
}
if (playNext) {
if (autoPlay && currentPlayingIndex >= 0) {
offset = 0
2020-06-23 18:40:44 +02:00
}
for (song in songs) {
val downloadFile = DownloadFile(song!!, save)
2021-08-28 00:02:50 +02:00
playlist.add(currentPlayingIndex + offset, downloadFile)
offset++
2020-06-23 18:40:44 +02:00
}
} else {
for (song in songs) {
val downloadFile = DownloadFile(song!!, save)
2021-08-28 00:02:50 +02:00
playlist.add(downloadFile)
2020-06-23 18:40:44 +02:00
}
}
2021-08-28 00:02:50 +02:00
playlistUpdateRevision++
checkDownloads()
2020-06-23 18:40:44 +02:00
}
@Synchronized
fun downloadBackground(songs: List<MusicDirectory.Entry>, save: Boolean) {
2020-06-23 18:40:44 +02:00
// Because of the priority handling we add the songs in the reverse order they
// were requested, then it is correct in the end.
for (song in songs.asReversed()) {
downloadQueue.add(DownloadFile(song, save))
}
2020-06-23 18:40:44 +02:00
2021-08-28 00:02:50 +02:00
checkDownloads()
2020-06-23 18:40:44 +02:00
}
@Synchronized
fun shuffle() {
2021-08-28 00:02:50 +02:00
playlist.shuffle()
// Move the current song to the top..
if (localMediaPlayer.currentPlaying != null) {
2021-08-28 00:02:50 +02:00
playlist.remove(localMediaPlayer.currentPlaying)
playlist.add(0, localMediaPlayer.currentPlaying!!)
2020-06-23 18:40:44 +02:00
}
2021-08-28 00:02:50 +02:00
playlistUpdateRevision++
2020-06-23 18:40:44 +02:00
}
@Synchronized
2021-08-28 00:02:50 +02:00
@Suppress("ReturnCount")
fun getDownloadFileForSong(song: MusicDirectory.Entry): DownloadFile {
2021-08-28 00:02:50 +02:00
for (downloadFile in playlist) {
if (downloadFile.song == song) {
return downloadFile
2020-06-23 18:40:44 +02:00
}
}
for (downloadFile in activelyDownloading) {
if (downloadFile.song == song) {
return downloadFile
2020-06-23 18:40:44 +02:00
}
}
for (downloadFile in downloadQueue) {
if (downloadFile.song == song) {
return downloadFile
2020-06-23 18:40:44 +02:00
}
}
var downloadFile = downloadFileCache[song]
if (downloadFile == null) {
downloadFile = DownloadFile(song, false)
downloadFileCache.put(song, downloadFile)
}
return downloadFile
2020-06-23 18:40:44 +02:00
}
@Synchronized
private fun checkShufflePlay() {
// Get users desired random playlist size
val listSize = Settings.maxSongs
2021-08-28 00:02:50 +02:00
val wasEmpty = playlist.isEmpty()
val revisionBefore = playlistUpdateRevision
2020-06-23 18:40:44 +02:00
// First, ensure that list is at least 20 songs long.
2021-08-28 00:02:50 +02:00
val size = playlist.size
if (size < listSize) {
for (song in shufflePlayBuffer[listSize - size]) {
val downloadFile = DownloadFile(song, false)
2021-08-28 00:02:50 +02:00
playlist.add(downloadFile)
playlistUpdateRevision++
2020-06-23 18:40:44 +02:00
}
}
2021-08-28 00:02:50 +02:00
val currIndex = if (localMediaPlayer.currentPlaying == null) 0 else currentPlayingIndex
2020-06-23 18:40:44 +02:00
// Only shift playlist if playing song #5 or later.
2021-08-28 00:02:50 +02:00
if (currIndex > SHUFFLE_BUFFER_LIMIT) {
val songsToShift = currIndex - 2
for (song in shufflePlayBuffer[songsToShift]) {
2021-08-28 00:02:50 +02:00
playlist.add(DownloadFile(song, false))
playlist[0].cancelDownload()
playlist.removeAt(0)
playlistUpdateRevision++
2020-06-23 18:40:44 +02:00
}
}
2021-08-28 00:02:50 +02:00
if (revisionBefore != playlistUpdateRevision) {
jukeboxMediaPlayer.updatePlaylist()
2020-06-23 18:40:44 +02:00
}
2021-08-28 00:02:50 +02:00
if (wasEmpty && playlist.isNotEmpty()) {
if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.skip(0, 0)
localMediaPlayer.setPlayerState(PlayerState.STARTED)
} else {
2021-08-28 00:02:50 +02:00
localMediaPlayer.play(playlist[0])
2020-06-23 18:40:44 +02:00
}
}
}
companion object {
const val PARALLEL_DOWNLOADS = 3
2021-08-28 00:02:50 +02:00
const val CHECK_INTERVAL = 5L
const val SHUFFLE_BUFFER_LIMIT = 4
}
2020-06-23 18:40:44 +02:00
}