1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-07-30 15:53:12 +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
&& playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null
&& 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
// and we should retry in this case
if (simpleExoPlayer.getPlaybackState()
@ -425,7 +425,7 @@ public final class Player implements PlaybackListener, Listener {
&& !samePlayQueue
&& !newQueue.isEmpty()
&& newQueue.getItem() != null
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
&& newQueue.getItem().getRecoveryPosition() == Long.MIN_VALUE) {
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
.observeOn(AndroidSchedulers.mainThread())
// 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
if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
if (item.getRecoveryPosition() != Long.MIN_VALUE) {
simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition());
playQueue.unsetRecovery(playQueueIndex);
} else {

View File

@ -38,9 +38,9 @@ import io.reactivex.rxjava3.internal.subscriptions.EmptySubscription;
import io.reactivex.rxjava3.schedulers.Schedulers;
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.StreamInfoLoadException;
import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
import static org.schabi.newpipe.util.ServiceHelper.getCacheExpirationMillis;
public class MediaSourceManager {

View File

@ -1,189 +1,173 @@
package org.schabi.newpipe.player.playqueue;
package org.schabi.newpipe.player.playqueue
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
import org.schabi.newpipe.player.playqueue.events.InitEvent;
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent;
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
import org.schabi.newpipe.player.playqueue.events.SelectEvent;
import java.io.Serializable;
import java.util.ArrayList;
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;
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
import org.schabi.newpipe.player.playqueue.events.AppendEvent
import org.schabi.newpipe.player.playqueue.events.ErrorEvent
import org.schabi.newpipe.player.playqueue.events.InitEvent
import org.schabi.newpipe.player.playqueue.events.MoveEvent
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent
import org.schabi.newpipe.player.playqueue.events.RemoveEvent
import org.schabi.newpipe.player.playqueue.events.ReorderEvent
import org.schabi.newpipe.player.playqueue.events.SelectEvent
import java.io.Serializable
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
/**
* PlayQueue is responsible for keeping track of a list of streams and the index of
* the stream that should be currently playing.
* <p>
*
* 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.
* </p>
* <p>
*
* This class can be serialized for passing intents, but in order to start the
* message bus, it must be initialized.
* </p>
*/
public abstract class PlayQueue implements Serializable {
public static final boolean DEBUG = MainActivity.DEBUG;
@NonNull
private final AtomicInteger queueIndex;
private final List<PlayQueueItem> history = new ArrayList<>();
abstract class PlayQueue internal constructor(
index: Int,
startWith: List<PlayQueueItem>,
) : Serializable {
private val queueIndex = AtomicInteger(index)
private val history = mutableListOf<PlayQueueItem>()
private var backup = mutableListOf<PlayQueueItem>()
private var streams = startWith.toMutableList()
private List<PlayQueueItem> backup;
private List<PlayQueueItem> streams;
@Transient
private var eventBroadcast: BehaviorSubject<PlayQueueEvent>? = null
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
private transient boolean disposed = false;
/**
* 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
*/
@Transient
var broadcastReceiver: Flowable<PlayQueueEvent>? = null
private set
PlayQueue(final int index, final List<PlayQueueItem> startWith) {
streams = new ArrayList<>(startWith);
@Transient
var isDisposed: Boolean = false
private set
if (streams.size() > index) {
history.add(streams.get(index));
init {
if (streams.size > index) {
history.add(streams[index])
}
queueIndex = new AtomicInteger(index);
}
/*//////////////////////////////////////////////////////////////////////////
// Playlist actions
//////////////////////////////////////////////////////////////////////////*/
////////////////////////////////////////////////////////////////////////// */
/**
* Initializes the play queue message buses.
* <p>
*
* Also starts a self reporter for logging if debug mode is enabled.
* </p>
*/
public void init() {
eventBroadcast = BehaviorSubject.create();
fun init() {
eventBroadcast = BehaviorSubject.create()
broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER)
broadcastReceiver =
eventBroadcast!!
.toFlowable(BackpressureStrategy.BUFFER)
.observeOn(AndroidSchedulers.mainThread())
.startWithItem(new InitEvent());
.startWithItem(InitEvent())
}
/**
* Dispose the play queue by stopping all message buses.
*/
public void dispose() {
if (eventBroadcast != null) {
eventBroadcast.onComplete();
}
eventBroadcast = null;
broadcastReceiver = null;
disposed = true;
open fun dispose() {
eventBroadcast?.onComplete()
eventBroadcast = null
broadcastReceiver = null
this.isDisposed = true
}
/**
* Checks if the queue is complete.
* <p>
*
* A queue is complete if it has loaded all items in an external playlist
* single stream or local queues are always complete.
* </p>
*
* @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.
*/
public abstract void fetch();
abstract fun fetch()
/*//////////////////////////////////////////////////////////////////////////
// Readonly ops
//////////////////////////////////////////////////////////////////////////*/
/**
* @return the current index that should be played
*/
public int getIndex() {
return queueIndex.get();
}
/**
* 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.
* </p>
* <p>
* Will emit a {@link SelectEvent} if the index is not the current playing index.
* </p>
*
* @param index the index to be set
*/
public synchronized void setIndex(final int index) {
final int oldIndex = getIndex();
final int newIndex;
if (index < 0) {
newIndex = 0;
} else if (index < streams.size()) {
// Regular assignment for index in bounds
newIndex = index;
} else if (streams.isEmpty()) {
// Out of bounds from here on
// Need to check if stream is empty to prevent arithmetic error and negative index
newIndex = 0;
} else if (isComplete()) {
// Circular indexing
newIndex = index % streams.size();
} else {
// Index of last element
newIndex = streams.size() - 1;
}
queueIndex.set(newIndex);
if (oldIndex != newIndex) {
history.add(streams.get(newIndex));
}
/*
TODO: Documentation states that a SelectEvent will only be emitted if the new index is...
different from the old one but this is emitted regardless? Not sure what this what it does
exactly so I won't touch it
////////////////////////////////////////////////////////////////////////// */
@set:Synchronized
var index: Int = 0
/**
* @return the current index that should be played
*/
broadcast(new SelectEvent(oldIndex, newIndex));
}
get() = queueIndex.get()
/**
* Changes the current playing index to a new index.
*
* This method is guarded using in a circular manner for index exceeding the play queue size.
*
* Will emit a [SelectEvent] if the index is not the current playing index.
*
* @param index the index to be set
*/
set(index) {
val oldIndex = field
val newIndex: Int
if (index < 0) {
newIndex = 0
} else if (index < streams.size) {
// Regular assignment for index in bounds
newIndex = index
} else if (streams.isEmpty()) {
// Out of bounds from here on
// Need to check if stream is empty to prevent arithmetic error and negative index
newIndex = 0
} else if (this.isComplete) {
// Circular indexing
newIndex = index % streams.size
} else {
// Index of last element
newIndex = streams.size - 1
}
queueIndex.set(newIndex)
if (oldIndex != newIndex) {
history.add(streams[newIndex])
}
/*
TODO: Documentation states that a SelectEvent will only be emitted if the new index is...
different from the old one but this is emitted regardless? Not sure what this what it does
exactly so I won't touch it
*/
broadcast(SelectEvent(oldIndex, newIndex))
}
/**
* @return the current item that should be played, or null if the queue is empty
*/
@Nullable
public PlayQueueItem getItem() {
return getItem(getIndex());
}
val item get() = getItem(this.index)
/**
* @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
*/
@Nullable
public PlayQueueItem getItem(final int index) {
if (index < 0 || index >= streams.size()) {
return null;
}
return streams.get(index);
}
fun getItem(index: Int) = streams.getOrNull(index)
/**
* 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
* @return the index of the given item
*/
public int indexOf(@NonNull final PlayQueueItem item) {
return streams.indexOf(item);
}
fun indexOf(item: PlayQueueItem): Int = streams.indexOf(item)
/**
* @return the current size of play queue.
*/
public int size() {
return streams.size();
}
fun size(): Int = streams.size
/**
* Checks if the play queue is empty.
*
* @return whether the play queue is empty
*/
public boolean isEmpty() {
return streams.isEmpty();
}
val isEmpty: Boolean
get() = streams.isEmpty()
/**
* Determines if the current play queue is shuffled.
*
* @return whether the play queue is shuffled
*/
public boolean isShuffled() {
return backup != null;
}
val isShuffled: Boolean
get() = backup.isNotEmpty()
/**
* @return an immutable view of the play queue
*/
@NonNull
public List<PlayQueueItem> getStreams() {
return Collections.unmodifiableList(streams);
}
fun getStreams(): List<PlayQueueItem> = Collections.unmodifiableList(streams)
/*//////////////////////////////////////////////////////////////////////////
// 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.
* <p>
* Will emit a {@link SelectEvent} if offset is non-zero.
* </p>
*
* Will emit a [SelectEvent] if offset is non-zero.
*
* @param offset the offset relative to the current index
*/
public synchronized void offsetIndex(final int offset) {
setIndex(getIndex() + offset);
@Synchronized
fun offsetIndex(offset: Int) {
this.index += offset
}
/**
* Notifies that a change has occurred.
*/
public synchronized void notifyChange() {
broadcast(new AppendEvent(0));
@Synchronized
fun notifyChange() {
broadcast(AppendEvent(0))
}
/**
* Appends the given {@link PlayQueueItem}s to the current play queue.
* <p>
* Appends the given [PlayQueueItem]s to the current play queue.
*
* 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.
* </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) {
final List<PlayQueueItem> itemList = new ArrayList<>(items);
@Synchronized
fun append(items: List<PlayQueueItem>) {
val itemList = items.toMutableList()
if (isShuffled()) {
backup.addAll(itemList);
Collections.shuffle(itemList);
if (this.isShuffled) {
backup.addAll(itemList)
itemList.shuffle()
}
if (!streams.isEmpty() && streams.get(streams.size() - 1).isAutoQueued()
&& !itemList.get(0).isAutoQueued()) {
streams.remove(streams.size() - 1);
if (!streams.isEmpty() && streams.last().isAutoQueued && !itemList[0].isAutoQueued) {
streams.removeAt(streams.lastIndex)
}
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.
* <p>
*
* 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.
* </p>
* <p>
* Will emit a {@link RemoveEvent} if the index is within the play queue index range.
* </p>
*
* Will emit a [RemoveEvent] if the index is within the play queue index range.
*
* @param index the index of the item to remove
*/
public synchronized void remove(final int index) {
if (index >= streams.size() || index < 0) {
return;
@Synchronized
fun remove(index: Int) {
if (index >= streams.size || index < 0) {
return
}
removeInternal(index);
broadcast(new RemoveEvent(index, getIndex()));
removeInternal(index)
broadcast(RemoveEvent(index, this.index))
}
/**
* 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
* different implementation regarding exceptions.
* </p>
*/
public synchronized void error() {
final int oldIndex = getIndex();
queueIndex.incrementAndGet();
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
@Synchronized
fun error() {
val oldIndex = this.index
queueIndex.incrementAndGet()
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) {
final int currentIndex = queueIndex.get();
final int size = size();
@Synchronized
private fun removeInternal(removeIndex: Int) {
val currentIndex = queueIndex.get()
val size = size()
if (currentIndex > removeIndex) {
queueIndex.decrementAndGet();
queueIndex.decrementAndGet()
} else if (currentIndex >= size) {
queueIndex.set(currentIndex % (size - 1));
queueIndex.set(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));
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
history.remove(streams.removeAt(removeIndex))
if (streams.size > queueIndex.get()) {
history.add(streams[queueIndex.get()])
}
}
/**
* 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
* 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.
* Vice versa if the an item after the currently playing is moved <b>BEFORE</b>.
* </p>
* Vice versa if the an item after the currently playing is moved **BEFORE**.
*
* @param source the original 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) {
return;
return
}
if (source >= streams.size() || target >= streams.size()) {
return;
if (source >= streams.size || target >= streams.size) {
return
}
final int current = getIndex();
val current = this.index
if (source == current) {
queueIndex.set(target);
queueIndex.set(target)
} else if (source < current && target >= current) {
queueIndex.decrementAndGet();
queueIndex.decrementAndGet()
} else if (source > current && target <= current) {
queueIndex.incrementAndGet();
queueIndex.incrementAndGet()
}
final PlayQueueItem playQueueItem = streams.remove(source);
playQueueItem.setAutoQueued(false);
streams.add(target, playQueueItem);
broadcast(new MoveEvent(source, target));
val playQueueItem = streams.removeAt(source)
playQueueItem.isAutoQueued = false
streams.add(target, playQueueItem)
broadcast(MoveEvent(source, target))
}
/**
* Sets the recovery record of the item at the index.
* <p>
*
* Broadcasts a recovery event.
* </p>
*
* @param index index of the item
* @param position the recovery position
*/
public synchronized void setRecovery(final int index, final long position) {
if (index < 0 || index >= streams.size()) {
return;
@Synchronized
fun setRecovery(
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.
* <p>
*
* Broadcasts a recovery event.
* </p>
*
* @param index index of the item
*/
public synchronized void unsetRecovery(final int index) {
setRecovery(index, PlayQueueItem.RECOVERY_UNSET);
@Synchronized
fun unsetRecovery(index: Int) {
setRecovery(index, Long.Companion.MIN_VALUE)
}
/**
* Shuffles the current play queue
* <p>
*
* 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
* beginning of the queue. This item will also be added to the history.
* </p>
* <p>
* Will emit a {@link ReorderEvent} if shuffled.
* </p>
*
* Will emit a [ReorderEvent] if shuffled.
*
* @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)
*/
public synchronized void shuffle() {
@Synchronized
fun shuffle() {
// Create a backup if it doesn't already exist
// Note: The backup-list has to be created at all cost (even when size <= 2).
// Otherwise it's not possible to enter shuffle-mode!
if (backup == null) {
backup = new ArrayList<>(streams);
if (backup.isEmpty()) {
backup = streams.toMutableList()
}
// Can't shuffle a list that's empty or only has one element
if (size() <= 2) {
return;
return
}
final int originalIndex = getIndex();
final PlayQueueItem currentItem = getItem();
val originalIndex = this.index
val currentItem = this.item
Collections.shuffle(streams);
streams.shuffle()
// Move currentItem to the head of the queue
streams.remove(currentItem);
streams.add(0, currentItem);
queueIndex.set(0);
streams.remove(currentItem!!)
streams.add(0, currentItem)
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.
* <p>
*
* This method undoes shuffling and index will be set to the previously playing item if found,
* otherwise, the index will reset to 0.
* </p>
* <p>
* Will emit a {@link ReorderEvent} if a backup exists.
* </p>
*
* Will emit a [ReorderEvent] if a backup exists.
*/
public synchronized void unshuffle() {
if (backup == null) {
return;
@Synchronized
fun unshuffle() {
if (backup.isEmpty()) {
return
}
final int originIndex = getIndex();
final PlayQueueItem current = getItem();
val originIndex = this.index
val current = this.item
streams = backup;
backup = null;
streams = backup
backup = mutableListOf()
final int newIndex = streams.indexOf(current);
val newIndex = streams.indexOf(current!!)
if (newIndex != -1) {
queueIndex.set(newIndex);
queueIndex.set(newIndex)
} else {
queueIndex.set(0);
queueIndex.set(0)
}
if (streams.size() > queueIndex.get()) {
history.add(streams.get(queueIndex.get()));
if (streams.size > 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
*
* @return true if history is not empty and the item can be played
* */
public synchronized boolean previous() {
if (history.size() <= 1) {
return false;
*/
@Synchronized
fun previous(): Boolean {
if (history.size <= 1) {
return false
}
history.remove(history.size() - 1);
history.removeAt(history.size - 1)
final PlayQueueItem last = history.remove(history.size() - 1);
setIndex(indexOf(last));
val last = history.removeAt(history.size - 1)
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
* VideoDetailFragment without duplicating items from two identical queues
*/
@Override
public boolean equals(final Object o) {
return o instanceof PlayQueue playQueue && streams.equals(playQueue.streams);
override fun equals(o: Any?): Boolean = o is PlayQueue && streams == o.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
//////////////////////////////////////////////////////////////////////////*/
private void broadcast(@NonNull final PlayQueueEvent event) {
if (eventBroadcast != null) {
eventBroadcast.onNext(event);
}
// Rx Broadcast
////////////////////////////////////////////////////////////////////////// */
private fun broadcast(event: PlayQueueEvent) {
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 androidx.annotation.Nullable;
import io.reactivex.rxjava3.core.Single
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;
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;
class PlayQueueItem private constructor(
val title: String,
val url: String,
val serviceId: Int,
val duration: Long,
val thumbnails: List<Image>,
val uploader: String,
val uploaderUrl: String?,
val streamType: StreamType,
) : Serializable {
//
// ////////////////////////////////////////////////////////////////////// */
// Item States, keep external access out
//
// ////////////////////////////////////////////////////////////////////// */
var isAutoQueued: Boolean = false
import java.io.Serializable;
import java.util.List;
import java.util.Objects;
// package-private
var recoveryPosition = Long.Companion.MIN_VALUE
var error: Throwable? = null
private set
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);
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
}
}
PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
item.getThumbnails(), item.getUploaderName(),
item.getUploaderUrl(), item.getStreamType());
}
constructor(item: StreamInfoItem) : this(
item.name.orEmpty(),
item.url.orEmpty(),
item.serviceId,
item.duration,
item.thumbnails,
item.uploaderName.orEmpty(),
item.uploaderUrl,
item.streamType,
)
@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)
val stream: Single<StreamInfo>
get() =
ExtractorHelper
.getStreamInfo(serviceId, url, false)
.subscribeOn(Schedulers.io())
.doOnError(throwable -> error = throwable);
}
.doOnError { throwable -> error = throwable }
public boolean isAutoQueued() {
return isAutoQueued;
}
override fun equals(o: Any?) = o is PlayQueueItem && serviceId == o.serviceId && url == o.url
////////////////////////////////////////////////////////////////////////////
// Item States, keep external access out
////////////////////////////////////////////////////////////////////////////
public void setAutoQueued(final boolean autoQueued) {
isAutoQueued = autoQueued;
}
@Override
public boolean equals(final Object o) {
return o instanceof PlayQueueItem item
&& serviceId == item.serviceId
&& url.equals(item.url);
}
@Override
public int hashCode() {
return Objects.hash(url, serviceId);
}
override fun hashCode() = Objects.hash(url, serviceId)
}