mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 15:30:31 +05:30
Merge pull request #7145 from Bnyro/master
feat: add progress indicator for local feed extraction
This commit is contained in:
commit
9d4a2d7c7d
@ -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<String>) = subscriptionsRepository.importSubscriptions(newChannels)
|
||||
suspend fun importSubscriptions(newChannels: List<String>) =
|
||||
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,
|
||||
|
@ -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<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.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<StreamItem> {
|
||||
override suspend fun getFeed(
|
||||
forceRefresh: Boolean,
|
||||
onProgressUpdate: (FeedProgress) -> Unit
|
||||
): List<StreamItem> {
|
||||
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<String>, minimumDateMillis: Long) {
|
||||
val extractionCount = AtomicInteger()
|
||||
private suspend fun refreshFeed(
|
||||
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)) {
|
||||
// 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.
|
||||
*/
|
||||
|
@ -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<StreamItem> {
|
||||
class PipedAccountFeedRepository : FeedRepository {
|
||||
override suspend fun getFeed(
|
||||
forceRefresh: Boolean,
|
||||
onProgressUpdate: (FeedProgress) -> Unit
|
||||
): List<StreamItem> {
|
||||
val token = PreferenceHelper.getToken()
|
||||
|
||||
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.obj.StreamItem
|
||||
|
||||
class PipedNoAccountFeedRepository: FeedRepository {
|
||||
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
|
||||
class PipedNoAccountFeedRepository : FeedRepository {
|
||||
override suspend fun getFeed(
|
||||
forceRefresh: Boolean,
|
||||
onProgressUpdate: (FeedProgress) -> Unit
|
||||
): List<StreamItem> {
|
||||
val channelIds = SubscriptionHelper.getSubscriptionChannelIds()
|
||||
|
||||
return when {
|
||||
|
@ -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)
|
||||
|
@ -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<List<StreamItem>?>()
|
||||
|
||||
var subscriptions = MutableLiveData<List<Subscription>?>()
|
||||
val feedProgress = MutableLiveData<FeedProgress?>()
|
||||
|
||||
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())
|
||||
|
@ -145,6 +145,45 @@
|
||||
|
||||
</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>
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
@ -531,6 +531,7 @@
|
||||
<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="show_upcoming_videos">Show upcoming videos</string>
|
||||
<string name="updating_feed">Updating feed …</string>
|
||||
|
||||
<!-- Notification channel strings -->
|
||||
<string name="download_channel_name">Download Service</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user