Merge branch 'develop' into semanticDownloadView

This commit is contained in:
Nite 2021-09-01 18:19:55 +02:00 committed by GitHub
commit 9f1315b6dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 466 additions and 603 deletions

View File

@ -4,7 +4,6 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ListView; import android.widget.ListView;
@ -25,8 +24,8 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker;
import org.moire.ultrasonic.subsonic.VideoPlayer; import org.moire.ultrasonic.subsonic.VideoPlayer;
import org.moire.ultrasonic.util.CancellationToken; import org.moire.ultrasonic.util.CancellationToken;
import org.moire.ultrasonic.util.Constants; import org.moire.ultrasonic.util.Constants;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.FragmentBackgroundTask; import org.moire.ultrasonic.util.FragmentBackgroundTask;
import org.moire.ultrasonic.util.Pair;
import org.moire.ultrasonic.util.Util; import org.moire.ultrasonic.util.Util;
import org.moire.ultrasonic.view.EntryAdapter; import org.moire.ultrasonic.view.EntryAdapter;
@ -78,23 +77,14 @@ public class BookmarksFragment extends Fragment {
refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh); refreshAlbumListView = view.findViewById(R.id.select_album_entries_refresh);
albumListView = view.findViewById(R.id.select_album_entries_list); albumListView = view.findViewById(R.id.select_album_entries_list);
refreshAlbumListView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() refreshAlbumListView.setOnRefreshListener(() -> {
{
@Override
public void onRefresh()
{
enableButtons(); enableButtons();
getBookmarks(); getBookmarks();
}
}); });
albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); albumListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
albumListView.setOnItemClickListener(new AdapterView.OnItemClickListener() albumListView.setOnItemClickListener((parent, view17, position, id) -> {
{
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
{
if (position >= 0) if (position >= 0)
{ {
MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position); MusicDirectory.Entry entry = (MusicDirectory.Entry) parent.getItemAtPosition(position);
@ -111,7 +101,6 @@ public class BookmarksFragment extends Fragment {
} }
} }
} }
}
}); });
ImageView selectButton = view.findViewById(R.id.select_album_select); ImageView selectButton = view.findViewById(R.id.select_album_select);
@ -130,58 +119,24 @@ public class BookmarksFragment extends Fragment {
playLastButton.setVisibility(View.GONE); playLastButton.setVisibility(View.GONE);
oreButton.setVisibility(View.GONE); oreButton.setVisibility(View.GONE);
playNowButton.setOnClickListener(new View.OnClickListener() playNowButton.setOnClickListener(view16 -> playNow(getSelectedSongs(albumListView)));
{
@Override
public void onClick(View view)
{
playNow(getSelectedSongs(albumListView));
}
});
selectButton.setOnClickListener(new View.OnClickListener() selectButton.setOnClickListener(view15 -> selectAllOrNone());
{ pinButton.setOnClickListener(view14 -> {
@Override
public void onClick(View view)
{
selectAllOrNone();
}
});
pinButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view)
{
downloadBackground(true); downloadBackground(true);
selectAll(false, false); selectAll(false, false);
}
}); });
unpinButton.setOnClickListener(new View.OnClickListener() unpinButton.setOnClickListener(view13 -> {
{
@Override
public void onClick(View view)
{
unpin(); unpin();
selectAll(false, false); selectAll(false, false);
}
}); });
downloadButton.setOnClickListener(new View.OnClickListener() downloadButton.setOnClickListener(view12 -> {
{
@Override
public void onClick(View view)
{
downloadBackground(false); downloadBackground(false);
selectAll(false, false); selectAll(false, false);
}
}); });
deleteButton.setOnClickListener(new View.OnClickListener() deleteButton.setOnClickListener(view1 -> {
{
@Override
public void onClick(View view)
{
delete(); delete();
selectAll(false, false); selectAll(false, false);
}
}); });
registerForContextMenu(albumListView); registerForContextMenu(albumListView);
@ -230,7 +185,8 @@ public class BookmarksFragment extends Fragment {
{ {
if (albumListView.isItemChecked(i)) if (albumListView.isItemChecked(i))
{ {
songs.add((MusicDirectory.Entry) albumListView.getItemAtPosition(i)); MusicDirectory.Entry song = (MusicDirectory.Entry) albumListView.getItemAtPosition(i);
if (song != null) songs.add(song);
} }
} }
} }
@ -291,6 +247,7 @@ public class BookmarksFragment extends Fragment {
for (MusicDirectory.Entry song : selection) for (MusicDirectory.Entry song : selection)
{ {
if (song == null) continue;
DownloadFile downloadFile = mediaPlayerController.getValue().getDownloadFileForSong(song); DownloadFile downloadFile = mediaPlayerController.getValue().getDownloadFileForSong(song);
if (downloadFile.isWorkDone()) if (downloadFile.isWorkDone())
{ {
@ -326,11 +283,7 @@ public class BookmarksFragment extends Fragment {
private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs) private void downloadBackground(final boolean save, final List<MusicDirectory.Entry> songs)
{ {
Runnable onValid = new Runnable() Runnable onValid = () -> {
{
@Override
public void run()
{
networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable(); networkAndStorageChecker.getValue().warnIfNetworkOrStorageUnavailable();
mediaPlayerController.getValue().downloadBackground(songs, save); mediaPlayerController.getValue().downloadBackground(songs, save);
@ -342,7 +295,6 @@ public class BookmarksFragment extends Fragment {
{ {
Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size())); Util.toast(getContext(), getResources().getQuantityString(R.plurals.select_album_n_songs_downloaded, songs.size(), songs.size()));
} }
}
}; };
onValid.run(); onValid.run();

View File

@ -562,7 +562,7 @@ public class SearchFragment extends Fragment {
mediaPlayerController.clear(); mediaPlayerController.clear();
} }
mediaPlayerController.download(Collections.singletonList(song), false, false, false, false, false); mediaPlayerController.addToPlaylist(Collections.singletonList(song), false, false, false, false, false);
if (true) if (true)
{ {

View File

@ -1,445 +0,0 @@
package org.moire.ultrasonic.service;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.util.LRUCache;
import org.moire.ultrasonic.util.ShufflePlayBuffer;
import org.moire.ultrasonic.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import kotlin.Lazy;
import timber.log.Timber;
import static org.koin.java.KoinJavaComponent.inject;
import static org.moire.ultrasonic.domain.PlayerState.DOWNLOADING;
import static org.moire.ultrasonic.domain.PlayerState.STARTED;
/**
* This class is responsible for maintaining the playlist and downloading
* its items from the network to the filesystem.
*/
public class Downloader
{
public final List<DownloadFile> downloadList = new ArrayList<>();
public final List<DownloadFile> backgroundDownloadList = new ArrayList<>();
public DownloadFile currentDownloading;
private final ShufflePlayBuffer shufflePlayBuffer;
private final ExternalStorageMonitor externalStorageMonitor;
private final LocalMediaPlayer localMediaPlayer;
// TODO: This is a circular reference, try to remove
private final Lazy<JukeboxMediaPlayer> jukeboxMediaPlayer = inject(JukeboxMediaPlayer.class);
private final List<DownloadFile> cleanupCandidates = new ArrayList<>();
private final LRUCache<MusicDirectory.Entry, DownloadFile> downloadFileCache = new LRUCache<>(100);
private ScheduledExecutorService executorService;
private long revision;
public Downloader(ShufflePlayBuffer shufflePlayBuffer, ExternalStorageMonitor externalStorageMonitor,
LocalMediaPlayer localMediaPlayer)
{
this.shufflePlayBuffer = shufflePlayBuffer;
this.externalStorageMonitor = externalStorageMonitor;
this.localMediaPlayer = localMediaPlayer;
}
public void onCreate()
{
Runnable downloadChecker = () -> {
try
{
checkDownloads();
}
catch (Throwable x)
{
Timber.e(x,"checkDownloads() failed.");
}
};
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleWithFixedDelay(downloadChecker, 5, 5, TimeUnit.SECONDS);
Timber.i("Downloader created");
}
public void onDestroy()
{
stop();
clear();
clearBackground();
Timber.i("Downloader destroyed");
}
public void stop()
{
if (executorService != null) executorService.shutdown();
Timber.i("Downloader stopped");
}
public synchronized void checkDownloads()
{
if (!Util.isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable())
{
return;
}
if (shufflePlayBuffer.isEnabled)
{
checkShufflePlay();
}
if (jukeboxMediaPlayer.getValue().isEnabled() || !Util.isNetworkConnected())
{
return;
}
if (downloadList.isEmpty() && backgroundDownloadList.isEmpty())
{
return;
}
// Need to download current playing?
if (localMediaPlayer.currentPlaying != null && localMediaPlayer.currentPlaying != currentDownloading && !localMediaPlayer.currentPlaying.isWorkDone())
{
// Cancel current download, if necessary.
if (currentDownloading != null)
{
currentDownloading.cancelDownload();
}
currentDownloading = localMediaPlayer.currentPlaying;
currentDownloading.download();
cleanupCandidates.add(currentDownloading);
// Delete obsolete .partial and .complete files.
cleanup();
return;
}
// Find a suitable target for download.
if (currentDownloading != null &&
!currentDownloading.isWorkDone() &&
(!currentDownloading.isFailed() || (downloadList.isEmpty() && backgroundDownloadList.isEmpty())))
{
cleanup();
return;
}
// There is a target to download
currentDownloading = null;
int n = downloadList.size();
int preloaded = 0;
if (n != 0)
{
int start = localMediaPlayer.currentPlaying == null ? 0 : getCurrentPlayingIndex();
if (start == -1) start = 0;
int i = start;
// Check all DownloadFiles on the playlist
do
{
DownloadFile downloadFile = downloadList.get(i);
if (!downloadFile.isWorkDone())
{
if (downloadFile.shouldSave() || preloaded < Util.getPreloadCount())
{
currentDownloading = downloadFile;
currentDownloading.download();
cleanupCandidates.add(currentDownloading);
if (i == (start + 1))
{
// The next file on the playlist is currently downloading
localMediaPlayer.setNextPlayerState(DOWNLOADING);
}
break;
}
}
else if (localMediaPlayer.currentPlaying != downloadFile)
{
preloaded++;
}
i = (i + 1) % n;
} while (i != start);
}
// If the downloadList contains no work, check the backgroundDownloadList
if ((preloaded + 1 == n || preloaded >= Util.getPreloadCount() || downloadList.isEmpty()) && !backgroundDownloadList.isEmpty())
{
for (int i = 0; i < backgroundDownloadList.size(); i++)
{
DownloadFile downloadFile = backgroundDownloadList.get(i);
if (downloadFile.isWorkDone() && (!downloadFile.shouldSave() || downloadFile.isSaved()))
{
Util.scanMedia(downloadFile.getCompleteFile());
// Don't need to keep list like active song list
backgroundDownloadList.remove(i);
revision++;
i--;
}
else if (downloadFile.isFailed() && !downloadFile.shouldRetry()) {
// Don't continue to attempt to download forever
backgroundDownloadList.remove(i);
revision++;
i--;
}
else
{
currentDownloading = downloadFile;
currentDownloading.download();
cleanupCandidates.add(currentDownloading);
break;
}
}
}
// Delete obsolete .partial and .complete files.
cleanup();
}
public synchronized int getCurrentPlayingIndex()
{
return downloadList.indexOf(localMediaPlayer.currentPlaying);
}
public long getDownloadListDuration()
{
long totalDuration = 0;
for (DownloadFile downloadFile : downloadList)
{
MusicDirectory.Entry entry = downloadFile.getSong();
if (!entry.isDirectory())
{
if (entry.getArtist() != null)
{
Integer duration = entry.getDuration();
if (duration != null)
{
totalDuration += duration;
}
}
}
}
return totalDuration;
}
public synchronized List<DownloadFile> getDownloads()
{
List<DownloadFile> temp = new ArrayList<>();
temp.addAll(downloadList);
temp.addAll(backgroundDownloadList);
return temp;
}
public long getDownloadListUpdateRevision()
{
return revision;
}
public synchronized void clear()
{
downloadList.clear();
revision++;
if (currentDownloading != null)
{
currentDownloading.cancelDownload();
currentDownloading = null;
}
}
private void clearBackground()
{
if (currentDownloading != null && backgroundDownloadList.contains(currentDownloading))
{
currentDownloading.cancelDownload();
currentDownloading = null;
}
backgroundDownloadList.clear();
}
public synchronized void removeDownloadFile(DownloadFile downloadFile)
{
if (downloadFile == currentDownloading)
{
currentDownloading.cancelDownload();
currentDownloading = null;
}
downloadList.remove(downloadFile);
backgroundDownloadList.remove(downloadFile);
revision++;
}
public synchronized void download(List<MusicDirectory.Entry> songs, boolean save, boolean autoPlay, boolean playNext, boolean newPlaylist)
{
shufflePlayBuffer.isEnabled = false;
int offset = 1;
if (songs.isEmpty())
{
return;
}
if (newPlaylist)
{
downloadList.clear();
}
if (playNext)
{
if (autoPlay && getCurrentPlayingIndex() >= 0)
{
offset = 0;
}
for (MusicDirectory.Entry song : songs)
{
DownloadFile downloadFile = new DownloadFile(song, save);
downloadList.add(getCurrentPlayingIndex() + offset, downloadFile);
offset++;
}
}
else
{
for (MusicDirectory.Entry song : songs)
{
DownloadFile downloadFile = new DownloadFile(song, save);
downloadList.add(downloadFile);
}
}
revision++;
}
public synchronized void downloadBackground(List<MusicDirectory.Entry> songs, boolean save)
{
for (MusicDirectory.Entry song : songs)
{
DownloadFile downloadFile = new DownloadFile(song, save);
backgroundDownloadList.add(downloadFile);
}
revision++;
checkDownloads();
}
public synchronized void shuffle()
{
Collections.shuffle(downloadList);
if (localMediaPlayer.currentPlaying != null)
{
downloadList.remove(localMediaPlayer.currentPlaying);
downloadList.add(0, localMediaPlayer.currentPlaying);
}
revision++;
}
public synchronized DownloadFile getDownloadFileForSong(MusicDirectory.Entry song)
{
for (DownloadFile downloadFile : downloadList)
{
if (downloadFile.getSong().equals(song) && ((downloadFile.isDownloading() && !downloadFile.isDownloadCancelled() && downloadFile.getPartialFile().exists()) || downloadFile.isWorkDone()))
{
return downloadFile;
}
}
for (DownloadFile downloadFile : backgroundDownloadList)
{
if (downloadFile.getSong().equals(song))
{
return downloadFile;
}
}
DownloadFile downloadFile = downloadFileCache.get(song);
if (downloadFile == null)
{
downloadFile = new DownloadFile(song, false);
downloadFileCache.put(song, downloadFile);
}
return downloadFile;
}
private synchronized void cleanup()
{
Iterator<DownloadFile> iterator = cleanupCandidates.iterator();
while (iterator.hasNext())
{
DownloadFile downloadFile = iterator.next();
if (downloadFile != localMediaPlayer.currentPlaying && downloadFile != currentDownloading)
{
if (downloadFile.cleanup())
{
iterator.remove();
}
}
}
}
private synchronized void checkShufflePlay()
{
// Get users desired random playlist size
int listSize = Util.getMaxSongs();
boolean wasEmpty = downloadList.isEmpty();
long revisionBefore = revision;
// First, ensure that list is at least 20 songs long.
int size = downloadList.size();
if (size < listSize)
{
for (MusicDirectory.Entry song : shufflePlayBuffer.get(listSize - size))
{
DownloadFile downloadFile = new DownloadFile(song, false);
downloadList.add(downloadFile);
revision++;
}
}
int currIndex = localMediaPlayer.currentPlaying == null ? 0 : getCurrentPlayingIndex();
// Only shift playlist if playing song #5 or later.
if (currIndex > 4)
{
int songsToShift = currIndex - 2;
for (MusicDirectory.Entry song : shufflePlayBuffer.get(songsToShift))
{
downloadList.add(new DownloadFile(song, false));
downloadList.get(0).cancelDownload();
downloadList.remove(0);
revision++;
}
}
if (revisionBefore != revision)
{
jukeboxMediaPlayer.getValue().updatePlaylist();
}
if (wasEmpty && !downloadList.isEmpty())
{
if (jukeboxMediaPlayer.getValue().isEnabled())
{
jukeboxMediaPlayer.getValue().skip(0, 0);
localMediaPlayer.setPlayerState(STARTED);
}
else
{
localMediaPlayer.play(downloadList.get(0));
}
}
}
}

View File

@ -0,0 +1,353 @@
package org.moire.ultrasonic.service
import java.util.ArrayList
import java.util.PriorityQueue
import java.util.concurrent.Executors
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
import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.util.LRUCache
import org.moire.ultrasonic.util.ShufflePlayBuffer
import org.moire.ultrasonic.util.Util.getMaxSongs
import org.moire.ultrasonic.util.Util.getPreloadCount
import org.moire.ultrasonic.util.Util.isExternalStoragePresent
import org.moire.ultrasonic.util.Util.isNetworkConnected
import timber.log.Timber
/**
* This class is responsible for maintaining the playlist and downloading
* its items from the network to the filesystem.
*
* TODO: Implement LiveData
* 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
) : KoinComponent {
val playlist: MutableList<DownloadFile> = ArrayList()
private val downloadQueue: PriorityQueue<DownloadFile> = PriorityQueue<DownloadFile>()
private val activelyDownloading: MutableList<DownloadFile> = ArrayList()
private val jukeboxMediaPlayer: JukeboxMediaPlayer by inject()
private val downloadFileCache = LRUCache<MusicDirectory.Entry, DownloadFile>(100)
private var executorService: ScheduledExecutorService? = null
var playlistUpdateRevision: Long = 0
private set
val downloadChecker = Runnable {
try {
Timber.w("checking Downloads")
checkDownloadsInternal()
} catch (all: Exception) {
Timber.e(all, "checkDownloads() failed.")
}
}
fun onCreate() {
executorService = Executors.newSingleThreadScheduledExecutor()
executorService!!.scheduleWithFixedDelay(
downloadChecker, CHECK_INTERVAL, CHECK_INTERVAL, TimeUnit.SECONDS
)
Timber.i("Downloader created")
}
fun onDestroy() {
stop()
clearPlaylist()
clearBackground()
Timber.i("Downloader destroyed")
}
fun stop() {
if (executorService != null) executorService!!.shutdown()
Timber.i("Downloader stopped")
}
fun checkDownloads() {
executorService?.execute(downloadChecker)
}
@Synchronized
fun checkDownloadsInternal() {
if (!isExternalStoragePresent() || !externalStorageMonitor.isExternalStorageAvailable) {
return
}
if (shufflePlayBuffer.isEnabled) {
checkShufflePlay()
}
if (jukeboxMediaPlayer.isEnabled || !isNetworkConnected()) {
return
}
// Check the active downloads for failures or completions and remove them
cleanupActiveDownloads()
// Check if need to preload more from playlist
val preloadCount = getPreloadCount()
// Start preloading at the current playing song
var start = currentPlayingIndex
if (start == -1) start = 0
val end = (start + preloadCount).coerceAtMost(playlist.size)
for (i in start until end) {
val download = playlist[i]
// Set correct priority (the lower the number, the higher the priority)
download.priority = i
// Add file to queue if not in one of the queues already.
if (!download.isWorkDone &&
!activelyDownloading.contains(download) &&
!downloadQueue.contains(download)
) {
downloadQueue.add(download)
}
}
// Fill up active List with waiting tasks
while (activelyDownloading.size < PARALLEL_DOWNLOADS && downloadQueue.size > 0) {
val task = downloadQueue.remove()
activelyDownloading.add(task)
task.download()
// The next file on the playlist is currently downloading
if (playlist.indexOf(task) == 1) {
localMediaPlayer.setNextPlayerState(PlayerState.DOWNLOADING)
}
}
}
private fun cleanupActiveDownloads() {
activelyDownloading.retainAll {
when {
it.isDownloading -> true
it.isFailed && it.shouldRetry() -> {
// Add it back to queue
downloadQueue.add(it)
false
}
else -> {
it.cleanup()
false
}
}
}
}
@get:Synchronized
val currentPlayingIndex: Int
get() = playlist.indexOf(localMediaPlayer.currentPlaying)
@get:Synchronized
val downloadListDuration: Long
get() {
var totalDuration: Long = 0
for (downloadFile in playlist) {
val song = downloadFile.song
if (!song.isDirectory) {
if (song.artist != null) {
if (song.duration != null) {
totalDuration += song.duration!!.toLong()
}
}
}
}
return totalDuration
}
@get:Synchronized
val downloads: List<DownloadFile?>
get() {
val temp: MutableList<DownloadFile?> = ArrayList()
temp.addAll(playlist)
temp.addAll(activelyDownloading)
temp.addAll(downloadQueue)
return temp.distinct()
}
@Synchronized
fun clearPlaylist() {
playlist.clear()
// Cancel all active downloads with a high priority
for (download in activelyDownloading) {
if (download.priority < 100)
download.cancelDownload()
}
playlistUpdateRevision++
}
@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()
}
}
@Synchronized
fun clearActiveDownloads() {
// Cancel all active downloads with a low priority
for (download in activelyDownloading) {
download.cancelDownload()
}
}
@Synchronized
fun removeFromPlaylist(downloadFile: DownloadFile) {
if (activelyDownloading.contains(downloadFile)) {
downloadFile.cancelDownload()
}
playlist.remove(downloadFile)
playlistUpdateRevision++
}
@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
}
if (newPlaylist) {
playlist.clear()
}
if (playNext) {
if (autoPlay && currentPlayingIndex >= 0) {
offset = 0
}
for (song in songs) {
val downloadFile = DownloadFile(song!!, save)
playlist.add(currentPlayingIndex + offset, downloadFile)
offset++
}
} else {
for (song in songs) {
val downloadFile = DownloadFile(song!!, save)
playlist.add(downloadFile)
}
}
playlistUpdateRevision++
checkDownloads()
}
@Synchronized
fun downloadBackground(songs: List<MusicDirectory.Entry>, save: Boolean) {
// 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))
}
checkDownloads()
}
@Synchronized
fun shuffle() {
playlist.shuffle()
// Move the current song to the top..
if (localMediaPlayer.currentPlaying != null) {
playlist.remove(localMediaPlayer.currentPlaying)
playlist.add(0, localMediaPlayer.currentPlaying!!)
}
playlistUpdateRevision++
}
@Synchronized
@Suppress("ReturnCount")
fun getDownloadFileForSong(song: MusicDirectory.Entry): DownloadFile {
for (downloadFile in playlist) {
if (downloadFile.song == song) {
return downloadFile
}
}
for (downloadFile in activelyDownloading) {
if (downloadFile.song == song) {
return downloadFile
}
}
for (downloadFile in downloadQueue) {
if (downloadFile.song == song) {
return downloadFile
}
}
var downloadFile = downloadFileCache[song]
if (downloadFile == null) {
downloadFile = DownloadFile(song, false)
downloadFileCache.put(song, downloadFile)
}
return downloadFile
}
@Synchronized
private fun checkShufflePlay() {
// Get users desired random playlist size
val listSize = getMaxSongs()
val wasEmpty = playlist.isEmpty()
val revisionBefore = playlistUpdateRevision
// First, ensure that list is at least 20 songs long.
val size = playlist.size
if (size < listSize) {
for (song in shufflePlayBuffer[listSize - size]) {
val downloadFile = DownloadFile(song, false)
playlist.add(downloadFile)
playlistUpdateRevision++
}
}
val currIndex = if (localMediaPlayer.currentPlaying == null) 0 else currentPlayingIndex
// Only shift playlist if playing song #5 or later.
if (currIndex > SHUFFLE_BUFFER_LIMIT) {
val songsToShift = currIndex - 2
for (song in shufflePlayBuffer[songsToShift]) {
playlist.add(DownloadFile(song, false))
playlist[0].cancelDownload()
playlist.removeAt(0)
playlistUpdateRevision++
}
}
if (revisionBefore != playlistUpdateRevision) {
jukeboxMediaPlayer.updatePlaylist()
}
if (wasEmpty && playlist.isNotEmpty()) {
if (jukeboxMediaPlayer.isEnabled) {
jukeboxMediaPlayer.skip(0, 0)
localMediaPlayer.setPlayerState(PlayerState.STARTED)
} else {
localMediaPlayer.play(playlist[0])
}
}
}
companion object {
const val PARALLEL_DOWNLOADS = 3
const val CHECK_INTERVAL = 5L
const val SHUFFLE_BUFFER_LIMIT = 4
}
}

View File

@ -456,7 +456,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
requireActivity().invalidateOptionsMenu() requireActivity().invalidateOptionsMenu()
} }
// Scroll to current playing/downloading. // Scroll to current playing.
private fun scrollToCurrent() { private fun scrollToCurrent() {
val adapter = playlistView.adapter val adapter = playlistView.adapter
if (adapter != null) { if (adapter != null) {
@ -467,13 +467,6 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
return return
} }
} }
val currentDownloading = mediaPlayerController.currentDownloading
for (i in 0 until count) {
if (currentDownloading == playlistView.getItemAtPosition(i)) {
playlistView.smoothScrollToPositionFromTop(i, 40)
return
}
}
} }
} }
@ -643,7 +636,7 @@ class PlayerFragment : Fragment(), GestureDetector.OnGestureListener, KoinCompon
return true return true
} }
R.id.menu_remove -> { R.id.menu_remove -> {
mediaPlayerController.remove(song!!) mediaPlayerController.removeFromPlaylist(song!!)
onDownloadListChanged() onDownloadListChanged()
return true return true
} }

View File

@ -517,6 +517,7 @@ class TrackCollectionFragment : Fragment() {
var pinnedCount = 0 var pinnedCount = 0
for (song in selection) { for (song in selection) {
if (song == null) continue
val downloadFile = mediaPlayerController.getDownloadFileForSong(song) val downloadFile = mediaPlayerController.getDownloadFileForSong(song)
if (downloadFile.isWorkDone) { if (downloadFile.isWorkDone) {
deleteEnabled = true deleteEnabled = true

View File

@ -1066,7 +1066,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
} }
private fun playSongs(songs: List<MusicDirectory.Entry?>?) { private fun playSongs(songs: List<MusicDirectory.Entry?>?) {
mediaPlayerController.download( mediaPlayerController.addToPlaylist(
songs, songs,
save = false, save = false,
autoPlay = true, autoPlay = true,
@ -1077,7 +1077,7 @@ class AutoMediaBrowserService : MediaBrowserServiceCompat() {
} }
private fun playSong(song: MusicDirectory.Entry) { private fun playSong(song: MusicDirectory.Entry) {
mediaPlayerController.download( mediaPlayerController.addToPlaylist(
listOf(song), listOf(song),
save = false, save = false,
autoPlay = false, autoPlay = false,

View File

@ -40,7 +40,7 @@ import timber.log.Timber
class DownloadFile( class DownloadFile(
val song: MusicDirectory.Entry, val song: MusicDirectory.Entry,
private val save: Boolean private val save: Boolean
) : KoinComponent { ) : KoinComponent, Comparable<DownloadFile> {
val partialFile: File val partialFile: File
val completeFile: File val completeFile: File
private val saveFile: File = FileUtil.getSongFile(song) private val saveFile: File = FileUtil.getSongFile(song)
@ -50,6 +50,8 @@ class DownloadFile(
private val desiredBitRate: Int = Util.getMaxBitRate() private val desiredBitRate: Int = Util.getMaxBitRate()
var priority = 100
@Volatile @Volatile
private var isPlaying = false private var isPlaying = false
@ -202,7 +204,6 @@ class DownloadFile(
return String.format("DownloadFile (%s)", song) return String.format("DownloadFile (%s)", song)
} }
@Suppress("TooGenericExceptionCaught")
private inner class DownloadTask : CancellableTask() { private inner class DownloadTask : CancellableTask() {
override fun execute() { override fun execute() {
var inputStream: InputStream? = null var inputStream: InputStream? = null
@ -291,7 +292,7 @@ class DownloadFile(
Util.renameFile(partialFile, completeFile) Util.renameFile(partialFile, completeFile)
} }
} }
} catch (e: Exception) { } catch (all: Exception) {
Util.close(outputStream) Util.close(outputStream)
Util.delete(completeFile) Util.delete(completeFile)
Util.delete(saveFile) Util.delete(saveFile)
@ -300,7 +301,7 @@ class DownloadFile(
if (retryCount > 0) { if (retryCount > 0) {
--retryCount --retryCount
} }
Timber.w(e, "Failed to download '%s'.", song) Timber.w(all, "Failed to download '%s'.", song)
} }
} finally { } finally {
Util.close(inputStream) Util.close(inputStream)
@ -388,6 +389,10 @@ class DownloadFile(
} }
} }
override fun compareTo(other: DownloadFile): Int {
return priority.compareTo(other.priority)
}
companion object { companion object {
const val MAX_RETRIES = 5 const val MAX_RETRIES = 5
} }

View File

@ -74,7 +74,7 @@ class MediaPlayerController(
autoPlay: Boolean, autoPlay: Boolean,
newPlaylist: Boolean newPlaylist: Boolean
) { ) {
download( addToPlaylist(
songs, songs,
save = false, save = false,
autoPlay = false, autoPlay = false,
@ -167,7 +167,7 @@ class MediaPlayerController(
@Synchronized @Synchronized
@Suppress("LongParameterList") @Suppress("LongParameterList")
fun download( fun addToPlaylist(
songs: List<MusicDirectory.Entry?>?, songs: List<MusicDirectory.Entry?>?,
save: Boolean, save: Boolean,
autoPlay: Boolean, autoPlay: Boolean,
@ -175,10 +175,12 @@ class MediaPlayerController(
shuffle: Boolean, shuffle: Boolean,
newPlaylist: Boolean newPlaylist: Boolean
) { ) {
downloader.download(songs, save, autoPlay, playNext, newPlaylist) if (songs == null) return
val filteredSongs = songs.filterNotNull()
downloader.addToPlaylist(filteredSongs, save, autoPlay, playNext, newPlaylist)
jukeboxMediaPlayer.updatePlaylist() jukeboxMediaPlayer.updatePlaylist()
if (shuffle) shuffle() if (shuffle) shuffle()
val isLastTrack = (downloader.downloadList.size - 1 == downloader.currentPlayingIndex) val isLastTrack = (downloader.playlist.size - 1 == downloader.currentPlayingIndex)
if (!playNext && !autoPlay && isLastTrack) { if (!playNext && !autoPlay && isLastTrack) {
val mediaPlayerService = runningInstance val mediaPlayerService = runningInstance
@ -188,15 +190,15 @@ class MediaPlayerController(
if (autoPlay) { if (autoPlay) {
play(0) play(0)
} else { } else {
if (localMediaPlayer.currentPlaying == null && downloader.downloadList.size > 0) { if (localMediaPlayer.currentPlaying == null && downloader.playlist.size > 0) {
localMediaPlayer.currentPlaying = downloader.downloadList[0] localMediaPlayer.currentPlaying = downloader.playlist[0]
downloader.downloadList[0].setPlaying(true) downloader.playlist[0].setPlaying(true)
} }
downloader.checkDownloads() downloader.checkDownloads()
} }
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
playerPosition playerPosition
) )
@ -204,9 +206,11 @@ class MediaPlayerController(
@Synchronized @Synchronized
fun downloadBackground(songs: List<MusicDirectory.Entry?>?, save: Boolean) { fun downloadBackground(songs: List<MusicDirectory.Entry?>?, save: Boolean) {
downloader.downloadBackground(songs, save) if (songs == null) return
val filteredSongs = songs.filterNotNull()
downloader.downloadBackground(filteredSongs, save)
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
playerPosition playerPosition
) )
@ -237,7 +241,7 @@ class MediaPlayerController(
fun shuffle() { fun shuffle() {
downloader.shuffle() downloader.shuffle()
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
playerPosition playerPosition
) )
@ -267,10 +271,10 @@ class MediaPlayerController(
mediaPlayerService.clear(serialize) mediaPlayerService.clear(serialize)
} else { } else {
// If no MediaPlayerService is available, just empty the playlist // If no MediaPlayerService is available, just empty the playlist
downloader.clear() downloader.clearPlaylist()
if (serialize) { if (serialize) {
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, playerPosition downloader.currentPlayingIndex, playerPosition
) )
} }
@ -281,7 +285,7 @@ class MediaPlayerController(
@Synchronized @Synchronized
fun clearIncomplete() { fun clearIncomplete() {
reset() reset()
val iterator = downloader.downloadList.iterator() val iterator = downloader.playlist.iterator()
while (iterator.hasNext()) { while (iterator.hasNext()) {
val downloadFile = iterator.next() val downloadFile = iterator.next()
if (!downloadFile.isCompleteFileAvailable) { if (!downloadFile.isCompleteFileAvailable) {
@ -290,7 +294,7 @@ class MediaPlayerController(
} }
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
playerPosition playerPosition
) )
@ -299,15 +303,15 @@ class MediaPlayerController(
} }
@Synchronized @Synchronized
fun remove(downloadFile: DownloadFile) { fun removeFromPlaylist(downloadFile: DownloadFile) {
if (downloadFile == localMediaPlayer.currentPlaying) { if (downloadFile == localMediaPlayer.currentPlaying) {
reset() reset()
currentPlaying = null currentPlaying = null
} }
downloader.removeDownloadFile(downloadFile) downloader.removeFromPlaylist(downloadFile)
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
playerPosition playerPosition
) )
@ -321,15 +325,17 @@ class MediaPlayerController(
} }
@Synchronized @Synchronized
// TODO: Make it require not null
fun delete(songs: List<MusicDirectory.Entry?>) { fun delete(songs: List<MusicDirectory.Entry?>) {
for (song in songs) { for (song in songs.filterNotNull()) {
downloader.getDownloadFileForSong(song).delete() downloader.getDownloadFileForSong(song).delete()
} }
} }
@Synchronized @Synchronized
// TODO: Make it require not null
fun unpin(songs: List<MusicDirectory.Entry?>) { fun unpin(songs: List<MusicDirectory.Entry?>) {
for (song in songs) { for (song in songs.filterNotNull()) {
downloader.getDownloadFileForSong(song).unpin() downloader.getDownloadFileForSong(song).unpin()
} }
} }
@ -357,12 +363,12 @@ class MediaPlayerController(
when (repeatMode) { when (repeatMode) {
RepeatMode.SINGLE, RepeatMode.OFF -> { RepeatMode.SINGLE, RepeatMode.OFF -> {
// Play next if exists // Play next if exists
if (index + 1 >= 0 && index + 1 < downloader.downloadList.size) { if (index + 1 >= 0 && index + 1 < downloader.playlist.size) {
play(index + 1) play(index + 1)
} }
} }
RepeatMode.ALL -> { RepeatMode.ALL -> {
play((index + 1) % downloader.downloadList.size) play((index + 1) % downloader.playlist.size)
} }
else -> { else -> {
} }
@ -409,9 +415,7 @@ class MediaPlayerController(
reset() reset()
// Cancel current download, if necessary. // Cancel current download, if necessary.
if (downloader.currentDownloading != null) { downloader.clearActiveDownloads()
downloader.currentDownloading.cancelDownload()
}
} else { } else {
jukeboxMediaPlayer.stopJukeboxService() jukeboxMediaPlayer.stopJukeboxService()
} }
@ -491,24 +495,21 @@ class MediaPlayerController(
} }
val playlistSize: Int val playlistSize: Int
get() = downloader.downloadList.size get() = downloader.playlist.size
val currentPlayingNumberOnPlaylist: Int val currentPlayingNumberOnPlaylist: Int
get() = downloader.currentPlayingIndex get() = downloader.currentPlayingIndex
val currentDownloading: DownloadFile?
get() = downloader.currentDownloading
val playList: List<DownloadFile> val playList: List<DownloadFile>
get() = downloader.downloadList get() = downloader.playlist
val playListUpdateRevision: Long val playListUpdateRevision: Long
get() = downloader.downloadListUpdateRevision get() = downloader.playlistUpdateRevision
val playListDuration: Long val playListDuration: Long
get() = downloader.downloadListDuration get() = downloader.downloadListDuration
fun getDownloadFileForSong(song: MusicDirectory.Entry?): DownloadFile { fun getDownloadFileForSong(song: MusicDirectory.Entry): DownloadFile {
return downloader.getDownloadFileForSong(song) return downloader.getDownloadFileForSong(song)
} }

View File

@ -76,7 +76,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
// Work-around: Serialize again, as the restore() method creates a // Work-around: Serialize again, as the restore() method creates a
// serialization without current playing info. // serialization without current playing info.
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
mediaPlayerController.playerPosition mediaPlayerController.playerPosition
) )
@ -94,7 +94,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
if (!created) return if (!created) return
downloadQueueSerializer.serializeDownloadQueueNow( downloadQueueSerializer.serializeDownloadQueueNow(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
mediaPlayerController.playerPosition mediaPlayerController.playerPosition
) )

View File

@ -88,7 +88,7 @@ class MediaPlayerService : Service() {
localMediaPlayer.onPrepared = { localMediaPlayer.onPrepared = {
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, downloader.currentPlayingIndex,
playerPosition playerPosition
) )
@ -189,7 +189,7 @@ class MediaPlayerService : Service() {
@Synchronized @Synchronized
fun setCurrentPlaying(currentPlayingIndex: Int) { fun setCurrentPlaying(currentPlayingIndex: Int) {
try { try {
localMediaPlayer.setCurrentPlaying(downloader.downloadList[currentPlayingIndex]) localMediaPlayer.setCurrentPlaying(downloader.playlist[currentPlayingIndex])
} catch (ignored: IndexOutOfBoundsException) { } catch (ignored: IndexOutOfBoundsException) {
} }
} }
@ -208,7 +208,7 @@ class MediaPlayerService : Service() {
if (index != -1) { if (index != -1) {
when (repeatMode) { when (repeatMode) {
RepeatMode.OFF -> index += 1 RepeatMode.OFF -> index += 1
RepeatMode.ALL -> index = (index + 1) % downloader.downloadList.size RepeatMode.ALL -> index = (index + 1) % downloader.playlist.size
RepeatMode.SINGLE -> { RepeatMode.SINGLE -> {
} }
else -> { else -> {
@ -217,8 +217,8 @@ class MediaPlayerService : Service() {
} }
localMediaPlayer.clearNextPlaying(false) localMediaPlayer.clearNextPlaying(false)
if (index < downloader.downloadList.size && index != -1) { if (index < downloader.playlist.size && index != -1) {
localMediaPlayer.setNextPlaying(downloader.downloadList[index]) localMediaPlayer.setNextPlaying(downloader.playlist[index])
} else { } else {
localMediaPlayer.clearNextPlaying(true) localMediaPlayer.clearNextPlaying(true)
} }
@ -271,7 +271,7 @@ class MediaPlayerService : Service() {
@Synchronized @Synchronized
fun play(index: Int, start: Boolean) { fun play(index: Int, start: Boolean) {
Timber.v("play requested for %d", index) Timber.v("play requested for %d", index)
if (index < 0 || index >= downloader.downloadList.size) { if (index < 0 || index >= downloader.playlist.size) {
resetPlayback() resetPlayback()
} else { } else {
setCurrentPlaying(index) setCurrentPlaying(index)
@ -280,7 +280,7 @@ class MediaPlayerService : Service() {
jukeboxMediaPlayer.skip(index, 0) jukeboxMediaPlayer.skip(index, 0)
localMediaPlayer.setPlayerState(PlayerState.STARTED) localMediaPlayer.setPlayerState(PlayerState.STARTED)
} else { } else {
localMediaPlayer.play(downloader.downloadList[index]) localMediaPlayer.play(downloader.playlist[index])
} }
} }
downloader.checkDownloads() downloader.checkDownloads()
@ -293,7 +293,7 @@ class MediaPlayerService : Service() {
localMediaPlayer.reset() localMediaPlayer.reset()
localMediaPlayer.setCurrentPlaying(null) localMediaPlayer.setCurrentPlaying(null)
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, playerPosition downloader.currentPlayingIndex, playerPosition
) )
} }
@ -395,7 +395,7 @@ class MediaPlayerService : Service() {
if (playerState === PlayerState.PAUSED) { if (playerState === PlayerState.PAUSED) {
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.currentPlayingIndex, playerPosition downloader.playlist, downloader.currentPlayingIndex, playerPosition
) )
} }
@ -408,8 +408,8 @@ class MediaPlayerService : Service() {
Util.broadcastPlaybackStatusChange(context, playerState) Util.broadcastPlaybackStatusChange(context, playerState)
Util.broadcastA2dpPlayStatusChange( Util.broadcastA2dpPlayStatusChange(
context, playerState, song, context, playerState, song,
downloader.downloadList.size + downloader.backgroundDownloadList.size, downloader.playlist.size,
downloader.downloadList.indexOf(currentPlaying) + 1, playerPosition downloader.playlist.indexOf(currentPlaying) + 1, playerPosition
) )
// Update widget // Update widget
@ -455,7 +455,7 @@ class MediaPlayerService : Service() {
if (index != -1) { if (index != -1) {
when (repeatMode) { when (repeatMode) {
RepeatMode.OFF -> { RepeatMode.OFF -> {
if (index + 1 < 0 || index + 1 >= downloader.downloadList.size) { if (index + 1 < 0 || index + 1 >= downloader.playlist.size) {
if (Util.getShouldClearPlaylist()) { if (Util.getShouldClearPlaylist()) {
clear(true) clear(true)
jukeboxMediaPlayer.updatePlaylist() jukeboxMediaPlayer.updatePlaylist()
@ -466,7 +466,7 @@ class MediaPlayerService : Service() {
} }
} }
RepeatMode.ALL -> { RepeatMode.ALL -> {
play((index + 1) % downloader.downloadList.size) play((index + 1) % downloader.playlist.size)
} }
RepeatMode.SINGLE -> play(index) RepeatMode.SINGLE -> play(index)
else -> { else -> {
@ -480,12 +480,12 @@ class MediaPlayerService : Service() {
@Synchronized @Synchronized
fun clear(serialize: Boolean) { fun clear(serialize: Boolean) {
localMediaPlayer.reset() localMediaPlayer.reset()
downloader.clear() downloader.clearPlaylist()
localMediaPlayer.setCurrentPlaying(null) localMediaPlayer.setCurrentPlaying(null)
setNextPlaying() setNextPlaying()
if (serialize) { if (serialize) {
downloadQueueSerializer.serializeDownloadQueue( downloadQueueSerializer.serializeDownloadQueue(
downloader.downloadList, downloader.playlist,
downloader.currentPlayingIndex, playerPosition downloader.currentPlayingIndex, playerPosition
) )
} }

View File

@ -39,7 +39,7 @@ class DownloadHandler(
mediaPlayerController.clear() mediaPlayerController.clear()
} }
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable() networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
mediaPlayerController.download( mediaPlayerController.addToPlaylist(
songs, songs,
save, save,
autoPlay, autoPlay,
@ -297,7 +297,7 @@ class DownloadHandler(
if (unpin) { if (unpin) {
mediaPlayerController.unpin(songs) mediaPlayerController.unpin(songs)
} else { } else {
mediaPlayerController.download( mediaPlayerController.addToPlaylist(
songs, songs,
save, save,
autoPlay, autoPlay,

View File

@ -218,10 +218,13 @@ class SongView(context: Context) : UpdateView(context), Checkable, KoinComponent
override fun updateBackground() {} override fun updateBackground() {}
@Synchronized
public override fun update() { public override fun update() {
updateBackground() updateBackground()
downloadFile = mediaPlayerController.getDownloadFileForSong(entry) val song = entry ?: return
downloadFile = mediaPlayerController.getDownloadFileForSong(song)
updateDownloadStatus(downloadFile!!) updateDownloadStatus(downloadFile!!)