diff --git a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt index d932fca71..231741bf3 100644 --- a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt +++ b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt @@ -5,6 +5,7 @@ import com.github.libretube.R import com.github.libretube.constants.PreferenceKeys import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.repo.AccountSubscriptionsRepository +import com.github.libretube.repo.FeedProgress import com.github.libretube.repo.FeedRepository import com.github.libretube.repo.LocalFeedRepository import com.github.libretube.repo.LocalSubscriptionsRepository @@ -22,23 +23,32 @@ object SubscriptionHelper { const val GET_SUBSCRIPTIONS_LIMIT = 100 private val token get() = PreferenceHelper.getToken() - private val subscriptionsRepository: SubscriptionsRepository get() = when { - token.isNotEmpty() -> AccountSubscriptionsRepository() - else -> LocalSubscriptionsRepository() - } - private val feedRepository: FeedRepository get() = when { - PreferenceHelper.getBoolean(PreferenceKeys.LOCAL_FEED_EXTRACTION, false) -> LocalFeedRepository() - token.isNotEmpty() -> PipedAccountFeedRepository() - else -> PipedNoAccountFeedRepository() - } + private val subscriptionsRepository: SubscriptionsRepository + get() = when { + token.isNotEmpty() -> AccountSubscriptionsRepository() + else -> LocalSubscriptionsRepository() + } + private val feedRepository: FeedRepository + get() = when { + PreferenceHelper.getBoolean( + PreferenceKeys.LOCAL_FEED_EXTRACTION, + false + ) -> LocalFeedRepository() + + token.isNotEmpty() -> PipedAccountFeedRepository() + else -> PipedNoAccountFeedRepository() + } suspend fun subscribe(channelId: String) = subscriptionsRepository.subscribe(channelId) suspend fun unsubscribe(channelId: String) = subscriptionsRepository.unsubscribe(channelId) suspend fun isSubscribed(channelId: String) = subscriptionsRepository.isSubscribed(channelId) - suspend fun importSubscriptions(newChannels: List) = subscriptionsRepository.importSubscriptions(newChannels) + suspend fun importSubscriptions(newChannels: List) = + subscriptionsRepository.importSubscriptions(newChannels) + suspend fun getSubscriptions() = subscriptionsRepository.getSubscriptions() 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( context: Context, diff --git a/app/src/main/java/com/github/libretube/repo/FeedRepository.kt b/app/src/main/java/com/github/libretube/repo/FeedRepository.kt index d679a7b5a..d08dec06c 100644 --- a/app/src/main/java/com/github/libretube/repo/FeedRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/FeedRepository.kt @@ -2,6 +2,14 @@ package com.github.libretube.repo import com.github.libretube.api.obj.StreamItem +data class FeedProgress( + val currentProgress: Int, + val total: Int +) + interface FeedRepository { - suspend fun getFeed(forceRefresh: Boolean): List + suspend fun getFeed( + forceRefresh: Boolean, + onProgressUpdate: (FeedProgress) -> Unit + ): List } \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt b/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt index 5bf8679f8..d5e9ab40e 100644 --- a/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt @@ -13,7 +13,9 @@ import com.github.libretube.extensions.toID import com.github.libretube.helpers.NewPipeExtractorInstance import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs @@ -33,7 +35,10 @@ class LocalFeedRepository : FeedRepository { if (filter.isEnabled) tab else null }.toTypedArray() - override suspend fun getFeed(forceRefresh: Boolean): List { + override suspend fun getFeed( + forceRefresh: Boolean, + onProgressUpdate: (FeedProgress) -> Unit + ): List { val nowMillis = Instant.now().toEpochMilli() val minimumDateMillis = nowMillis - Duration.ofDays(MAX_FEED_AGE_DAYS).toMillis() @@ -58,21 +63,31 @@ class LocalFeedRepository : FeedRepository { } DatabaseHolder.Database.feedDao().cleanUpOlderThan(minimumDateMillis) - refreshFeed(channelIds, minimumDateMillis) + refreshFeed(channelIds, minimumDateMillis, onProgressUpdate) PreferenceHelper.putLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, nowMillis) return DatabaseHolder.Database.feedDao().getAll().map(SubscriptionsFeedItem::toStreamItem) } - private suspend fun refreshFeed(channelIds: List, minimumDateMillis: Long) { - val extractionCount = AtomicInteger() + private suspend fun refreshFeed( + channelIds: List, + 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)) { // add a delay after each BATCH_SIZE amount of visited channels - val count = extractionCount.get(); + val count = chunkedExtractionCount.get(); if (count >= BATCH_SIZE) { delay(BATCH_DELAY.random()) - extractionCount.set(0) + chunkedExtractionCount.set(0) } val collectedFeedItems = channelIdChunk.parallelMap { channelId -> @@ -82,7 +97,12 @@ class LocalFeedRepository : FeedRepository { Log.e(channelId, e.stackTraceToString()) null } finally { - extractionCount.incrementAndGet(); + chunkedExtractionCount.incrementAndGet() + val currentProgress = totalExtractionCount.incrementAndGet() + + withContext(Dispatchers.Main) { + onProgressUpdate(FeedProgress(currentProgress, channelIds.size)) + } } }.filterNotNull().flatten().map(StreamItem::toFeedItem) @@ -133,10 +153,12 @@ class LocalFeedRepository : FeedRepository { companion object { private const val CHUNK_SIZE = 2 + /** * Maximum amount of feeds that should be fetched together, before a delay should be applied. */ private const val BATCH_SIZE = 50 + /** * Millisecond delay between two consecutive batches to avoid throttling. */ diff --git a/app/src/main/java/com/github/libretube/repo/PipedAccountFeedRepository.kt b/app/src/main/java/com/github/libretube/repo/PipedAccountFeedRepository.kt index 94cd28c7f..7dd04a6d8 100644 --- a/app/src/main/java/com/github/libretube/repo/PipedAccountFeedRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/PipedAccountFeedRepository.kt @@ -4,8 +4,11 @@ import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.obj.StreamItem import com.github.libretube.helpers.PreferenceHelper -class PipedAccountFeedRepository: FeedRepository { - override suspend fun getFeed(forceRefresh: Boolean): List { +class PipedAccountFeedRepository : FeedRepository { + override suspend fun getFeed( + forceRefresh: Boolean, + onProgressUpdate: (FeedProgress) -> Unit + ): List { val token = PreferenceHelper.getToken() return RetrofitInstance.authApi.getFeed(token) diff --git a/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt b/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt index 29f48af86..5fc75cae8 100644 --- a/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt +++ b/app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt @@ -5,8 +5,11 @@ import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper.GET_SUBSCRIPTIONS_LIMIT import com.github.libretube.api.obj.StreamItem -class PipedNoAccountFeedRepository: FeedRepository { - override suspend fun getFeed(forceRefresh: Boolean): List { +class PipedNoAccountFeedRepository : FeedRepository { + override suspend fun getFeed( + forceRefresh: Boolean, + onProgressUpdate: (FeedProgress) -> Unit + ): List { val channelIds = SubscriptionHelper.getSubscriptionChannelIds() return when { diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt index d5a7d877d..c81252971 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt @@ -95,6 +95,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub _binding?.subFeed?.layoutManager = VideosAdapter.getLayout(requireContext(), gridItems) } + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { _binding = FragmentSubscriptionsBinding.bind(view) super.onViewCreated(view, savedInstanceState) @@ -150,6 +151,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub 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 { viewModel.fetchSubscriptions(requireContext()) viewModel.fetchFeed(requireContext(), forceRefresh = true) diff --git a/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt index f5aa3c75d..1ec92be6a 100644 --- a/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt @@ -13,6 +13,7 @@ import com.github.libretube.extensions.TAG import com.github.libretube.extensions.toID import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.helpers.PreferenceHelper +import com.github.libretube.repo.FeedProgress import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -20,11 +21,14 @@ class SubscriptionsViewModel : ViewModel() { var videoFeed = MutableLiveData?>() var subscriptions = MutableLiveData?>() + val feedProgress = MutableLiveData() fun fetchFeed(context: Context, forceRefresh: Boolean) { viewModelScope.launch(Dispatchers.IO) { val videoFeed = try { - SubscriptionHelper.getFeed(forceRefresh = forceRefresh) + SubscriptionHelper.getFeed(forceRefresh = forceRefresh) { feedProgress -> + this@SubscriptionsViewModel.feedProgress.postValue(feedProgress) + } } catch (e: Exception) { context.toastFromMainDispatcher(R.string.server_error) Log.e(TAG(), e.toString()) diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml index e56aa8c0b..341b7b69b 100644 --- a/app/src/main/res/layout/fragment_subscriptions.xml +++ b/app/src/main/res/layout/fragment_subscriptions.xml @@ -145,6 +145,45 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75f90ce7e..9253d5048 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -531,6 +531,7 @@ Local feed extraction Directly fetch the feed from YouTube. This may be significantly slower. Show upcoming videos + Updating feed … Download Service