mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-05-29 04:50:19 +02:00
Creator sort options views and watchtime, subscription header ordered by views, view/watchtime tracking for subscriptions, optional view/watchtime metrics in creator tab, cache channel results if subscribed, update subs only if older than 5 min
This commit is contained in:
parent
93f5260e20
commit
f8ee340499
@ -185,6 +185,25 @@ fun OffsetDateTime.toHumanNowDiffString(abs: Boolean = false) : String {
|
|||||||
|
|
||||||
return "${value} ${unit}";
|
return "${value} ${unit}";
|
||||||
};
|
};
|
||||||
|
fun Int.toHumanTimeIndicator(abs: Boolean = false) : String {
|
||||||
|
var value = this;
|
||||||
|
|
||||||
|
var unit = "s";
|
||||||
|
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
if(value >= secondsInHour) {
|
||||||
|
value = (this / secondsInHour).toInt();
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
unit = "hr" + (if(value > 1) "s" else "");
|
||||||
|
}
|
||||||
|
else if(value >= secondsInMinute) {
|
||||||
|
value = (this / secondsInMinute).toInt();
|
||||||
|
if(abs) value = abs(value);
|
||||||
|
unit = "min";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "${value}${unit}";
|
||||||
|
}
|
||||||
|
|
||||||
fun Long.toHumanTime(isMs: Boolean): String {
|
fun Long.toHumanTime(isMs: Boolean): String {
|
||||||
var scaler = 1;
|
var scaler = 1;
|
||||||
|
@ -86,6 +86,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
@FormField(
|
@FormField(
|
||||||
R.string.submit_feedback, FieldForm.BUTTON,
|
R.string.submit_feedback, FieldForm.BUTTON,
|
||||||
R.string.give_feedback_on_the_application, -1
|
R.string.give_feedback_on_the_application, -1
|
||||||
@ -104,7 +105,7 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
//Ignored
|
//Ignored
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@FormField(
|
@FormField(
|
||||||
R.string.manage_tabs, FieldForm.BUTTON,
|
R.string.manage_tabs, FieldForm.BUTTON,
|
||||||
@ -201,6 +202,12 @@ class Settings : FragmentedStorageFileJson() {
|
|||||||
fun getSubscriptionsConcurrency() : Int {
|
fun getSubscriptionsConcurrency() : Int {
|
||||||
return threadIndexToCount(subscriptionConcurrency);
|
return threadIndexToCount(subscriptionConcurrency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FormField(R.string.show_watch_metrics, FieldForm.TOGGLE, R.string.show_watch_metrics_description, 9)
|
||||||
|
var showWatchMetrics: Boolean = false;
|
||||||
|
|
||||||
|
@FormField(R.string.track_playtime_locally, FieldForm.TOGGLE, R.string.track_playtime_locally_description, 10)
|
||||||
|
var allowPlaytimeTracking: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
@FormField(R.string.player, "group", R.string.change_behavior_of_the_player, 4)
|
||||||
|
@ -92,7 +92,7 @@ class UISlideOverlays {
|
|||||||
menu.selectOption(null, "fetchPosts", true, true);
|
menu.selectOption(null, "fetchPosts", true, true);
|
||||||
|
|
||||||
menu.onOK.subscribe {
|
menu.onOK.subscribe {
|
||||||
StateSubscriptions.instance.saveSubscription(subscription);
|
subscription.save();
|
||||||
menu.hide(true);
|
menu.hide(true);
|
||||||
};
|
};
|
||||||
menu.onCancel.subscribe {
|
menu.onCancel.subscribe {
|
||||||
|
@ -41,7 +41,7 @@ class ChannelContentCache {
|
|||||||
val trimmed: Int;
|
val trimmed: Int;
|
||||||
if(toTrim > 0) {
|
if(toTrim > 0) {
|
||||||
val redundantContent = _channelContents.flatMap { it.value.getItems().filter { it.datetime != null && it.datetime!!.isBefore(minDays) }.drop(9) }
|
val redundantContent = _channelContents.flatMap { it.value.getItems().filter { it.datetime != null && it.datetime!!.isBefore(minDays) }.drop(9) }
|
||||||
.sortedByDescending { it.datetime!! }.take(toTrim);
|
.sortedBy { it.datetime!! }.take(toTrim);
|
||||||
for(content in redundantContent)
|
for(content in redundantContent)
|
||||||
uncacheContent(content);
|
uncacheContent(content);
|
||||||
trimmed = redundantContent.size;
|
trimmed = redundantContent.size;
|
||||||
|
@ -25,6 +25,7 @@ import com.futo.platformplayer.api.media.structures.IPager
|
|||||||
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
import com.futo.platformplayer.api.media.structures.IRefreshPager
|
||||||
import com.futo.platformplayer.api.media.structures.IReplacerPager
|
import com.futo.platformplayer.api.media.structures.IReplacerPager
|
||||||
import com.futo.platformplayer.api.media.structures.MultiPager
|
import com.futo.platformplayer.api.media.structures.MultiPager
|
||||||
|
import com.futo.platformplayer.cache.ChannelContentCache
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.constructs.Event2
|
import com.futo.platformplayer.constructs.Event2
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
@ -33,6 +34,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
|||||||
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
import com.futo.platformplayer.fragment.mainactivity.main.FeedView
|
||||||
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
|
||||||
import com.futo.platformplayer.states.StatePolycentric
|
import com.futo.platformplayer.states.StatePolycentric
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
import com.futo.platformplayer.views.FeedStyle
|
||||||
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
|
import com.futo.platformplayer.views.adapters.PreviewContentListAdapter
|
||||||
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
|
||||||
@ -74,9 +76,14 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
|||||||
|
|
||||||
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
|
private val _taskLoadVideos = TaskHandler<IPlatformChannel, IPager<IPlatformContent>>({lifecycleScope}, {
|
||||||
return@TaskHandler getContentPager(it);
|
return@TaskHandler getContentPager(it);
|
||||||
}).success {
|
}).success { livePager ->
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setPager(it);
|
|
||||||
|
val pager = if(_channel?.let { StateSubscriptions.instance.isSubscribed(it) } == true)
|
||||||
|
ChannelContentCache.cachePagerResults(lifecycleScope, livePager);
|
||||||
|
else livePager;
|
||||||
|
|
||||||
|
setPager(pager);
|
||||||
}
|
}
|
||||||
.exception<ScriptCaptchaRequiredException> { }
|
.exception<ScriptCaptchaRequiredException> { }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
|
@ -118,7 +118,11 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
if(recyclerData.loadedFeedStyle != feedStyle ||
|
if(recyclerData.loadedFeedStyle != feedStyle ||
|
||||||
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
recyclerData.lastLoad.getNowDiffSeconds() > 60 ) {
|
||||||
recyclerData.lastLoad = OffsetDateTime.now();
|
recyclerData.lastLoad = OffsetDateTime.now();
|
||||||
loadResults();
|
|
||||||
|
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5)
|
||||||
|
loadResults(false);
|
||||||
|
else if(recyclerData.results.size == 0)
|
||||||
|
loadCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
val announcementsView = _announcementsView;
|
val announcementsView = _announcementsView;
|
||||||
@ -201,7 +205,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
context.getString(R.string.rate_limit_warning), context.getString(R.string.this_is_a_temporary_measure_to_prevent_people_from_hitting_rate_limit_until_we_have_better_support_for_lots_of_subscriptions) + context.getString(R.string.you_have_too_many_subscriptions_for_the_following_plugins),
|
context.getString(R.string.rate_limit_warning), context.getString(R.string.this_is_a_temporary_measure_to_prevent_people_from_hitting_rate_limit_until_we_have_better_support_for_lots_of_subscriptions) + context.getString(R.string.you_have_too_many_subscriptions_for_the_following_plugins),
|
||||||
subsByLimited.map { it.first.config.name + ": " + it.second.size + " " + context.getString(R.string.subscriptions) } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", {
|
subsByLimited.map { it.first.config.name + ": " + it.second.size + " " + context.getString(R.string.subscriptions) } .joinToString("\n"), 0, UIDialogs.Action("Refresh Anyway", {
|
||||||
_bypassRateLimit = true;
|
_bypassRateLimit = true;
|
||||||
loadResults();
|
loadResults(true);
|
||||||
}, UIDialogs.ActionStyle.DANGEROUS_TEXT),
|
}, UIDialogs.ActionStyle.DANGEROUS_TEXT),
|
||||||
UIDialogs.Action("OK", {
|
UIDialogs.Action("OK", {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
@ -213,7 +217,7 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
Logger.w(ChannelFragment.TAG, "Failed to load channel.", it);
|
||||||
if(it !is CancellationException)
|
if(it !is CancellationException)
|
||||||
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults() });
|
UIDialogs.showGeneralRetryErrorDialog(context, it.message ?: "", it, { loadResults(true) });
|
||||||
else {
|
else {
|
||||||
finishRefreshLayoutLoader();
|
finishRefreshLayoutLoader();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -278,16 +282,19 @@ class SubscriptionsFeedFragment : MainFragment() {
|
|||||||
loadResults(true);
|
loadResults(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadCache() {
|
||||||
|
Logger.i(TAG, "Subscriptions load cache");
|
||||||
|
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
||||||
|
val results = cachePager.getResults();
|
||||||
|
Logger.i(TAG, "Subscriptions show cache (${results.size})");
|
||||||
|
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
||||||
|
setPager(cachePager);
|
||||||
|
}
|
||||||
private fun loadResults(withRefetch: Boolean = false) {
|
private fun loadResults(withRefetch: Boolean = false) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Logger.i(TAG, "Subscriptions load");
|
Logger.i(TAG, "Subscriptions load");
|
||||||
if(recyclerData.results.size == 0) {
|
if(recyclerData.results.size == 0) {
|
||||||
Logger.i(TAG, "Subscriptions load cache");
|
loadCache();
|
||||||
val cachePager = ChannelContentCache.instance.getSubscriptionCachePager();
|
|
||||||
val results = cachePager.getResults();
|
|
||||||
Logger.i(TAG, "Subscriptions show cache (${results.size})");
|
|
||||||
setTextCentered(if (results.isEmpty()) context.getString(R.string.no_results_found_swipe_down_to_refresh) else null);
|
|
||||||
setPager(cachePager);
|
|
||||||
} else {
|
} else {
|
||||||
setTextCentered(null);
|
setTextCentered(null);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ import android.graphics.drawable.BitmapDrawable
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.Icon
|
import android.graphics.drawable.Icon
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Browser
|
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@ -23,7 +22,6 @@ import android.view.MotionEvent
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@ -66,6 +64,7 @@ import com.futo.platformplayer.engine.exceptions.ScriptUnavailableException
|
|||||||
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
import com.futo.platformplayer.exceptions.UnsupportedCastException
|
||||||
import com.futo.platformplayer.helpers.VideoHelper
|
import com.futo.platformplayer.helpers.VideoHelper
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
|
import com.futo.platformplayer.models.Subscription
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.receivers.MediaControlReceiver
|
import com.futo.platformplayer.receivers.MediaControlReceiver
|
||||||
import com.futo.platformplayer.states.*
|
import com.futo.platformplayer.states.*
|
||||||
@ -96,7 +95,6 @@ import com.google.android.exoplayer2.Format
|
|||||||
import com.google.android.exoplayer2.ui.PlayerControlView
|
import com.google.android.exoplayer2.ui.PlayerControlView
|
||||||
import com.google.android.exoplayer2.ui.TimeBar
|
import com.google.android.exoplayer2.ui.TimeBar
|
||||||
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException
|
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException
|
||||||
import com.google.common.base.Stopwatch
|
|
||||||
import com.google.protobuf.ByteString
|
import com.google.protobuf.ByteString
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
@ -436,6 +434,7 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
if (!_isCasting && !_didStop) {
|
if (!_isCasting && !_didStop) {
|
||||||
setLastPositionMilliseconds(position, true);
|
setLastPositionMilliseconds(position, true);
|
||||||
}
|
}
|
||||||
|
updatePlaybackTracking(position);
|
||||||
};
|
};
|
||||||
|
|
||||||
_player.onVideoClicked.subscribe {
|
_player.onVideoClicked.subscribe {
|
||||||
@ -610,6 +609,61 @@ class VideoDetailView : ConstraintLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val _trackingUpdateTimeLock = Object();
|
||||||
|
val _trackingUpdateInterval = 3000;
|
||||||
|
var _trackingLastUpdateTime = System.currentTimeMillis();
|
||||||
|
var _trackingLastPosition: Long = 0;
|
||||||
|
var _trackingLastVideo: IPlatformVideoDetails? = null;
|
||||||
|
var _trackingTotalWatched: Long = 0;
|
||||||
|
var _trackingDidCountView: Boolean = false;
|
||||||
|
var _trackingLastVideoSubscription: Subscription? = null;
|
||||||
|
fun updatePlaybackTracking(position: Long) {
|
||||||
|
if(!Settings.instance.subscriptions.allowPlaytimeTracking)
|
||||||
|
return;
|
||||||
|
val now = System.currentTimeMillis();
|
||||||
|
val shouldUpdate = synchronized(_trackingUpdateTimeLock) {
|
||||||
|
val doUpdate = (now - _trackingLastUpdateTime) > _trackingUpdateInterval;
|
||||||
|
if(doUpdate)
|
||||||
|
_trackingLastUpdateTime = now;
|
||||||
|
return@synchronized doUpdate;
|
||||||
|
}
|
||||||
|
if(shouldUpdate) {
|
||||||
|
val currentVideo = video;
|
||||||
|
val delta = position - _trackingLastPosition;
|
||||||
|
_trackingLastPosition = position;
|
||||||
|
|
||||||
|
if(currentVideo != null && currentVideo == _trackingLastVideo) {
|
||||||
|
if(delta > 500 && delta < _trackingUpdateInterval * 1.5) {
|
||||||
|
_trackingLastVideoSubscription?.let {
|
||||||
|
Logger.i(TAG, "Subscription [${it.channel.name}] watch time delta [${delta}]" +
|
||||||
|
"(${"%.2f".format((_trackingTotalWatched / 1000) / currentVideo.duration.toDouble().coerceAtLeast(1.0))})");
|
||||||
|
it.updatePlayback(currentVideo, (delta / 1000).toInt());
|
||||||
|
_trackingTotalWatched += delta;
|
||||||
|
if(!_trackingDidCountView && currentVideo.duration > 0) {
|
||||||
|
val percentage = (_trackingTotalWatched / 1000) / currentVideo.duration.toDouble();
|
||||||
|
if(percentage > 0.4) {
|
||||||
|
Logger.i(TAG, "Subscription [${it.channel.name}] new view");
|
||||||
|
_trackingDidCountView = true;
|
||||||
|
it.addPlaybackView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.saveAsync();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if(_trackingLastVideo == null && currentVideo == null)
|
||||||
|
return;
|
||||||
|
_trackingLastVideo = currentVideo;
|
||||||
|
_trackingTotalWatched = 0;
|
||||||
|
if(currentVideo?.author?.url != null)
|
||||||
|
_trackingLastVideoSubscription = StateSubscriptions.instance.getSubscription(currentVideo.author.url);
|
||||||
|
else
|
||||||
|
_trackingLastVideoSubscription = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateMoreButtons() {
|
fun updateMoreButtons() {
|
||||||
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
val buttons = listOf(RoundButton(context, R.drawable.ic_add, context.getString(R.string.add), TAG_ADD) {
|
||||||
(video ?: _searchVideo)?.let {
|
(video ?: _searchVideo)?.let {
|
||||||
|
@ -4,9 +4,11 @@ import com.futo.platformplayer.api.media.models.ResultCapabilities
|
|||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||||
|
import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails
|
||||||
import com.futo.platformplayer.getNowDiffDays
|
import com.futo.platformplayer.getNowDiffDays
|
||||||
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.states.StateSubscriptions
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
@ -43,6 +45,9 @@ class Subscription {
|
|||||||
var uploadStreamInterval : Int = 0;
|
var uploadStreamInterval : Int = 0;
|
||||||
var uploadPostInterval : Int = 0;
|
var uploadPostInterval : Int = 0;
|
||||||
|
|
||||||
|
var playbackSeconds: Int = 0;
|
||||||
|
var playbackViews: Int = 0;
|
||||||
|
|
||||||
|
|
||||||
constructor(channel : SerializedChannel) {
|
constructor(channel : SerializedChannel) {
|
||||||
this.channel = channel;
|
this.channel = channel;
|
||||||
@ -55,10 +60,24 @@ class Subscription {
|
|||||||
|
|
||||||
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
fun getClient() = StatePlatform.instance.getChannelClientOrNull(channel.url);
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
StateSubscriptions.instance.saveSubscription(this);
|
||||||
|
}
|
||||||
|
fun saveAsync() {
|
||||||
|
StateSubscriptions.instance.saveSubscription(this);
|
||||||
|
}
|
||||||
|
|
||||||
fun updateChannel(channel: IPlatformChannel) {
|
fun updateChannel(channel: IPlatformChannel) {
|
||||||
this.channel = SerializedChannel.fromChannel(channel);
|
this.channel = SerializedChannel.fromChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updatePlayback(content: IPlatformContentDetails, seconds: Int) {
|
||||||
|
playbackSeconds += seconds;
|
||||||
|
}
|
||||||
|
fun addPlaybackView() {
|
||||||
|
playbackViews += 1;
|
||||||
|
}
|
||||||
|
|
||||||
fun updateSubscriptionState(type: String, initialPage: List<IPlatformContent>) {
|
fun updateSubscriptionState(type: String, initialPage: List<IPlatformContent>) {
|
||||||
val interval: Int;
|
val interval: Int;
|
||||||
val mostRecent: OffsetDateTime?;
|
val mostRecent: OffsetDateTime?;
|
||||||
|
@ -456,8 +456,9 @@ class StateApp {
|
|||||||
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
val isRateLimitReached = !subRequestCounts.any { clientCount -> clientCount.key.config.subscriptionRateLimit?.let { rateLimit -> clientCount.value > rateLimit } == true };
|
||||||
if (isRateLimitReached) {
|
if (isRateLimitReached) {
|
||||||
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
Logger.w(TAG, "Subscriptions request on boot, request counts:\n${reqCountStr}");
|
||||||
delay(8000);
|
delay(5000);
|
||||||
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
if(StateSubscriptions.instance.getOldestUpdateTime().getNowDiffMinutes() > 5)
|
||||||
|
StateSubscriptions.instance.updateSubscriptionFeed(scope, false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
|
Logger.w(TAG, "Too many subscription requests required:\n${reqCountStr}");
|
||||||
|
@ -762,7 +762,7 @@ class StatePlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(hasChanges)
|
if(hasChanges)
|
||||||
StateSubscriptions.instance.saveSubscription(sub);
|
sub.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
return pagerResult;
|
return pagerResult;
|
||||||
|
@ -32,6 +32,7 @@ import com.futo.platformplayer.stores.v2.ManagedStore
|
|||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithm
|
||||||
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
import com.futo.platformplayer.subscription.SubscriptionFetchAlgorithms
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import java.time.OffsetDateTime
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import java.util.concurrent.ForkJoinPool
|
import java.util.concurrent.ForkJoinPool
|
||||||
import java.util.concurrent.ForkJoinTask
|
import java.util.concurrent.ForkJoinTask
|
||||||
@ -74,6 +75,9 @@ class StateSubscriptions {
|
|||||||
|
|
||||||
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
val onSubscriptionsChanged = Event2<List<Subscription>, Boolean>();
|
||||||
|
|
||||||
|
fun getOldestUpdateTime(): OffsetDateTime {
|
||||||
|
return getSubscriptions().minOf { it.lastVideoUpdate };
|
||||||
|
}
|
||||||
fun getGlobalSubscriptionProgress(): Pair<Int, Int> {
|
fun getGlobalSubscriptionProgress(): Pair<Int, Int> {
|
||||||
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
return Pair(_lastGlobalSubscriptionProgress, _lastGlobalSubscriptionTotal);
|
||||||
}
|
}
|
||||||
@ -170,6 +174,9 @@ class StateSubscriptions {
|
|||||||
fun saveSubscription(sub: Subscription) {
|
fun saveSubscription(sub: Subscription) {
|
||||||
_subscriptions.save(sub, false, true);
|
_subscriptions.save(sub, false, true);
|
||||||
}
|
}
|
||||||
|
fun saveSubscriptionAsync(sub: Subscription) {
|
||||||
|
_subscriptions.saveAsync(sub, false, true);
|
||||||
|
}
|
||||||
fun getSubscriptionCount(): Int {
|
fun getSubscriptionCount(): Int {
|
||||||
synchronized(_subscriptions) {
|
synchronized(_subscriptions) {
|
||||||
return _subscriptions.getItems().size;
|
return _subscriptions.getItems().size;
|
||||||
@ -242,7 +249,7 @@ class StateSubscriptions {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val usePolycentric = false;
|
val usePolycentric = true;
|
||||||
val subUrls = getSubscriptions().associateWith {
|
val subUrls = getSubscriptions().associateWith {
|
||||||
if(usePolycentric)
|
if(usePolycentric)
|
||||||
StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id);
|
StatePolycentric.instance.getChannelUrls(it.channel.url, it.channel.id);
|
||||||
|
@ -227,18 +227,18 @@ class ManagedStore<T>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun saveAsync(obj: T, withReconstruction: Boolean = false) {
|
fun saveAsync(obj: T, withReconstruction: Boolean = false, onlyExisting: Boolean = false) {
|
||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
if(scope != null)
|
if(scope != null)
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
save(obj, withReconstruction);
|
save(obj, withReconstruction, onlyExisting);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to save.", e);
|
Logger.e(TAG, "Failed to save.", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
else
|
else
|
||||||
save(obj, withReconstruction);
|
save(obj, withReconstruction, onlyExisting);
|
||||||
}
|
}
|
||||||
fun saveAllAsync(objs: List<T>, withReconstruction: Boolean = false) {
|
fun saveAllAsync(objs: List<T>, withReconstruction: Boolean = false) {
|
||||||
val scope = StateApp.instance.scopeOrNull;
|
val scope = StateApp.instance.scopeOrNull;
|
||||||
|
@ -54,7 +54,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
val clientTaskCount = clientTasks.value.filter { !it.fromCache }.size;
|
||||||
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
val clientCacheCount = clientTasks.value.size - clientTaskCount;
|
||||||
if(clientCacheCount > 0) {
|
if(clientCacheCount > 0) {
|
||||||
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels.")
|
UIDialogs.toast("[${clientTasks.key.name}] only updating ${clientTaskCount} most urgent channels. (${clientCacheCount} cached)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
|
|||||||
|
|
||||||
val initialPage = pager.getResults();
|
val initialPage = pager.getResults();
|
||||||
task.sub.updateSubscriptionState(task.type, initialPage);
|
task.sub.updateSubscriptionState(task.type, initialPage);
|
||||||
StateSubscriptions.instance.saveSubscription(task.sub);
|
task.sub.save();
|
||||||
|
|
||||||
finished++;
|
finished++;
|
||||||
onProgress.emit(finished, forkTasks.size);
|
onProgress.emit(finished, forkTasks.size);
|
||||||
|
@ -14,7 +14,7 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||||||
private val _confirmationMessage: String;
|
private val _confirmationMessage: String;
|
||||||
|
|
||||||
var onClick = Event1<Subscription>();
|
var onClick = Event1<Subscription>();
|
||||||
var sortBy: Int = 0
|
var sortBy: Int = 3
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value;
|
field = value;
|
||||||
updateDataset();
|
updateDataset();
|
||||||
@ -51,6 +51,10 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
|||||||
_sortedDataset = when (sortBy) {
|
_sortedDataset = when (sortBy) {
|
||||||
0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name })
|
0 -> StateSubscriptions.instance.getSubscriptions().sortedBy({ u -> u.channel.name })
|
||||||
1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name })
|
1 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending({ u -> u.channel.name })
|
||||||
|
2 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackViews }
|
||||||
|
3 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackViews }
|
||||||
|
4 -> StateSubscriptions.instance.getSubscriptions().sortedBy { it.playbackSeconds }
|
||||||
|
5 -> StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackSeconds }
|
||||||
else -> throw IllegalStateException("Invalid sorting algorithm selected.");
|
else -> throw IllegalStateException("Invalid sorting algorithm selected.");
|
||||||
}.toList();
|
}.toList();
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
|||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.api.media.PlatformID
|
import com.futo.platformplayer.api.media.PlatformID
|
||||||
import com.futo.platformplayer.models.Subscription
|
import com.futo.platformplayer.models.Subscription
|
||||||
@ -17,6 +18,8 @@ import com.futo.platformplayer.constructs.Event1
|
|||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
|
import com.futo.platformplayer.toHumanBytesSpeed
|
||||||
|
import com.futo.platformplayer.toHumanTimeIndicator
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.platform.PlatformIndicator
|
import com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
@ -27,11 +30,12 @@ class SubscriptionViewHolder : ViewHolder {
|
|||||||
private val _creatorThumbnail: CreatorThumbnail;
|
private val _creatorThumbnail: CreatorThumbnail;
|
||||||
private val _buttonTrash: ImageButton;
|
private val _buttonTrash: ImageButton;
|
||||||
private val _platformIndicator : PlatformIndicator;
|
private val _platformIndicator : PlatformIndicator;
|
||||||
|
private val _textMeta: TextView;
|
||||||
|
|
||||||
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
private val _taskLoadProfile = TaskHandler<PlatformID, PolycentricCache.CachedPolycentricProfile?>(
|
||||||
StateApp.instance.scopeGetter,
|
StateApp.instance.scopeGetter,
|
||||||
{ PolycentricCache.instance.getProfileAsync(it) })
|
{ PolycentricCache.instance.getProfileAsync(it) })
|
||||||
.success { it -> onProfileLoaded(it, true) }
|
.success { it -> onProfileLoaded(null, it, true) }
|
||||||
.exception<Throwable> {
|
.exception<Throwable> {
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
Logger.w(TAG, "Failed to load profile.", it);
|
||||||
};
|
};
|
||||||
@ -45,6 +49,7 @@ class SubscriptionViewHolder : ViewHolder {
|
|||||||
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_subscription, viewGroup, false)) {
|
constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_subscription, viewGroup, false)) {
|
||||||
_layoutSubscription = itemView.findViewById(R.id.layout_subscription);
|
_layoutSubscription = itemView.findViewById(R.id.layout_subscription);
|
||||||
_textName = itemView.findViewById(R.id.text_name);
|
_textName = itemView.findViewById(R.id.text_name);
|
||||||
|
_textMeta = itemView.findViewById(R.id.text_meta);
|
||||||
_creatorThumbnail = itemView.findViewById(R.id.creator_thumbnail);
|
_creatorThumbnail = itemView.findViewById(R.id.creator_thumbnail);
|
||||||
_buttonTrash = itemView.findViewById(R.id.button_trash);
|
_buttonTrash = itemView.findViewById(R.id.button_trash);
|
||||||
_platformIndicator = itemView.findViewById(R.id.platform);
|
_platformIndicator = itemView.findViewById(R.id.platform);
|
||||||
@ -68,17 +73,18 @@ class SubscriptionViewHolder : ViewHolder {
|
|||||||
|
|
||||||
val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
|
val cachedProfile = PolycentricCache.instance.getCachedProfile(sub.channel.url, true);
|
||||||
if (cachedProfile != null) {
|
if (cachedProfile != null) {
|
||||||
onProfileLoaded(cachedProfile, false);
|
onProfileLoaded(sub, cachedProfile, false);
|
||||||
} else {
|
} else {
|
||||||
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
_creatorThumbnail.setThumbnail(sub.channel.thumbnail, false);
|
||||||
_taskLoadProfile.run(sub.channel.id);
|
_taskLoadProfile.run(sub.channel.id);
|
||||||
_textName.text = sub.channel.name;
|
_textName.text = sub.channel.name;
|
||||||
|
bindViewMetrics(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
_platformIndicator.setPlatformFromClientID(sub.channel.id.pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProfileLoaded(cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
private fun onProfileLoaded(sub: Subscription?, cachedPolycentricProfile: PolycentricCache.CachedPolycentricProfile?, animate: Boolean) {
|
||||||
val dp_46 = 46.dp(itemView.context.resources);
|
val dp_46 = 46.dp(itemView.context.resources);
|
||||||
val profile = cachedPolycentricProfile?.profile;
|
val profile = cachedPolycentricProfile?.profile;
|
||||||
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
|
val avatar = profile?.systemState?.avatar?.selectBestImage(dp_46 * dp_46)
|
||||||
@ -94,6 +100,19 @@ class SubscriptionViewHolder : ViewHolder {
|
|||||||
if (profile != null) {
|
if (profile != null) {
|
||||||
_textName.text = profile.systemState.username;
|
_textName.text = profile.systemState.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(sub != null)
|
||||||
|
bindViewMetrics(sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bindViewMetrics(sub: Subscription?) {
|
||||||
|
if(sub == null || !Settings.instance.subscriptions.showWatchMetrics)
|
||||||
|
_textMeta.text = "";
|
||||||
|
else
|
||||||
|
_textMeta.text = listOf(
|
||||||
|
if(sub.playbackViews > 0) "${sub.playbackViews} view" + (if(sub.playbackViews > 1) "s" else "") else null,
|
||||||
|
if(sub.playbackSeconds > 0) sub.playbackSeconds.toHumanTimeIndicator() else null
|
||||||
|
).filterNotNull().joinToString(" · ");
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -25,7 +25,7 @@ class SubscriptionBar : LinearLayout {
|
|||||||
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) {
|
||||||
inflate(context, R.layout.view_subscription_bar, this);
|
inflate(context, R.layout.view_subscription_bar, this);
|
||||||
|
|
||||||
val subscriptions = StateSubscriptions.instance.getSubscriptions();
|
val subscriptions = StateSubscriptions.instance.getSubscriptions().sortedByDescending { it.playbackViews };
|
||||||
_adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) {
|
_adapterView = findViewById<RecyclerView>(R.id.recycler_creators).asAny(subscriptions, orientation = RecyclerView.HORIZONTAL) {
|
||||||
it.onClick.subscribe { c ->
|
it.onClick.subscribe { c ->
|
||||||
onClickChannel.emit(c.channel);
|
onClickChannel.emit(c.channel);
|
||||||
|
@ -35,10 +35,25 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:ellipsize="end" />
|
android:ellipsize="end" />
|
||||||
|
|
||||||
<com.futo.platformplayer.views.platform.PlatformIndicator
|
<LinearLayout
|
||||||
android:id="@+id/platform"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="25dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="25dp" />
|
android:orientation="horizontal">
|
||||||
|
<com.futo.platformplayer.views.platform.PlatformIndicator
|
||||||
|
android:id="@+id/platform"
|
||||||
|
android:layout_width="25dp"
|
||||||
|
android:layout_height="25dp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_meta"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="11dp"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:fontFamily="@font/inter_light"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:layout_marginLeft="5dp"
|
||||||
|
android:text="Testing " />
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
@ -339,6 +339,10 @@
|
|||||||
<string name="submit_logs">Submit logs</string>
|
<string name="submit_logs">Submit logs</string>
|
||||||
<string name="submit_logs_to_help_us_narrow_down_issues">Submit logs to help us narrow down issues</string>
|
<string name="submit_logs_to_help_us_narrow_down_issues">Submit logs to help us narrow down issues</string>
|
||||||
<string name="subscription_concurrency">Subscription Concurrency</string>
|
<string name="subscription_concurrency">Subscription Concurrency</string>
|
||||||
|
<string name="track_playtime_locally">Track Playtime Locally</string>
|
||||||
|
<string name="track_playtime_locally_description">Locally track playtime of subscriptions, used for subscriptions ordering and local creator recommendations.</string>
|
||||||
|
<string name="show_watch_metrics">Show Watch Metrics</string>
|
||||||
|
<string name="show_watch_metrics_description">Shows the watch time and views of each creator in the creators tab</string>
|
||||||
<string name="this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees">This prevents the device from rotating within the given amount of degrees</string>
|
<string name="this_prevents_the_device_from_rotating_within_the_given_amount_of_degrees">This prevents the device from rotating within the given amount of degrees</string>
|
||||||
<string name="use_the_live_chat_web_window_when_available_over_native_implementation">Use the live chat web window when available over native implementation</string>
|
<string name="use_the_live_chat_web_window_when_available_over_native_implementation">Use the live chat web window when available over native implementation</string>
|
||||||
<string name="version_code">Version Code</string>
|
<string name="version_code">Version Code</string>
|
||||||
@ -700,6 +704,10 @@
|
|||||||
<string-array name="subscriptions_sortby_array">
|
<string-array name="subscriptions_sortby_array">
|
||||||
<item>Name Ascending</item>
|
<item>Name Ascending</item>
|
||||||
<item>Name Descending</item>
|
<item>Name Descending</item>
|
||||||
|
<item>Views Ascending</item>
|
||||||
|
<item>Views Descending</item>
|
||||||
|
<item>Watchtime Ascending</item>
|
||||||
|
<item>Watchtime Descending</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="feed_style">
|
<string-array name="feed_style">
|
||||||
<item>Preview</item>
|
<item>Preview</item>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user