mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-17 05:22:40 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06478f3e36 | |||
| 40f20002b2 | |||
| 442272f517 | |||
| 88dae8e9c4 | |||
| 1bbfa7d39e | |||
| edc2b3d295 | |||
| 0006da7385 | |||
| b5ac8b3ec6 | |||
| 78f5169880 | |||
| 3361b77aec | |||
| 8b7c9df286 | |||
| 157d5b4c36 |
+1
-1
@@ -197,7 +197,7 @@ dependencies {
|
||||
implementation 'org.jsoup:jsoup:1.15.3'
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:5.1'
|
||||
implementation 'com.arthenica:ffmpeg-kit-full:6.0-2.LTS'
|
||||
implementation 'org.jetbrains.kotlin:kotlin-reflect:1.9.0'
|
||||
implementation 'com.github.dhaval2404:imagepicker:2.1'
|
||||
implementation 'com.google.zxing:core:3.4.1'
|
||||
|
||||
@@ -156,7 +156,6 @@
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:screenOrientation="sensorPortrait"
|
||||
android:theme="@style/Theme.FutoVideo.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.DeveloperActivity"
|
||||
|
||||
@@ -263,6 +263,10 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||
this.rating = obj.rating ?? null; //IRating
|
||||
this.subtitles = obj.subtitles ?? [];
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
|
||||
if (obj.getContentRecommendations) {
|
||||
this.getContentRecommendations = obj.getContentRecommendations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1075,8 +1075,9 @@ class UISlideOverlays {
|
||||
StatePlayer.TYPE_WATCHLATER,
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -101,7 +101,8 @@ class SyncHomeActivity : AppCompatActivity() {
|
||||
private fun updateDeviceView(syncDeviceView: SyncDeviceView, publicKey: String, session: SyncSession?): SyncDeviceView {
|
||||
val connected = session?.connected ?: false
|
||||
syncDeviceView.setLinkType(if (connected) LinkType.Local else LinkType.None)
|
||||
.setName(publicKey)
|
||||
.setName(session?.displayName ?: StateSync.instance.getCachedName(publicKey) ?: publicKey)
|
||||
//TODO: also display public key?
|
||||
.setStatus(if (connected) "Connected" else "Disconnected")
|
||||
return syncDeviceView
|
||||
}
|
||||
|
||||
+2
-2
@@ -238,8 +238,8 @@ class ChannelFragment : MainFragment() {
|
||||
}
|
||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||
if (content is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
|
||||
+2
-2
@@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
};
|
||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
|
||||
+39
-4
@@ -23,6 +23,7 @@ import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import com.futo.platformplayer.views.NoResultsView
|
||||
import com.futo.platformplayer.views.ToggleBar
|
||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
|
||||
import com.futo.platformplayer.views.adapters.InsertedViewHolder
|
||||
@@ -94,6 +95,8 @@ class HomeFragment : MainFragment() {
|
||||
class HomeView : ContentFeedView<HomeFragment> {
|
||||
override val feedStyle: FeedStyle get() = Settings.instance.home.getHomeFeedStyle();
|
||||
|
||||
private var _toggleBar: ToggleBar? = null;
|
||||
|
||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
||||
override val shouldShowTimeBar: Boolean get() = Settings.instance.home.progressBar
|
||||
|
||||
@@ -127,6 +130,8 @@ class HomeFragment : MainFragment() {
|
||||
}, fragment);
|
||||
};
|
||||
|
||||
initializeToolbarContent();
|
||||
|
||||
setPreviewsEnabled(Settings.instance.home.previewFeedItems);
|
||||
showAnnouncementView()
|
||||
}
|
||||
@@ -201,13 +206,43 @@ class HomeFragment : MainFragment() {
|
||||
loadResults();
|
||||
}
|
||||
|
||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||
return results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) };
|
||||
private val _filterLock = Object();
|
||||
private var _toggleRecent = false;
|
||||
fun initializeToolbarContent() {
|
||||
//Not stable enough with current viewport paging, doesn't work with less results, and reloads content instead of just re-filtering existing
|
||||
/*
|
||||
_toggleBar = ToggleBar(context).apply {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
synchronized(_filterLock) {
|
||||
_toggleBar?.setToggles(
|
||||
//TODO: loadResults needs to be replaced with an internal reload of the current content
|
||||
ToggleBar.Toggle("Recent", _toggleRecent) { _toggleRecent = it; loadResults(false) }
|
||||
)
|
||||
}
|
||||
|
||||
_toolbarContentView.addView(_toggleBar, 0);
|
||||
*/
|
||||
}
|
||||
|
||||
private fun loadResults() {
|
||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||
return results.filter {
|
||||
if(StateMeta.instance.isVideoHidden(it.url))
|
||||
return@filter false;
|
||||
if(StateMeta.instance.isCreatorHidden(it.author.url))
|
||||
return@filter false;
|
||||
|
||||
if(_toggleRecent && (it.datetime?.getNowDiffHours() ?: 0) > 23) {
|
||||
return@filter false;
|
||||
}
|
||||
|
||||
return@filter true;
|
||||
};
|
||||
}
|
||||
|
||||
private fun loadResults(withRefetch: Boolean = true) {
|
||||
setLoading(true);
|
||||
_taskGetPager.run(true);
|
||||
_taskGetPager.run(withRefetch);
|
||||
}
|
||||
private fun loadedResult(pager : IPager<IPlatformContent>) {
|
||||
if (pager is EmptyPager<IPlatformContent>) {
|
||||
|
||||
+1
-1
@@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version && config.name != "Youtube") {
|
||||
if (config.version <= c.version) {
|
||||
Logger.i(TAG, "Plugin is up to date.");
|
||||
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
||||
return@launch;
|
||||
|
||||
+60
-23
@@ -579,6 +579,14 @@ class VideoDetailView : ConstraintLayout {
|
||||
_minimize_title.setOnClickListener { onMaximize.emit(false) };
|
||||
_minimize_meta.setOnClickListener { onMaximize.emit(false) };
|
||||
|
||||
_player.onStateChange.subscribe {
|
||||
if (_player.activelyPlaying) {
|
||||
Logger.i(TAG, "Play changed, resetting error counter _didTriggerDatasourceErrorCount = 0 (_player.activelyPlaying: ${_player.activelyPlaying})")
|
||||
_didTriggerDatasourceErrorCount = 0;
|
||||
_didTriggerDatasourceError = false;
|
||||
}
|
||||
}
|
||||
|
||||
_player.onPlayChanged.subscribe {
|
||||
if (StateCasting.instance.activeDevice == null) {
|
||||
handlePlayChanged(it);
|
||||
@@ -922,7 +930,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
} else if(devices.size == 1){
|
||||
val device = devices.first();
|
||||
Logger.i(TAG, "Send to device? (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non ${device.remotePublicKey}" , {
|
||||
UIDialogs.showConfirmationDialog(context, "Would you like to open\n[${videoToSend.name}]\non '${device.displayName}'" , {
|
||||
Logger.i(TAG, "Send to device confirmed (public key: ${device.remotePublicKey}): " + videoToSend.url)
|
||||
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
@@ -963,6 +971,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
throw IllegalStateException("Expected media content, found ${video.contentType}");
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
_videoResumePositionMilliseconds = _player.position
|
||||
setVideoDetails(video);
|
||||
}
|
||||
}
|
||||
@@ -1265,8 +1274,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
|
||||
Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
|
||||
_didTriggerDatasourceErrroCount = 0;
|
||||
_didTriggerDatasourceError = false;
|
||||
_autoplayVideo = null
|
||||
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
|
||||
|
||||
@@ -1277,6 +1284,10 @@ class VideoDetailView : ConstraintLayout {
|
||||
_lastVideoSource = null;
|
||||
_lastAudioSource = null;
|
||||
_lastSubtitleSource = null;
|
||||
|
||||
Logger.i(TAG, "_didTriggerDatasourceErrorCount reset to 0 because new video")
|
||||
_didTriggerDatasourceErrorCount = 0;
|
||||
_didTriggerDatasourceError = false;
|
||||
}
|
||||
|
||||
if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
|
||||
@@ -1831,7 +1842,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private var _didTriggerDatasourceErrroCount = 0;
|
||||
private var _didTriggerDatasourceErrorCount = 0;
|
||||
private var _didTriggerDatasourceError = false;
|
||||
private fun onDataSourceError(exception: Throwable) {
|
||||
Logger.e(TAG, "onDataSourceError", exception);
|
||||
@@ -1841,32 +1852,53 @@ class VideoDetailView : ConstraintLayout {
|
||||
return;
|
||||
val config = currentVideo.sourceConfig;
|
||||
|
||||
if(_didTriggerDatasourceErrroCount <= 3) {
|
||||
if(_didTriggerDatasourceErrorCount <= 3) {
|
||||
_didTriggerDatasourceError = true;
|
||||
_didTriggerDatasourceErrroCount++;
|
||||
_didTriggerDatasourceErrorCount++;
|
||||
|
||||
UIDialogs.toast("Detected video error, attempting automatic reload (${_didTriggerDatasourceErrorCount})");
|
||||
Logger.i(TAG, "Block detected, attempting bypass (_didTriggerDatasourceErrorCount = ${_didTriggerDatasourceErrorCount})");
|
||||
|
||||
UIDialogs.toast("Block detected, attempting bypass");
|
||||
//return;
|
||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||
val previousVideoSource = _lastVideoSource;
|
||||
val previousAudioSource = _lastAudioSource;
|
||||
try {
|
||||
val newDetails = StatePlatform.instance.getContentDetails(currentVideo.url, true).await();
|
||||
val previousVideoSource = _lastVideoSource;
|
||||
val previousAudioSource = _lastAudioSource;
|
||||
|
||||
if(newDetails is IPlatformVideoDetails) {
|
||||
val newVideoSource = if(previousVideoSource != null)
|
||||
VideoHelper.selectBestVideoSource(newDetails.video, previousVideoSource.height * previousVideoSource.width, FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS);
|
||||
else null;
|
||||
val newAudioSource = if(previousAudioSource != null)
|
||||
VideoHelper.selectBestAudioSource(newDetails.video, FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS, previousAudioSource.language, previousAudioSource.bitrate.toLong());
|
||||
else null;
|
||||
withContext(Dispatchers.Main) {
|
||||
video = newDetails;
|
||||
_player.setSource(newVideoSource, newAudioSource, true, true);
|
||||
if (newDetails is IPlatformVideoDetails) {
|
||||
val newVideoSource = if (previousVideoSource != null)
|
||||
VideoHelper.selectBestVideoSource(
|
||||
newDetails.video,
|
||||
previousVideoSource.height * previousVideoSource.width,
|
||||
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS
|
||||
);
|
||||
else null;
|
||||
val newAudioSource = if (previousAudioSource != null)
|
||||
VideoHelper.selectBestAudioSource(
|
||||
newDetails.video,
|
||||
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
|
||||
previousAudioSource.language,
|
||||
previousAudioSource.bitrate.toLong()
|
||||
);
|
||||
else null;
|
||||
withContext(Dispatchers.Main) {
|
||||
video = newDetails;
|
||||
_player.setSource(newVideoSource, newAudioSource, true, true);
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logger.e(TAG, "Failed to get video details, attempting retrying without reloading.", e)
|
||||
fragment.lifecycleScope.launch(Dispatchers.Main) {
|
||||
video?.let {
|
||||
_videoResumePositionMilliseconds = _player.position
|
||||
setVideoDetails(it, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(_didTriggerDatasourceErrroCount > 3) {
|
||||
else if(_didTriggerDatasourceErrorCount > 3) {
|
||||
UIDialogs.showDialog(context, R.drawable.ic_error_pred,
|
||||
context.getString(R.string.media_error),
|
||||
context.getString(R.string.the_media_source_encountered_an_unauthorized_error_this_might_be_solved_by_a_plugin_reload_would_you_like_to_reload_experimental),
|
||||
@@ -2595,8 +2627,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
}
|
||||
onAddToQueueClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -177,8 +177,11 @@ class StatePlaylists {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1) {
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
|
||||
var wasNew = false;
|
||||
synchronized(_watchlistStore) {
|
||||
if(!_watchlistStore.hasItem { it.url == video.url })
|
||||
wasNew = true;
|
||||
_watchlistStore.saveAsync(video);
|
||||
if(orderPosition == -1)
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||
@@ -198,6 +201,7 @@ class StatePlaylists {
|
||||
}
|
||||
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
return wasNew;
|
||||
}
|
||||
|
||||
fun getLastPlayedPlaylist() : Playlist? {
|
||||
|
||||
@@ -44,6 +44,7 @@ import kotlin.system.measureTimeMillis
|
||||
|
||||
class StateSync {
|
||||
private val _authorizedDevices = FragmentedStorage.get<StringArrayStorage>("authorized_devices")
|
||||
private val _nameStorage = FragmentedStorage.get<StringStringMapStorage>("sync_remembered_name_storage")
|
||||
private val _syncKeyPair = FragmentedStorage.get<StringStorage>("sync_key_pair")
|
||||
private val _lastAddressStorage = FragmentedStorage.get<StringStringMapStorage>("sync_last_address_storage")
|
||||
private val _syncSessionData = FragmentedStorage.get<StringTMapStorage<SyncSessionData>>("syncSessionData")
|
||||
@@ -305,12 +306,22 @@ class StateSync {
|
||||
synchronized(_sessions) {
|
||||
session = _sessions[s.remotePublicKey]
|
||||
if (session == null) {
|
||||
val remoteDeviceName = synchronized(_nameStorage) {
|
||||
_nameStorage.get(remotePublicKey)
|
||||
}
|
||||
|
||||
session = SyncSession(remotePublicKey, onAuthorized = { it, isNewlyAuthorized, isNewSession ->
|
||||
if (!isNewSession) {
|
||||
return@SyncSession
|
||||
}
|
||||
|
||||
Logger.i(TAG, "${s.remotePublicKey} authorized")
|
||||
it.remoteDeviceName?.let { remoteDeviceName ->
|
||||
synchronized(_nameStorage) {
|
||||
_nameStorage.setAndSave(remotePublicKey, remoteDeviceName)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.i(TAG, "${s.remotePublicKey} authorized (name: ${it.displayName})")
|
||||
synchronized(_lastAddressStorage) {
|
||||
_lastAddressStorage.setAndSave(remotePublicKey, s.remoteAddress)
|
||||
}
|
||||
@@ -341,7 +352,7 @@ class StateSync {
|
||||
|
||||
deviceRemoved.emit(it.remotePublicKey)
|
||||
|
||||
})
|
||||
}, remoteDeviceName)
|
||||
_sessions[remotePublicKey] = session!!
|
||||
}
|
||||
session!!.addSocketSession(s)
|
||||
@@ -469,6 +480,12 @@ class StateSync {
|
||||
}
|
||||
}
|
||||
|
||||
fun getCachedName(publicKey: String): String? {
|
||||
return synchronized(_nameStorage) {
|
||||
_nameStorage.get(publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(publicKey: String) {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
||||
@@ -6,12 +6,10 @@ import com.futo.platformplayer.api.media.Serializer
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.smartMerge
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateBackup
|
||||
import com.futo.platformplayer.states.StateHistory
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
@@ -30,6 +28,7 @@ import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.time.Instant
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.ZoneOffset
|
||||
@@ -53,6 +52,9 @@ class SyncSession : IAuthorizable {
|
||||
private val _id = UUID.randomUUID()
|
||||
private var _remoteId: UUID? = null
|
||||
private var _lastAuthorizedRemoteId: UUID? = null
|
||||
var remoteDeviceName: String? = null
|
||||
private set
|
||||
val displayName: String get() = remoteDeviceName ?: remotePublicKey
|
||||
|
||||
var connected: Boolean = false
|
||||
private set(v) {
|
||||
@@ -62,7 +64,7 @@ class SyncSession : IAuthorizable {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit) {
|
||||
constructor(remotePublicKey: String, onAuthorized: (session: SyncSession, isNewlyAuthorized: Boolean, isNewSession: Boolean) -> Unit, onUnauthorized: (session: SyncSession) -> Unit, onConnectedChanged: (session: SyncSession, connected: Boolean) -> Unit, onClose: (session: SyncSession) -> Unit, remoteDeviceName: String?) {
|
||||
this.remotePublicKey = remotePublicKey
|
||||
_onAuthorized = onAuthorized
|
||||
_onUnauthorized = onUnauthorized
|
||||
@@ -85,7 +87,20 @@ class SyncSession : IAuthorizable {
|
||||
|
||||
fun authorize(socketSession: SyncSocketSession) {
|
||||
Logger.i(TAG, "Sent AUTHORIZED with session id $_id")
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||
|
||||
if (socketSession.remoteVersion >= 3) {
|
||||
val idStringBytes = _id.toString().toByteArray()
|
||||
val nameBytes = "${android.os.Build.MANUFACTURER}-${android.os.Build.MODEL}".toByteArray()
|
||||
val buffer = ByteArray(1 + idStringBytes.size + 1 + nameBytes.size)
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||
put(idStringBytes.size.toByte())
|
||||
put(idStringBytes)
|
||||
put(nameBytes.size.toByte())
|
||||
put(nameBytes)
|
||||
}.apply { flip() })
|
||||
} else {
|
||||
socketSession.send(Opcode.NOTIFY_AUTHORIZED.value, 0u, ByteBuffer.wrap(_id.toString().toByteArray()))
|
||||
}
|
||||
_authorized = true
|
||||
checkAuthorized()
|
||||
}
|
||||
@@ -138,15 +153,37 @@ class SyncSession : IAuthorizable {
|
||||
|
||||
when (opcode) {
|
||||
Opcode.NOTIFY_AUTHORIZED.value -> {
|
||||
val str = data.toUtf8String()
|
||||
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||
if (socketSession.remoteVersion >= 3) {
|
||||
val idByteCount = data.get().toInt()
|
||||
if (idByteCount > 64)
|
||||
throw Exception("Id should always be smaller than 64 bytes")
|
||||
|
||||
val idBytes = ByteArray(idByteCount)
|
||||
data.get(idBytes)
|
||||
|
||||
val nameByteCount = data.get().toInt()
|
||||
if (nameByteCount > 64)
|
||||
throw Exception("Name should always be smaller than 64 bytes")
|
||||
|
||||
val nameBytes = ByteArray(nameByteCount)
|
||||
data.get(nameBytes)
|
||||
|
||||
_remoteId = UUID.fromString(idBytes.toString(Charsets.UTF_8))
|
||||
remoteDeviceName = nameBytes.toString(Charsets.UTF_8)
|
||||
} else {
|
||||
val str = data.toUtf8String()
|
||||
_remoteId = if (data.remaining() >= 0) UUID.fromString(str) else UUID.fromString("00000000-0000-0000-0000-000000000000")
|
||||
remoteDeviceName = null
|
||||
}
|
||||
|
||||
_remoteAuthorized = true
|
||||
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId")
|
||||
Logger.i(TAG, "Received AUTHORIZED with session id $_remoteId (device name: '${remoteDeviceName ?: "not set"}')")
|
||||
checkAuthorized()
|
||||
return
|
||||
}
|
||||
Opcode.NOTIFY_UNAUTHORIZED.value -> {
|
||||
_remoteId = null
|
||||
remoteDeviceName = null
|
||||
_lastAuthorizedRemoteId = null
|
||||
_remoteAuthorized = false
|
||||
_onUnauthorized(this)
|
||||
|
||||
@@ -46,6 +46,8 @@ class SyncSocketSession {
|
||||
val localPublicKey: String get() = _localPublicKey
|
||||
private val _onData: (session: SyncSocketSession, opcode: UByte, subOpcode: UByte, data: ByteBuffer) -> Unit
|
||||
var authorizable: IAuthorizable? = null
|
||||
var remoteVersion: Int = -1
|
||||
private set
|
||||
|
||||
val remoteAddress: String
|
||||
|
||||
@@ -162,11 +164,12 @@ class SyncSocketSession {
|
||||
}
|
||||
|
||||
private fun performVersionCheck() {
|
||||
val CURRENT_VERSION = 2
|
||||
val CURRENT_VERSION = 3
|
||||
val MINIMUM_VERSION = 2
|
||||
_outputStream.writeInt(CURRENT_VERSION)
|
||||
val version = _inputStream.readInt()
|
||||
Logger.i(TAG, "performVersionCheck (version = $version)")
|
||||
if (version != CURRENT_VERSION)
|
||||
remoteVersion = _inputStream.readInt()
|
||||
Logger.i(TAG, "performVersionCheck (version = $remoteVersion)")
|
||||
if (remoteVersion < MINIMUM_VERSION)
|
||||
throw Exception("Invalid version")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.futo.platformplayer.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.states.StateSubscriptionGroups
|
||||
import com.futo.platformplayer.states.StateSubscriptions
|
||||
import com.futo.platformplayer.views.AnyAdapterView.Companion.asAny
|
||||
import com.futo.platformplayer.views.others.ToggleTagView
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionBarViewHolder
|
||||
import com.futo.platformplayer.views.adapters.viewholders.SubscriptionGroupBarViewHolder
|
||||
import com.futo.platformplayer.views.subscriptions.SubscriptionExploreButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ToggleBar : LinearLayout {
|
||||
private val _tagsContainer: LinearLayout;
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
StateSubscriptionGroups.instance.onGroupsChanged.remove(this);
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||
inflate(context, R.layout.view_toggle_bar, this);
|
||||
|
||||
_tagsContainer = findViewById(R.id.container_tags);
|
||||
}
|
||||
|
||||
fun setToggles(vararg buttons: Toggle) {
|
||||
_tagsContainer.removeAllViews();
|
||||
for(button in buttons) {
|
||||
_tagsContainer.addView(ToggleTagView(context).apply {
|
||||
this.setInfo(button.name, button.isActive);
|
||||
this.onClick.subscribe { button.action(it); };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Toggle {
|
||||
val name: String;
|
||||
val icon: Int;
|
||||
val action: (Boolean)->Unit;
|
||||
val isActive: Boolean;
|
||||
|
||||
constructor(name: String, icon: Int, isActive: Boolean = false, action: (Boolean)->Unit) {
|
||||
this.name = name;
|
||||
this.icon = icon;
|
||||
this.action = action;
|
||||
this.isActive = isActive;
|
||||
}
|
||||
constructor(name: String, isActive: Boolean = false, action: (Boolean)->Unit) {
|
||||
this.name = name;
|
||||
this.icon = 0;
|
||||
this.action = action;
|
||||
this.isActive = isActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -132,7 +132,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
fun hideAddTo() {
|
||||
_button_add_to.visibility = View.GONE
|
||||
_button_add_to_queue.visibility = View.GONE
|
||||
//_button_add_to_queue.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class QueueEditorOverlay : LinearLayout {
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_editor = findViewById(R.id.editor);
|
||||
_btnSettings = findViewById(R.id.button_settings);
|
||||
_overlayContainer = findViewById(R.id.overlay_container);
|
||||
_overlayContainer = findViewById(R.id.overlay_container_queue);
|
||||
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
|
||||
@@ -96,6 +96,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
val exoPlayerStateName: String;
|
||||
|
||||
var playing: Boolean = false;
|
||||
val activelyPlaying: Boolean get() = (exoPlayer?.player?.playbackState == Player.STATE_READY) && (exoPlayer?.player?.playWhenReady ?: false)
|
||||
val position: Long get() = exoPlayer?.player?.currentPosition ?: 0;
|
||||
val duration: Long get() = exoPlayer?.player?.duration ?: 0;
|
||||
|
||||
@@ -829,7 +830,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
Logger.i(TAG, "onPlayerError error=$error error.errorCode=${error.errorCode} connectivityLoss");
|
||||
|
||||
when (error.errorCode) {
|
||||
PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> {
|
||||
Logger.w(TAG, "ERROR_CODE_IO_BAD_HTTP_STATUS ${error.cause?.javaClass?.simpleName}");
|
||||
if(error.cause is HttpDataSource.InvalidResponseCodeException) {
|
||||
val cause = error.cause as HttpDataSource.InvalidResponseCodeException
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
app:srcCompat="@drawable/ic_settings" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_container"
|
||||
android:id="@+id/overlay_container_queue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="horizontal">
|
||||
<LinearLayout
|
||||
android:id="@+id/container_tags"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
@@ -418,7 +418,7 @@
|
||||
<string name="log_level">Log Level</string>
|
||||
<string name="logging">Logging</string>
|
||||
<string name="sync_grayjay">Sync Grayjay</string>
|
||||
<string name="sync_grayjay_description">Sync your settings across multiple devices</string>
|
||||
<string name="sync_grayjay_description">Sync your data across multiple devices</string>
|
||||
<string name="manage_polycentric_identity">Manage Polycentric identity</string>
|
||||
<string name="manage_your_polycentric_identity">Manage your Polycentric identity</string>
|
||||
<string name="manual_check">Manual check</string>
|
||||
|
||||
Submodule app/src/stable/assets/sources/apple-podcasts updated: 090104c7fa...07e39f9df7
Submodule app/src/stable/assets/sources/bilibili updated: 13b30fd76e...ce0571bdea
Submodule app/src/stable/assets/sources/bitchute updated: 7f869aa4b1...3fbd872ad8
Submodule app/src/stable/assets/sources/dailymotion updated: d00c7ff8e5...b34134ca2d
Submodule app/src/stable/assets/sources/kick updated: 8d957b6fc4...2046944c18
Submodule app/src/stable/assets/sources/nebula updated: 9e6dcf0935...f30a3bfc0f
Submodule app/src/stable/assets/sources/odysee updated: 04b4d8ed31...f2f83344eb
Submodule app/src/stable/assets/sources/patreon updated: 9c835e075c...e5dce87c9d
Submodule app/src/stable/assets/sources/peertube updated: 20fd03d984...2bcab14d01
Submodule app/src/stable/assets/sources/rumble updated: b9e6259f4e...a32dbb626a
Submodule app/src/stable/assets/sources/soundcloud updated: a72aeb85d0...ae47f2eaac
Submodule app/src/stable/assets/sources/spotify updated: eb231adeae...0d05e35cfc
Submodule app/src/stable/assets/sources/twitch updated: 1b2833cdf2...a75e846045
Submodule app/src/stable/assets/sources/youtube updated: 8f8774a782...857c147b3a
Submodule app/src/unstable/assets/sources/apple-podcasts updated: 090104c7fa...07e39f9df7
Submodule app/src/unstable/assets/sources/bilibili updated: 13b30fd76e...ce0571bdea
Submodule app/src/unstable/assets/sources/bitchute updated: 7f869aa4b1...3fbd872ad8
Submodule app/src/unstable/assets/sources/dailymotion updated: d00c7ff8e5...b34134ca2d
Submodule app/src/unstable/assets/sources/kick updated: 8d957b6fc4...2046944c18
Submodule app/src/unstable/assets/sources/nebula updated: 9e6dcf0935...f30a3bfc0f
Submodule app/src/unstable/assets/sources/odysee updated: 04b4d8ed31...f2f83344eb
Submodule app/src/unstable/assets/sources/patreon updated: 9c835e075c...e5dce87c9d
Submodule app/src/unstable/assets/sources/peertube updated: 20fd03d984...2bcab14d01
Submodule app/src/unstable/assets/sources/rumble updated: b9e6259f4e...a32dbb626a
Submodule app/src/unstable/assets/sources/soundcloud updated: a72aeb85d0...ae47f2eaac
Submodule app/src/unstable/assets/sources/spotify updated: eb231adeae...0d05e35cfc
Submodule app/src/unstable/assets/sources/twitch updated: 1b2833cdf2...a75e846045
Submodule app/src/unstable/assets/sources/youtube updated: 8f8774a782...857c147b3a
@@ -8,7 +8,7 @@ The goal of the authentication system is to provide plugins the ability to make
|
||||
>
|
||||
>You should always only login (and install for that matter) plugins you trust.
|
||||
|
||||
How to actually use the authenticated client is described in the Http package documentation (See [Package: Http](_blank)).
|
||||
How to actually use the authenticated client is described in the Http package documentation (See [Package: Http](docs/packages/packageHttp.md)).
|
||||
This documentation will exclusively focus on configuring authentication and how it behaves.
|
||||
|
||||
## How it works
|
||||
@@ -58,5 +58,5 @@ Headers are exclusively applied to the domains they are retrieved from. A plugin
|
||||
By default, when authentication requests are made, the authenticated client will behave similar to that of a normal browser. Meaning that if the server you are communicating with sets new cookies, the client will use those cookies instead. These new cookies are NOT saved to disk, meaning that whenever that plugin reloads the cookies will revert to those assigned at login.
|
||||
|
||||
This behavior can be modified by using custom http clients as described in the http package documentation.
|
||||
(See [Package: Http](_blank))
|
||||
(See [Package: Http](docs/packages/packageHttp.md))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user