2
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-09-03 16:15:18 +00:00

Convert play queue classes to Kotlin

This commit is contained in:
Isira Seneviratne
2025-07-04 06:34:49 +05:30
parent c2b6c71947
commit 31f8dd05a7
4 changed files with 319 additions and 454 deletions

View File

@@ -397,7 +397,7 @@ public final class Player implements PlaybackListener, Listener {
&& newQueue.size() == 1 && newQueue.getItem() != null && newQueue.size() == 1 && newQueue.getItem() != null
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
&& newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl()) && newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl())
&& newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { && newQueue.getItem().getRecoveryPosition() != Long.MIN_VALUE) {
// Player can have state = IDLE when playback is stopped or failed // Player can have state = IDLE when playback is stopped or failed
// and we should retry in this case // and we should retry in this case
if (simpleExoPlayer.getPlaybackState() if (simpleExoPlayer.getPlaybackState()
@@ -425,7 +425,7 @@ public final class Player implements PlaybackListener, Listener {
&& !samePlayQueue && !samePlayQueue
&& !newQueue.isEmpty() && !newQueue.isEmpty()
&& newQueue.getItem() != null && newQueue.getItem() != null
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { && newQueue.getItem().getRecoveryPosition() == Long.MIN_VALUE) {
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem()) databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
// Do not place initPlayback() in doFinally() because // Do not place initPlayback() in doFinally() because
@@ -1588,7 +1588,7 @@ public final class Player implements PlaybackListener, Listener {
} }
// sync the player index with the queue index, and seek to the correct position // sync the player index with the queue index, and seek to the correct position
if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { if (item.getRecoveryPosition() != Long.MIN_VALUE) {
simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition()); simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition());
playQueue.unsetRecovery(playQueueIndex); playQueue.unsetRecovery(playQueueIndex);
} else { } else {

View File

@@ -38,9 +38,9 @@ import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.PublishSubject;
import static org.schabi.newpipe.BuildConfig.DEBUG;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException; import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis; import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis;
public class MediaSourceManager { public class MediaSourceManager {

View File

@@ -1,63 +1,62 @@
package org.schabi.newpipe.player.playqueue; package org.schabi.newpipe.player.playqueue
import androidx.annotation.NonNull; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import androidx.annotation.Nullable; import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import org.schabi.newpipe.MainActivity; import io.reactivex.rxjava3.subjects.BehaviorSubject
import org.schabi.newpipe.player.playqueue.events.AppendEvent; import org.schabi.newpipe.player.playqueue.events.AppendEvent
import org.schabi.newpipe.player.playqueue.events.ErrorEvent; import org.schabi.newpipe.player.playqueue.events.ErrorEvent
import org.schabi.newpipe.player.playqueue.events.InitEvent; import org.schabi.newpipe.player.playqueue.events.InitEvent
import org.schabi.newpipe.player.playqueue.events.MoveEvent; import org.schabi.newpipe.player.playqueue.events.MoveEvent
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent; import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent; import org.schabi.newpipe.player.playqueue.events.RecoveryEvent
import org.schabi.newpipe.player.playqueue.events.RemoveEvent; import org.schabi.newpipe.player.playqueue.events.RemoveEvent
import org.schabi.newpipe.player.playqueue.events.ReorderEvent; import org.schabi.newpipe.player.playqueue.events.ReorderEvent
import org.schabi.newpipe.player.playqueue.events.SelectEvent; import org.schabi.newpipe.player.playqueue.events.SelectEvent
import java.io.Serializable
import java.io.Serializable; import java.util.Collections
import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
/** /**
* PlayQueue is responsible for keeping track of a list of streams and the index of * PlayQueue is responsible for keeping track of a list of streams and the index of
* the stream that should be currently playing. * the stream that should be currently playing.
* <p> *
* This class contains basic manipulation of a playlist while also functions as a * This class contains basic manipulation of a playlist while also functions as a
* message bus, providing all listeners with new updates to the play queue. * message bus, providing all listeners with new updates to the play queue.
* </p> *
* <p>
* This class can be serialized for passing intents, but in order to start the * This class can be serialized for passing intents, but in order to start the
* message bus, it must be initialized. * message bus, it must be initialized.
* </p>
*/ */
public abstract class PlayQueue implements Serializable { abstract class PlayQueue internal constructor(
public static final boolean DEBUG = MainActivity.DEBUG; index: Int,
@NonNull startWith: List<PlayQueueItem>,
private final AtomicInteger queueIndex; ) : Serializable {
private final List<PlayQueueItem> history = new ArrayList<>(); private val queueIndex = AtomicInteger(index)
private val history = mutableListOf<PlayQueueItem>()
private var backup = mutableListOf<PlayQueueItem>()
private var streams = startWith.toMutableList()
private List<PlayQueueItem> backup; @Transient
private List<PlayQueueItem> streams; private var eventBroadcast: BehaviorSubject<PlayQueueEvent>? = null
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast; /**
private transient Flowable<PlayQueueEvent> broadcastReceiver; * Returns the play queue's update broadcast.
private transient boolean disposed = false; * May be null if the play queue message bus is not initialized.
*
* @return the play queue's update broadcast
*/
@Transient
var broadcastReceiver: Flowable<PlayQueueEvent>? = null
private set
PlayQueue(final int index, final List<PlayQueueItem> startWith) { @Transient
streams = new ArrayList<>(startWith); var isDisposed: Boolean = false
private set
if (streams.size() > index) { init {
history.add(streams.get(index)); if (streams.size > index) {
history.add(streams[index])
} }
queueIndex = new AtomicInteger(index);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@@ -66,95 +65,89 @@ public abstract class PlayQueue implements Serializable {
/** /**
* Initializes the play queue message buses. * Initializes the play queue message buses.
* <p> *
* Also starts a self reporter for logging if debug mode is enabled. * Also starts a self reporter for logging if debug mode is enabled.
* </p>
*/ */
public void init() { fun init() {
eventBroadcast = BehaviorSubject.create(); eventBroadcast = BehaviorSubject.create()
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) broadcastReceiver =
eventBroadcast!!
.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.startWithItem(new InitEvent()); .startWithItem(InitEvent())
} }
/** /**
* Dispose the play queue by stopping all message buses. * Dispose the play queue by stopping all message buses.
*/ */
public void dispose() { open fun dispose() {
if (eventBroadcast != null) { eventBroadcast?.onComplete()
eventBroadcast.onComplete(); eventBroadcast = null
} broadcastReceiver = null
this.isDisposed = true
eventBroadcast = null;
broadcastReceiver = null;
disposed = true;
} }
/** /**
* Checks if the queue is complete. * Checks if the queue is complete.
* <p> *
* A queue is complete if it has loaded all items in an external playlist * A queue is complete if it has loaded all items in an external playlist
* single stream or local queues are always complete. * single stream or local queues are always complete.
* </p>
* *
* @return whether the queue is complete * @return whether the queue is complete
*/ */
public abstract boolean isComplete(); abstract val isComplete: Boolean
/** /**
* Load partial queue in the background, does nothing if the queue is complete. * Load partial queue in the background, does nothing if the queue is complete.
*/ */
public abstract void fetch(); abstract fun fetch()
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Readonly ops // Readonly ops
////////////////////////////////////////////////////////////////////////// */ ////////////////////////////////////////////////////////////////////////// */
@set:Synchronized
var index: Int = 0
/** /**
* @return the current index that should be played * @return the current index that should be played
*/ */
public int getIndex() { get() = queueIndex.get()
return queueIndex.get();
}
/** /**
* Changes the current playing index to a new index. * Changes the current playing index to a new index.
* <p> *
* This method is guarded using in a circular manner for index exceeding the play queue size. * This method is guarded using in a circular manner for index exceeding the play queue size.
* </p> *
* <p> * Will emit a [SelectEvent] if the index is not the current playing index.
* Will emit a {@link SelectEvent} if the index is not the current playing index.
* </p>
* *
* @param index the index to be set * @param index the index to be set
*/ */
public synchronized void setIndex(final int index) { set(index) {
final int oldIndex = getIndex(); val oldIndex = field
final int newIndex; val newIndex: Int
if (index < 0) { if (index < 0) {
newIndex = 0; newIndex = 0
} else if (index < streams.size()) { } else if (index < streams.size) {
// Regular assignment for index in bounds // Regular assignment for index in bounds
newIndex = index; newIndex = index
} else if (streams.isEmpty()) { } else if (streams.isEmpty()) {
// Out of bounds from here on // Out of bounds from here on
// Need to check if stream is empty to prevent arithmetic error and negative index // Need to check if stream is empty to prevent arithmetic error and negative index
newIndex = 0; newIndex = 0
} else if (isComplete()) { } else if (this.isComplete) {
// Circular indexing // Circular indexing
newIndex = index % streams.size(); newIndex = index % streams.size
} else { } else {
// Index of last element // Index of last element
newIndex = streams.size() - 1; newIndex = streams.size - 1
} }
queueIndex.set(newIndex); queueIndex.set(newIndex)
if (oldIndex != newIndex) { if (oldIndex != newIndex) {
history.add(streams.get(newIndex)); history.add(streams[newIndex])
} }
/* /*
@@ -162,28 +155,19 @@ public abstract class PlayQueue implements Serializable {
different from the old one but this is emitted regardless? Not sure what this what it does different from the old one but this is emitted regardless? Not sure what this what it does
exactly so I won't touch it exactly so I won't touch it
*/ */
broadcast(new SelectEvent(oldIndex, newIndex)); broadcast(SelectEvent(oldIndex, newIndex))
} }
/** /**
* @return the current item that should be played, or null if the queue is empty * @return the current item that should be played, or null if the queue is empty
*/ */
@Nullable val item get() = getItem(this.index)
public PlayQueueItem getItem() {
return getItem(getIndex());
}
/** /**
* @param index the index of the item to return * @param index the index of the item to return
* @return the item at the given index, or null if the index is out of bounds * @return the item at the given index, or null if the index is out of bounds
*/ */
@Nullable fun getItem(index: Int) = streams.getOrNull(index)
public PlayQueueItem getItem(final int index) {
if (index < 0 || index >= streams.size()) {
return null;
}
return streams.get(index);
}
/** /**
* Returns the index of the given item using referential equality. * Returns the index of the given item using referential equality.
@@ -192,303 +176,280 @@ public abstract class PlayQueue implements Serializable {
* @param item the item to find the index of * @param item the item to find the index of
* @return the index of the given item * @return the index of the given item
*/ */
public int indexOf(@NonNull final PlayQueueItem item) { fun indexOf(item: PlayQueueItem): Int = streams.indexOf(item)
return streams.indexOf(item);
}
/** /**
* @return the current size of play queue. * @return the current size of play queue.
*/ */
public int size() { fun size(): Int = streams.size
return streams.size();
}
/** /**
* Checks if the play queue is empty. * Checks if the play queue is empty.
* *
* @return whether the play queue is empty * @return whether the play queue is empty
*/ */
public boolean isEmpty() { val isEmpty: Boolean
return streams.isEmpty(); get() = streams.isEmpty()
}
/** /**
* Determines if the current play queue is shuffled. * Determines if the current play queue is shuffled.
* *
* @return whether the play queue is shuffled * @return whether the play queue is shuffled
*/ */
public boolean isShuffled() { val isShuffled: Boolean
return backup != null; get() = backup.isNotEmpty()
}
/** /**
* @return an immutable view of the play queue * @return an immutable view of the play queue
*/ */
@NonNull fun getStreams(): List<PlayQueueItem> = Collections.unmodifiableList(streams)
public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Write ops // Write ops
////////////////////////////////////////////////////////////////////////// */ ////////////////////////////////////////////////////////////////////////// */
/**
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
*
* @return the play queue's update broadcast
*/
@Nullable
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver;
}
/** /**
* Changes the current playing index by an offset amount. * Changes the current playing index by an offset amount.
* <p> *
* Will emit a {@link SelectEvent} if offset is non-zero. * Will emit a [SelectEvent] if offset is non-zero.
* </p>
* *
* @param offset the offset relative to the current index * @param offset the offset relative to the current index
*/ */
public synchronized void offsetIndex(final int offset) { @Synchronized
setIndex(getIndex() + offset); fun offsetIndex(offset: Int) {
this.index += offset
} }
/** /**
* Notifies that a change has occurred. * Notifies that a change has occurred.
*/ */
public synchronized void notifyChange() { @Synchronized
broadcast(new AppendEvent(0)); fun notifyChange() {
broadcast(AppendEvent(0))
} }
/** /**
* Appends the given {@link PlayQueueItem}s to the current play queue. * Appends the given [PlayQueueItem]s to the current play queue.
* <p> *
* If the play queue is shuffled, then append the items to the backup queue as is and * If the play queue is shuffled, then append the items to the backup queue as is and
* append the shuffle items to the play queue. * append the shuffle items to the play queue.
* </p>
* <p>
* Will emit a {@link AppendEvent} on any given context.
* </p>
* *
* @param items {@link PlayQueueItem}s to append * Will emit a [AppendEvent] on any given context.
*
* @param items [PlayQueueItem]s to append
*/ */
public synchronized void append(@NonNull final List<PlayQueueItem> items) { @Synchronized
final List<PlayQueueItem> itemList = new ArrayList<>(items); fun append(items: List<PlayQueueItem>) {
val itemList = items.toMutableList()
if (isShuffled()) { if (this.isShuffled) {
backup.addAll(itemList); backup.addAll(itemList)
Collections.shuffle(itemList); itemList.shuffle()
} }
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued() if (!streams.isEmpty() && streams.last().isAutoQueued && !itemList[0].isAutoQueued) {
&& !itemList.get(0).isAutoQueued()) { streams.removeAt(streams.lastIndex)
streams.remove(streams.size() - 1);
} }
streams.addAll(itemList); streams.addAll(itemList)
broadcast(new AppendEvent(itemList.size())); broadcast(AppendEvent(itemList.size))
} }
/** /**
* Removes the item at the given index from the play queue. * Removes the item at the given index from the play queue.
* <p> *
* The current playing index will decrement if it is greater than the index being removed. * The current playing index will decrement if it is greater than the index being removed.
* On cases where the current playing index exceeds the playlist range, it is set to 0. * On cases where the current playing index exceeds the playlist range, it is set to 0.
* </p> *
* <p> * Will emit a [RemoveEvent] if the index is within the play queue index range.
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
* </p>
* *
* @param index the index of the item to remove * @param index the index of the item to remove
*/ */
public synchronized void remove(final int index) { @Synchronized
if (index >= streams.size() || index < 0) { fun remove(index: Int) {
return; if (index >= streams.size || index < 0) {
return
} }
removeInternal(index); removeInternal(index)
broadcast(new RemoveEvent(index, getIndex())); broadcast(RemoveEvent(index, this.index))
} }
/** /**
* Report an exception for the item at the current index in order and skip to the next one * Report an exception for the item at the current index in order and skip to the next one
* <p> *
* This is done as a separate event as the underlying manager may have * This is done as a separate event as the underlying manager may have
* different implementation regarding exceptions. * different implementation regarding exceptions.
* </p>
*/ */
public synchronized void error() { @Synchronized
final int oldIndex = getIndex(); fun error() {
queueIndex.incrementAndGet(); val oldIndex = this.index
if (streams.size() > queueIndex.get()) { queueIndex.incrementAndGet()
history.add(streams.get(queueIndex.get())); if (streams.size > queueIndex.get()) {
history.add(streams[queueIndex.get()])
} }
broadcast(new ErrorEvent(oldIndex, getIndex())); broadcast(ErrorEvent(oldIndex, this.index))
} }
private synchronized void removeInternal(final int removeIndex) { @Synchronized
final int currentIndex = queueIndex.get(); private fun removeInternal(removeIndex: Int) {
final int size = size(); val currentIndex = queueIndex.get()
val size = size()
if (currentIndex > removeIndex) { if (currentIndex > removeIndex) {
queueIndex.decrementAndGet(); queueIndex.decrementAndGet()
} else if (currentIndex >= size) { } else if (currentIndex >= size) {
queueIndex.set(currentIndex % (size - 1)); queueIndex.set(currentIndex % (size - 1))
} else if (currentIndex == removeIndex && currentIndex == size - 1) { } else if (currentIndex == removeIndex && currentIndex == size - 1) {
queueIndex.set(0); queueIndex.set(0)
} }
if (backup != null) { backup.remove(getItem(removeIndex)!!)
backup.remove(getItem(removeIndex));
}
history.remove(streams.remove(removeIndex)); history.remove(streams.removeAt(removeIndex))
if (streams.size() > queueIndex.get()) { if (streams.size > queueIndex.get()) {
history.add(streams.get(queueIndex.get())); history.add(streams[queueIndex.get()])
} }
} }
/** /**
* Moves a queue item at the source index to the target index. * Moves a queue item at the source index to the target index.
* <p> *
* If the item being moved is the currently playing, then the current playing index is set * If the item being moved is the currently playing, then the current playing index is set
* to that of the target. * to that of the target.
* If the moved item is not the currently playing and moves to an index <b>AFTER</b> the * If the moved item is not the currently playing and moves to an index **AFTER** the
* current playing index, then the current playing index is decremented. * current playing index, then the current playing index is decremented.
* Vice versa if the an item after the currently playing is moved <b>BEFORE</b>. * Vice versa if the an item after the currently playing is moved **BEFORE**.
* </p>
* *
* @param source the original index of the item * @param source the original index of the item
* @param target the new index of the item * @param target the new index of the item
*/ */
public synchronized void move(final int source, final int target) { @Synchronized
fun move(
source: Int,
target: Int,
) {
if (source < 0 || target < 0) { if (source < 0 || target < 0) {
return; return
} }
if (source >= streams.size() || target >= streams.size()) { if (source >= streams.size || target >= streams.size) {
return; return
} }
final int current = getIndex(); val current = this.index
if (source == current) { if (source == current) {
queueIndex.set(target); queueIndex.set(target)
} else if (source < current && target >= current) { } else if (source < current && target >= current) {
queueIndex.decrementAndGet(); queueIndex.decrementAndGet()
} else if (source > current && target <= current) { } else if (source > current && target <= current) {
queueIndex.incrementAndGet(); queueIndex.incrementAndGet()
} }
final PlayQueueItem playQueueItem = streams.remove(source); val playQueueItem = streams.removeAt(source)
playQueueItem.setAutoQueued(false); playQueueItem.isAutoQueued = false
streams.add(target, playQueueItem); streams.add(target, playQueueItem)
broadcast(new MoveEvent(source, target)); broadcast(MoveEvent(source, target))
} }
/** /**
* Sets the recovery record of the item at the index. * Sets the recovery record of the item at the index.
* <p> *
* Broadcasts a recovery event. * Broadcasts a recovery event.
* </p>
* *
* @param index index of the item * @param index index of the item
* @param position the recovery position * @param position the recovery position
*/ */
public synchronized void setRecovery(final int index, final long position) { @Synchronized
if (index < 0 || index >= streams.size()) { fun setRecovery(
return; index: Int,
position: Long,
) {
streams.getOrNull(index)?.let {
it.recoveryPosition = position
broadcast(RecoveryEvent(index, position))
} }
streams.get(index).setRecoveryPosition(position);
broadcast(new RecoveryEvent(index, position));
} }
/** /**
* Revoke the recovery record of the item at the index. * Revoke the recovery record of the item at the index.
* <p> *
* Broadcasts a recovery event. * Broadcasts a recovery event.
* </p>
* *
* @param index index of the item * @param index index of the item
*/ */
public synchronized void unsetRecovery(final int index) { @Synchronized
setRecovery(index, PlayQueueItem.RECOVERY_UNSET); fun unsetRecovery(index: Int) {
setRecovery(index, Long.Companion.MIN_VALUE)
} }
/** /**
* Shuffles the current play queue * Shuffles the current play queue
* <p> *
* This method first backs up the existing play queue and item being played. Then a newly * This method first backs up the existing play queue and item being played. Then a newly
* shuffled play queue will be generated along with currently playing item placed at the * shuffled play queue will be generated along with currently playing item placed at the
* beginning of the queue. This item will also be added to the history. * beginning of the queue. This item will also be added to the history.
* </p> *
* <p> * Will emit a [ReorderEvent] if shuffled.
* Will emit a {@link ReorderEvent} if shuffled.
* </p>
* *
* @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on * @implNote Does nothing if the queue has a size <= 2 (the currently playing video must stay on
* top, so shuffling a size-2 list does nothing) * top, so shuffling a size-2 list does nothing)
*/ */
public synchronized void shuffle() { @Synchronized
fun shuffle() {
// Create a backup if it doesn't already exist // Create a backup if it doesn't already exist
// Note: The backup-list has to be created at all cost (even when size <= 2). // Note: The backup-list has to be created at all cost (even when size <= 2).
// Otherwise it's not possible to enter shuffle-mode! // Otherwise it's not possible to enter shuffle-mode!
if (backup == null) { if (backup.isEmpty()) {
backup = new ArrayList<>(streams); backup = streams.toMutableList()
} }
// Can't shuffle a list that's empty or only has one element // Can't shuffle a list that's empty or only has one element
if (size() <= 2) { if (size() <= 2) {
return; return
} }
final int originalIndex = getIndex(); val originalIndex = this.index
final PlayQueueItem currentItem = getItem(); val currentItem = this.item
Collections.shuffle(streams); streams.shuffle()
// Move currentItem to the head of the queue // Move currentItem to the head of the queue
streams.remove(currentItem); streams.remove(currentItem!!)
streams.add(0, currentItem); streams.add(0, currentItem)
queueIndex.set(0); queueIndex.set(0)
history.add(currentItem); history.add(currentItem)
broadcast(new ReorderEvent(originalIndex, 0)); broadcast(ReorderEvent(originalIndex, 0))
} }
/** /**
* Unshuffles the current play queue if a backup play queue exists. * Unshuffles the current play queue if a backup play queue exists.
* <p> *
* This method undoes shuffling and index will be set to the previously playing item if found, * This method undoes shuffling and index will be set to the previously playing item if found,
* otherwise, the index will reset to 0. * otherwise, the index will reset to 0.
* </p> *
* <p> * Will emit a [ReorderEvent] if a backup exists.
* Will emit a {@link ReorderEvent} if a backup exists.
* </p>
*/ */
public synchronized void unshuffle() { @Synchronized
if (backup == null) { fun unshuffle() {
return; if (backup.isEmpty()) {
return
} }
final int originIndex = getIndex(); val originIndex = this.index
final PlayQueueItem current = getItem(); val current = this.item
streams = backup; streams = backup
backup = null; backup = mutableListOf()
final int newIndex = streams.indexOf(current); val newIndex = streams.indexOf(current!!)
if (newIndex != -1) { if (newIndex != -1) {
queueIndex.set(newIndex); queueIndex.set(newIndex)
} else { } else {
queueIndex.set(0); queueIndex.set(0)
} }
if (streams.size() > queueIndex.get()) { if (streams.size > queueIndex.get()) {
history.add(streams.get(queueIndex.get())); history.add(streams[queueIndex.get()])
} }
broadcast(new ReorderEvent(originIndex, queueIndex.get())); broadcast(ReorderEvent(originIndex, queueIndex.get()))
} }
/** /**
@@ -498,18 +459,19 @@ public abstract class PlayQueue implements Serializable {
* starts playing the last item from history if it exists * starts playing the last item from history if it exists
* *
* @return true if history is not empty and the item can be played * @return true if history is not empty and the item can be played
* */ */
public synchronized boolean previous() { @Synchronized
if (history.size() <= 1) { fun previous(): Boolean {
return false; if (history.size <= 1) {
return false
} }
history.remove(history.size() - 1); history.removeAt(history.size - 1)
final PlayQueueItem last = history.remove(history.size() - 1); val last = history.removeAt(history.size - 1)
setIndex(indexOf(last)); this.index = indexOf(last)
return true; return true
} }
/* /*
@@ -518,31 +480,18 @@ public abstract class PlayQueue implements Serializable {
* This method also gives a chance to track history of items in a queue in * This method also gives a chance to track history of items in a queue in
* VideoDetailFragment without duplicating items from two identical queues * VideoDetailFragment without duplicating items from two identical queues
*/ */
@Override override fun equals(o: Any?): Boolean = o is PlayQueue && streams == o.streams
public boolean equals(final Object o) {
return o instanceof PlayQueue playQueue && streams.equals(playQueue.streams); override fun hashCode(): Int = streams.hashCode()
fun equalStreamsAndIndex(other: PlayQueue?): Boolean {
return equals(other) && other!!.index == this.index // NOSONAR: other is not null
} }
@Override
public int hashCode() {
return streams.hashCode();
}
public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) {
return equals(other) && other.getIndex() == getIndex(); //NOSONAR: other is not null
}
public boolean isDisposed() {
return disposed;
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Rx Broadcast // Rx Broadcast
////////////////////////////////////////////////////////////////////////// */ ////////////////////////////////////////////////////////////////////////// */
private fun broadcast(event: PlayQueueEvent) {
private void broadcast(@NonNull final PlayQueueEvent event) { eventBroadcast?.onNext(event)
if (eventBroadcast != null) {
eventBroadcast.onNext(event);
} }
} }
}

View File

@@ -1,155 +1,71 @@
package org.schabi.newpipe.player.playqueue; package org.schabi.newpipe.player.playqueue
import androidx.annotation.NonNull; import io.reactivex.rxjava3.core.Single
import androidx.annotation.Nullable; import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.util.ExtractorHelper
import java.io.Serializable
import java.util.Objects
import org.schabi.newpipe.extractor.Image; class PlayQueueItem private constructor(
import org.schabi.newpipe.extractor.stream.StreamInfo; val title: String,
import org.schabi.newpipe.extractor.stream.StreamInfoItem; val url: String,
import org.schabi.newpipe.extractor.stream.StreamType; val serviceId: Int,
import org.schabi.newpipe.util.ExtractorHelper; val duration: Long,
val thumbnails: List<Image>,
import java.io.Serializable; val uploader: String,
import java.util.List; val uploaderUrl: String?,
import java.util.Objects; val streamType: StreamType,
) : Serializable {
import io.reactivex.rxjava3.core.Single; //
import io.reactivex.rxjava3.schedulers.Schedulers; // ////////////////////////////////////////////////////////////////////// */
public class PlayQueueItem implements Serializable {
public static final long RECOVERY_UNSET = Long.MIN_VALUE;
private static final String EMPTY_STRING = "";
@NonNull
private final String title;
@NonNull
private final String url;
private final int serviceId;
private final long duration;
@NonNull
private final List<Image> thumbnails;
@NonNull
private final String uploader;
private final String uploaderUrl;
@NonNull
private final StreamType streamType;
private boolean isAutoQueued;
private long recoveryPosition;
private Throwable error;
PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnails(), info.getUploaderName(),
info.getUploaderUrl(), info.getStreamType());
if (info.getStartPosition() > 0) {
setRecoveryPosition(info.getStartPosition() * 1000);
}
}
PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
item.getThumbnails(), item.getUploaderName(),
item.getUploaderUrl(), item.getStreamType());
}
@SuppressWarnings("ParameterNumber")
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
final int serviceId, final long duration,
final List<Image> thumbnails, @Nullable final String uploader,
final String uploaderUrl, @NonNull final StreamType streamType) {
this.title = name != null ? name : EMPTY_STRING;
this.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId;
this.duration = duration;
this.thumbnails = thumbnails;
this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.uploaderUrl = uploaderUrl;
this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET;
}
@NonNull
public String getTitle() {
return title;
}
@NonNull
public String getUrl() {
return url;
}
public int getServiceId() {
return serviceId;
}
public long getDuration() {
return duration;
}
@NonNull
public List<Image> getThumbnails() {
return thumbnails;
}
@NonNull
public String getUploader() {
return uploader;
}
public String getUploaderUrl() {
return uploaderUrl;
}
@NonNull
public StreamType getStreamType() {
return streamType;
}
public long getRecoveryPosition() {
return recoveryPosition;
}
/*package-private*/ void setRecoveryPosition(final long recoveryPosition) {
this.recoveryPosition = recoveryPosition;
}
@Nullable
public Throwable getError() {
return error;
}
@NonNull
public Single<StreamInfo> getStream() {
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
.subscribeOn(Schedulers.io())
.doOnError(throwable -> error = throwable);
}
public boolean isAutoQueued() {
return isAutoQueued;
}
////////////////////////////////////////////////////////////////////////////
// Item States, keep external access out // Item States, keep external access out
//////////////////////////////////////////////////////////////////////////// //
// ////////////////////////////////////////////////////////////////////// */
var isAutoQueued: Boolean = false
public void setAutoQueued(final boolean autoQueued) { // package-private
isAutoQueued = autoQueued; var recoveryPosition = Long.Companion.MIN_VALUE
var error: Throwable? = null
private set
constructor(info: StreamInfo) : this(
info.name.orEmpty(),
info.url.orEmpty(),
info.serviceId,
info.duration,
info.thumbnails,
info.uploaderName.orEmpty(),
info.uploaderUrl,
info.streamType,
) {
if (info.startPosition > 0) {
this.recoveryPosition = info.startPosition * 1000
}
} }
@Override constructor(item: StreamInfoItem) : this(
public boolean equals(final Object o) { item.name.orEmpty(),
return o instanceof PlayQueueItem item item.url.orEmpty(),
&& serviceId == item.serviceId item.serviceId,
&& url.equals(item.url); item.duration,
} item.thumbnails,
item.uploaderName.orEmpty(),
item.uploaderUrl,
item.streamType,
)
@Override val stream: Single<StreamInfo>
public int hashCode() { get() =
return Objects.hash(url, serviceId); ExtractorHelper
} .getStreamInfo(serviceId, url, false)
.subscribeOn(Schedulers.io())
.doOnError { throwable -> error = throwable }
override fun equals(o: Any?) = o is PlayQueueItem && serviceId == o.serviceId && url == o.url
override fun hashCode() = Objects.hash(url, serviceId)
} }