mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 07:50:31 +05:30
feat: add progress indicator for local feed extraction
This commit is contained in:
parent
ca58a2fc18
commit
dc0be0d352
@ -5,6 +5,7 @@ import com.github.libretube.R
|
|||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
import com.github.libretube.repo.AccountSubscriptionsRepository
|
import com.github.libretube.repo.AccountSubscriptionsRepository
|
||||||
|
import com.github.libretube.repo.FeedProgress
|
||||||
import com.github.libretube.repo.FeedRepository
|
import com.github.libretube.repo.FeedRepository
|
||||||
import com.github.libretube.repo.LocalFeedRepository
|
import com.github.libretube.repo.LocalFeedRepository
|
||||||
import com.github.libretube.repo.LocalSubscriptionsRepository
|
import com.github.libretube.repo.LocalSubscriptionsRepository
|
||||||
@ -22,23 +23,32 @@ object SubscriptionHelper {
|
|||||||
const val GET_SUBSCRIPTIONS_LIMIT = 100
|
const val GET_SUBSCRIPTIONS_LIMIT = 100
|
||||||
|
|
||||||
private val token get() = PreferenceHelper.getToken()
|
private val token get() = PreferenceHelper.getToken()
|
||||||
private val subscriptionsRepository: SubscriptionsRepository get() = when {
|
private val subscriptionsRepository: SubscriptionsRepository
|
||||||
token.isNotEmpty() -> AccountSubscriptionsRepository()
|
get() = when {
|
||||||
else -> LocalSubscriptionsRepository()
|
token.isNotEmpty() -> AccountSubscriptionsRepository()
|
||||||
}
|
else -> LocalSubscriptionsRepository()
|
||||||
private val feedRepository: FeedRepository get() = when {
|
}
|
||||||
PreferenceHelper.getBoolean(PreferenceKeys.LOCAL_FEED_EXTRACTION, false) -> LocalFeedRepository()
|
private val feedRepository: FeedRepository
|
||||||
token.isNotEmpty() -> PipedAccountFeedRepository()
|
get() = when {
|
||||||
else -> PipedNoAccountFeedRepository()
|
PreferenceHelper.getBoolean(
|
||||||
}
|
PreferenceKeys.LOCAL_FEED_EXTRACTION,
|
||||||
|
false
|
||||||
|
) -> LocalFeedRepository()
|
||||||
|
|
||||||
|
token.isNotEmpty() -> PipedAccountFeedRepository()
|
||||||
|
else -> PipedNoAccountFeedRepository()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun subscribe(channelId: String) = subscriptionsRepository.subscribe(channelId)
|
suspend fun subscribe(channelId: String) = subscriptionsRepository.subscribe(channelId)
|
||||||
suspend fun unsubscribe(channelId: String) = subscriptionsRepository.unsubscribe(channelId)
|
suspend fun unsubscribe(channelId: String) = subscriptionsRepository.unsubscribe(channelId)
|
||||||
suspend fun isSubscribed(channelId: String) = subscriptionsRepository.isSubscribed(channelId)
|
suspend fun isSubscribed(channelId: String) = subscriptionsRepository.isSubscribed(channelId)
|
||||||
suspend fun importSubscriptions(newChannels: List<String>) = subscriptionsRepository.importSubscriptions(newChannels)
|
suspend fun importSubscriptions(newChannels: List<String>) =
|
||||||
|
subscriptionsRepository.importSubscriptions(newChannels)
|
||||||
|
|
||||||
suspend fun getSubscriptions() = subscriptionsRepository.getSubscriptions()
|
suspend fun getSubscriptions() = subscriptionsRepository.getSubscriptions()
|
||||||
suspend fun getSubscriptionChannelIds() = subscriptionsRepository.getSubscriptionChannelIds()
|
suspend fun getSubscriptionChannelIds() = subscriptionsRepository.getSubscriptionChannelIds()
|
||||||
suspend fun getFeed(forceRefresh: Boolean) = feedRepository.getFeed(forceRefresh)
|
suspend fun getFeed(forceRefresh: Boolean, onProgressUpdate: (FeedProgress) -> Unit = {}) =
|
||||||
|
feedRepository.getFeed(forceRefresh, onProgressUpdate)
|
||||||
|
|
||||||
fun handleUnsubscribe(
|
fun handleUnsubscribe(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -2,6 +2,14 @@ package com.github.libretube.repo
|
|||||||
|
|
||||||
import com.github.libretube.api.obj.StreamItem
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
|
||||||
|
data class FeedProgress(
|
||||||
|
val currentProgress: Int,
|
||||||
|
val total: Int
|
||||||
|
)
|
||||||
|
|
||||||
interface FeedRepository {
|
interface FeedRepository {
|
||||||
suspend fun getFeed(forceRefresh: Boolean): List<StreamItem>
|
suspend fun getFeed(
|
||||||
|
forceRefresh: Boolean,
|
||||||
|
onProgressUpdate: (FeedProgress) -> Unit
|
||||||
|
): List<StreamItem>
|
||||||
}
|
}
|
@ -13,7 +13,9 @@ import com.github.libretube.extensions.toID
|
|||||||
import com.github.libretube.helpers.NewPipeExtractorInstance
|
import com.github.libretube.helpers.NewPipeExtractorInstance
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
|
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs
|
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs
|
||||||
@ -33,7 +35,10 @@ class LocalFeedRepository : FeedRepository {
|
|||||||
if (filter.isEnabled) tab else null
|
if (filter.isEnabled) tab else null
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
|
override suspend fun getFeed(
|
||||||
|
forceRefresh: Boolean,
|
||||||
|
onProgressUpdate: (FeedProgress) -> Unit
|
||||||
|
): List<StreamItem> {
|
||||||
val nowMillis = Instant.now().toEpochMilli()
|
val nowMillis = Instant.now().toEpochMilli()
|
||||||
val minimumDateMillis = nowMillis - Duration.ofDays(MAX_FEED_AGE_DAYS).toMillis()
|
val minimumDateMillis = nowMillis - Duration.ofDays(MAX_FEED_AGE_DAYS).toMillis()
|
||||||
|
|
||||||
@ -58,21 +63,31 @@ class LocalFeedRepository : FeedRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DatabaseHolder.Database.feedDao().cleanUpOlderThan(minimumDateMillis)
|
DatabaseHolder.Database.feedDao().cleanUpOlderThan(minimumDateMillis)
|
||||||
refreshFeed(channelIds, minimumDateMillis)
|
refreshFeed(channelIds, minimumDateMillis, onProgressUpdate)
|
||||||
PreferenceHelper.putLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, nowMillis)
|
PreferenceHelper.putLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, nowMillis)
|
||||||
|
|
||||||
return DatabaseHolder.Database.feedDao().getAll().map(SubscriptionsFeedItem::toStreamItem)
|
return DatabaseHolder.Database.feedDao().getAll().map(SubscriptionsFeedItem::toStreamItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun refreshFeed(channelIds: List<String>, minimumDateMillis: Long) {
|
private suspend fun refreshFeed(
|
||||||
val extractionCount = AtomicInteger()
|
channelIds: List<String>,
|
||||||
|
minimumDateMillis: Long,
|
||||||
|
onProgressUpdate: (FeedProgress) -> Unit
|
||||||
|
) {
|
||||||
|
if (channelIds.isEmpty()) return
|
||||||
|
|
||||||
|
val totalExtractionCount = AtomicInteger()
|
||||||
|
val chunkedExtractionCount = AtomicInteger()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgressUpdate(FeedProgress(0, channelIds.size))
|
||||||
|
}
|
||||||
|
|
||||||
for (channelIdChunk in channelIds.chunked(CHUNK_SIZE)) {
|
for (channelIdChunk in channelIds.chunked(CHUNK_SIZE)) {
|
||||||
// add a delay after each BATCH_SIZE amount of visited channels
|
// add a delay after each BATCH_SIZE amount of visited channels
|
||||||
val count = extractionCount.get();
|
val count = chunkedExtractionCount.get();
|
||||||
if (count >= BATCH_SIZE) {
|
if (count >= BATCH_SIZE) {
|
||||||
delay(BATCH_DELAY.random())
|
delay(BATCH_DELAY.random())
|
||||||
extractionCount.set(0)
|
chunkedExtractionCount.set(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val collectedFeedItems = channelIdChunk.parallelMap { channelId ->
|
val collectedFeedItems = channelIdChunk.parallelMap { channelId ->
|
||||||
@ -82,7 +97,12 @@ class LocalFeedRepository : FeedRepository {
|
|||||||
Log.e(channelId, e.stackTraceToString())
|
Log.e(channelId, e.stackTraceToString())
|
||||||
null
|
null
|
||||||
} finally {
|
} finally {
|
||||||
extractionCount.incrementAndGet();
|
chunkedExtractionCount.incrementAndGet()
|
||||||
|
val currentProgress = totalExtractionCount.incrementAndGet()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onProgressUpdate(FeedProgress(currentProgress, channelIds.size))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.filterNotNull().flatten().map(StreamItem::toFeedItem)
|
}.filterNotNull().flatten().map(StreamItem::toFeedItem)
|
||||||
|
|
||||||
@ -133,10 +153,12 @@ class LocalFeedRepository : FeedRepository {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CHUNK_SIZE = 2
|
private const val CHUNK_SIZE = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum amount of feeds that should be fetched together, before a delay should be applied.
|
* Maximum amount of feeds that should be fetched together, before a delay should be applied.
|
||||||
*/
|
*/
|
||||||
private const val BATCH_SIZE = 50
|
private const val BATCH_SIZE = 50
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Millisecond delay between two consecutive batches to avoid throttling.
|
* Millisecond delay between two consecutive batches to avoid throttling.
|
||||||
*/
|
*/
|
||||||
|
@ -4,8 +4,11 @@ import com.github.libretube.api.RetrofitInstance
|
|||||||
import com.github.libretube.api.obj.StreamItem
|
import com.github.libretube.api.obj.StreamItem
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
|
|
||||||
class PipedAccountFeedRepository: FeedRepository {
|
class PipedAccountFeedRepository : FeedRepository {
|
||||||
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
|
override suspend fun getFeed(
|
||||||
|
forceRefresh: Boolean,
|
||||||
|
onProgressUpdate: (FeedProgress) -> Unit
|
||||||
|
): List<StreamItem> {
|
||||||
val token = PreferenceHelper.getToken()
|
val token = PreferenceHelper.getToken()
|
||||||
|
|
||||||
return RetrofitInstance.authApi.getFeed(token)
|
return RetrofitInstance.authApi.getFeed(token)
|
||||||
|
@ -5,8 +5,11 @@ import com.github.libretube.api.SubscriptionHelper
|
|||||||
import com.github.libretube.api.SubscriptionHelper.GET_SUBSCRIPTIONS_LIMIT
|
import com.github.libretube.api.SubscriptionHelper.GET_SUBSCRIPTIONS_LIMIT
|
||||||
import com.github.libretube.api.obj.StreamItem
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
|
||||||
class PipedNoAccountFeedRepository: FeedRepository {
|
class PipedNoAccountFeedRepository : FeedRepository {
|
||||||
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
|
override suspend fun getFeed(
|
||||||
|
forceRefresh: Boolean,
|
||||||
|
onProgressUpdate: (FeedProgress) -> Unit
|
||||||
|
): List<StreamItem> {
|
||||||
val channelIds = SubscriptionHelper.getSubscriptionChannelIds()
|
val channelIds = SubscriptionHelper.getSubscriptionChannelIds()
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
|
@ -95,6 +95,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
|
|||||||
_binding?.subFeed?.layoutManager = VideosAdapter.getLayout(requireContext(), gridItems)
|
_binding?.subFeed?.layoutManager = VideosAdapter.getLayout(requireContext(), gridItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
_binding = FragmentSubscriptionsBinding.bind(view)
|
_binding = FragmentSubscriptionsBinding.bind(view)
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@ -150,6 +151,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
|
|||||||
if (isCurrentTabSubChannels && it != null) showSubscriptions()
|
if (isCurrentTabSubChannels && it != null) showSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.feedProgress.observe(viewLifecycleOwner) { progress ->
|
||||||
|
if (progress == null || progress.currentProgress == progress.total) {
|
||||||
|
binding.feedProgressContainer.isGone = true
|
||||||
|
} else {
|
||||||
|
binding.feedProgressContainer.isVisible = true
|
||||||
|
binding.feedProgressText.text = "${progress.currentProgress}/${progress.total}"
|
||||||
|
binding.feedProgressBar.max = progress.total
|
||||||
|
binding.feedProgressBar.progress = progress.currentProgress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
binding.subRefresh.setOnRefreshListener {
|
binding.subRefresh.setOnRefreshListener {
|
||||||
viewModel.fetchSubscriptions(requireContext())
|
viewModel.fetchSubscriptions(requireContext())
|
||||||
viewModel.fetchFeed(requireContext(), forceRefresh = true)
|
viewModel.fetchFeed(requireContext(), forceRefresh = true)
|
||||||
|
@ -13,6 +13,7 @@ import com.github.libretube.extensions.TAG
|
|||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
import com.github.libretube.extensions.toastFromMainDispatcher
|
import com.github.libretube.extensions.toastFromMainDispatcher
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
|
import com.github.libretube.repo.FeedProgress
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ -20,11 +21,14 @@ class SubscriptionsViewModel : ViewModel() {
|
|||||||
var videoFeed = MutableLiveData<List<StreamItem>?>()
|
var videoFeed = MutableLiveData<List<StreamItem>?>()
|
||||||
|
|
||||||
var subscriptions = MutableLiveData<List<Subscription>?>()
|
var subscriptions = MutableLiveData<List<Subscription>?>()
|
||||||
|
val feedProgress = MutableLiveData<FeedProgress?>()
|
||||||
|
|
||||||
fun fetchFeed(context: Context, forceRefresh: Boolean) {
|
fun fetchFeed(context: Context, forceRefresh: Boolean) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val videoFeed = try {
|
val videoFeed = try {
|
||||||
SubscriptionHelper.getFeed(forceRefresh = forceRefresh)
|
SubscriptionHelper.getFeed(forceRefresh = forceRefresh) { feedProgress ->
|
||||||
|
this@SubscriptionsViewModel.feedProgress.postValue(feedProgress)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
context.toastFromMainDispatcher(R.string.server_error)
|
context.toastFromMainDispatcher(R.string.server_error)
|
||||||
Log.e(TAG(), e.toString())
|
Log.e(TAG(), e.toString())
|
||||||
|
@ -145,6 +145,45 @@
|
|||||||
|
|
||||||
</HorizontalScrollView>
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:visibility="gone"
|
||||||
|
android:id="@+id/feed_progress_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="10dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginHorizontal="12dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
|
android:id="@+id/feed_progress_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
tools:progress="70" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/updating_feed" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/feed_progress_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:text="5/20" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||||
|
@ -531,6 +531,7 @@
|
|||||||
<string name="local_feed_extraction">Local feed extraction</string>
|
<string name="local_feed_extraction">Local feed extraction</string>
|
||||||
<string name="local_feed_extraction_summary">Directly fetch the feed from YouTube. This may be significantly slower.</string>
|
<string name="local_feed_extraction_summary">Directly fetch the feed from YouTube. This may be significantly slower.</string>
|
||||||
<string name="show_upcoming_videos">Show upcoming videos</string>
|
<string name="show_upcoming_videos">Show upcoming videos</string>
|
||||||
|
<string name="updating_feed">Updating feed …</string>
|
||||||
|
|
||||||
<!-- Notification channel strings -->
|
<!-- Notification channel strings -->
|
||||||
<string name="download_channel_name">Download Service</string>
|
<string name="download_channel_name">Download Service</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user