Skip to content

Commit 9d4a2d7

Browse files
authored
Merge pull request libre-tube#7145 from Bnyro/master
feat: add progress indicator for local feed extraction
2 parents ca58a2f + dc0be0d commit 9d4a2d7

9 files changed

Lines changed: 126 additions & 24 deletions

File tree

app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.github.libretube.R
55
import com.github.libretube.constants.PreferenceKeys
66
import com.github.libretube.helpers.PreferenceHelper
77
import com.github.libretube.repo.AccountSubscriptionsRepository
8+
import com.github.libretube.repo.FeedProgress
89
import com.github.libretube.repo.FeedRepository
910
import com.github.libretube.repo.LocalFeedRepository
1011
import com.github.libretube.repo.LocalSubscriptionsRepository
@@ -22,23 +23,32 @@ object SubscriptionHelper {
2223
const val GET_SUBSCRIPTIONS_LIMIT = 100
2324

2425
private val token get() = PreferenceHelper.getToken()
25-
private val subscriptionsRepository: SubscriptionsRepository get() = when {
26-
token.isNotEmpty() -> AccountSubscriptionsRepository()
27-
else -> LocalSubscriptionsRepository()
28-
}
29-
private val feedRepository: FeedRepository get() = when {
30-
PreferenceHelper.getBoolean(PreferenceKeys.LOCAL_FEED_EXTRACTION, false) -> LocalFeedRepository()
31-
token.isNotEmpty() -> PipedAccountFeedRepository()
32-
else -> PipedNoAccountFeedRepository()
33-
}
26+
private val subscriptionsRepository: SubscriptionsRepository
27+
get() = when {
28+
token.isNotEmpty() -> AccountSubscriptionsRepository()
29+
else -> LocalSubscriptionsRepository()
30+
}
31+
private val feedRepository: FeedRepository
32+
get() = when {
33+
PreferenceHelper.getBoolean(
34+
PreferenceKeys.LOCAL_FEED_EXTRACTION,
35+
false
36+
) -> LocalFeedRepository()
37+
38+
token.isNotEmpty() -> PipedAccountFeedRepository()
39+
else -> PipedNoAccountFeedRepository()
40+
}
3441

3542
suspend fun subscribe(channelId: String) = subscriptionsRepository.subscribe(channelId)
3643
suspend fun unsubscribe(channelId: String) = subscriptionsRepository.unsubscribe(channelId)
3744
suspend fun isSubscribed(channelId: String) = subscriptionsRepository.isSubscribed(channelId)
38-
suspend fun importSubscriptions(newChannels: List<String>) = subscriptionsRepository.importSubscriptions(newChannels)
45+
suspend fun importSubscriptions(newChannels: List<String>) =
46+
subscriptionsRepository.importSubscriptions(newChannels)
47+
3948
suspend fun getSubscriptions() = subscriptionsRepository.getSubscriptions()
4049
suspend fun getSubscriptionChannelIds() = subscriptionsRepository.getSubscriptionChannelIds()
41-
suspend fun getFeed(forceRefresh: Boolean) = feedRepository.getFeed(forceRefresh)
50+
suspend fun getFeed(forceRefresh: Boolean, onProgressUpdate: (FeedProgress) -> Unit = {}) =
51+
feedRepository.getFeed(forceRefresh, onProgressUpdate)
4252

4353
fun handleUnsubscribe(
4454
context: Context,

app/src/main/java/com/github/libretube/repo/FeedRepository.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ package com.github.libretube.repo
22

33
import com.github.libretube.api.obj.StreamItem
44

5+
data class FeedProgress(
6+
val currentProgress: Int,
7+
val total: Int
8+
)
9+
510
interface FeedRepository {
6-
suspend fun getFeed(forceRefresh: Boolean): List<StreamItem>
11+
suspend fun getFeed(
12+
forceRefresh: Boolean,
13+
onProgressUpdate: (FeedProgress) -> Unit
14+
): List<StreamItem>
715
}

app/src/main/java/com/github/libretube/repo/LocalFeedRepository.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import com.github.libretube.extensions.toID
1313
import com.github.libretube.helpers.NewPipeExtractorInstance
1414
import com.github.libretube.helpers.PreferenceHelper
1515
import com.github.libretube.ui.dialogs.ShareDialog.Companion.YOUTUBE_FRONTEND_URL
16+
import kotlinx.coroutines.Dispatchers
1617
import kotlinx.coroutines.delay
18+
import kotlinx.coroutines.withContext
1719
import org.schabi.newpipe.extractor.channel.ChannelInfo
1820
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
1921
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs
@@ -33,7 +35,10 @@ class LocalFeedRepository : FeedRepository {
3335
if (filter.isEnabled) tab else null
3436
}.toTypedArray()
3537

36-
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
38+
override suspend fun getFeed(
39+
forceRefresh: Boolean,
40+
onProgressUpdate: (FeedProgress) -> Unit
41+
): List<StreamItem> {
3742
val nowMillis = Instant.now().toEpochMilli()
3843
val minimumDateMillis = nowMillis - Duration.ofDays(MAX_FEED_AGE_DAYS).toMillis()
3944

@@ -58,21 +63,31 @@ class LocalFeedRepository : FeedRepository {
5863
}
5964

6065
DatabaseHolder.Database.feedDao().cleanUpOlderThan(minimumDateMillis)
61-
refreshFeed(channelIds, minimumDateMillis)
66+
refreshFeed(channelIds, minimumDateMillis, onProgressUpdate)
6267
PreferenceHelper.putLong(PreferenceKeys.LAST_FEED_REFRESH_TIMESTAMP_MILLIS, nowMillis)
6368

6469
return DatabaseHolder.Database.feedDao().getAll().map(SubscriptionsFeedItem::toStreamItem)
6570
}
6671

67-
private suspend fun refreshFeed(channelIds: List<String>, minimumDateMillis: Long) {
68-
val extractionCount = AtomicInteger()
72+
private suspend fun refreshFeed(
73+
channelIds: List<String>,
74+
minimumDateMillis: Long,
75+
onProgressUpdate: (FeedProgress) -> Unit
76+
) {
77+
if (channelIds.isEmpty()) return
78+
79+
val totalExtractionCount = AtomicInteger()
80+
val chunkedExtractionCount = AtomicInteger()
81+
withContext(Dispatchers.Main) {
82+
onProgressUpdate(FeedProgress(0, channelIds.size))
83+
}
6984

7085
for (channelIdChunk in channelIds.chunked(CHUNK_SIZE)) {
7186
// add a delay after each BATCH_SIZE amount of visited channels
72-
val count = extractionCount.get();
87+
val count = chunkedExtractionCount.get();
7388
if (count >= BATCH_SIZE) {
7489
delay(BATCH_DELAY.random())
75-
extractionCount.set(0)
90+
chunkedExtractionCount.set(0)
7691
}
7792

7893
val collectedFeedItems = channelIdChunk.parallelMap { channelId ->
@@ -82,7 +97,12 @@ class LocalFeedRepository : FeedRepository {
8297
Log.e(channelId, e.stackTraceToString())
8398
null
8499
} finally {
85-
extractionCount.incrementAndGet();
100+
chunkedExtractionCount.incrementAndGet()
101+
val currentProgress = totalExtractionCount.incrementAndGet()
102+
103+
withContext(Dispatchers.Main) {
104+
onProgressUpdate(FeedProgress(currentProgress, channelIds.size))
105+
}
86106
}
87107
}.filterNotNull().flatten().map(StreamItem::toFeedItem)
88108

@@ -133,10 +153,12 @@ class LocalFeedRepository : FeedRepository {
133153

134154
companion object {
135155
private const val CHUNK_SIZE = 2
156+
136157
/**
137158
* Maximum amount of feeds that should be fetched together, before a delay should be applied.
138159
*/
139160
private const val BATCH_SIZE = 50
161+
140162
/**
141163
* Millisecond delay between two consecutive batches to avoid throttling.
142164
*/

app/src/main/java/com/github/libretube/repo/PipedAccountFeedRepository.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import com.github.libretube.api.RetrofitInstance
44
import com.github.libretube.api.obj.StreamItem
55
import com.github.libretube.helpers.PreferenceHelper
66

7-
class PipedAccountFeedRepository: FeedRepository {
8-
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
7+
class PipedAccountFeedRepository : FeedRepository {
8+
override suspend fun getFeed(
9+
forceRefresh: Boolean,
10+
onProgressUpdate: (FeedProgress) -> Unit
11+
): List<StreamItem> {
912
val token = PreferenceHelper.getToken()
1013

1114
return RetrofitInstance.authApi.getFeed(token)

app/src/main/java/com/github/libretube/repo/PipedNoAccountFeedRepository.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import com.github.libretube.api.SubscriptionHelper
55
import com.github.libretube.api.SubscriptionHelper.GET_SUBSCRIPTIONS_LIMIT
66
import com.github.libretube.api.obj.StreamItem
77

8-
class PipedNoAccountFeedRepository: FeedRepository {
9-
override suspend fun getFeed(forceRefresh: Boolean): List<StreamItem> {
8+
class PipedNoAccountFeedRepository : FeedRepository {
9+
override suspend fun getFeed(
10+
forceRefresh: Boolean,
11+
onProgressUpdate: (FeedProgress) -> Unit
12+
): List<StreamItem> {
1013
val channelIds = SubscriptionHelper.getSubscriptionChannelIds()
1114

1215
return when {

app/src/main/java/com/github/libretube/ui/fragments/SubscriptionsFragment.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
9595
_binding?.subFeed?.layoutManager = VideosAdapter.getLayout(requireContext(), gridItems)
9696
}
9797

98+
@SuppressLint("SetTextI18n")
9899
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
99100
_binding = FragmentSubscriptionsBinding.bind(view)
100101
super.onViewCreated(view, savedInstanceState)
@@ -150,6 +151,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
150151
if (isCurrentTabSubChannels && it != null) showSubscriptions()
151152
}
152153

154+
viewModel.feedProgress.observe(viewLifecycleOwner) { progress ->
155+
if (progress == null || progress.currentProgress == progress.total) {
156+
binding.feedProgressContainer.isGone = true
157+
} else {
158+
binding.feedProgressContainer.isVisible = true
159+
binding.feedProgressText.text = "${progress.currentProgress}/${progress.total}"
160+
binding.feedProgressBar.max = progress.total
161+
binding.feedProgressBar.progress = progress.currentProgress
162+
}
163+
}
164+
153165
binding.subRefresh.setOnRefreshListener {
154166
viewModel.fetchSubscriptions(requireContext())
155167
viewModel.fetchFeed(requireContext(), forceRefresh = true)

app/src/main/java/com/github/libretube/ui/models/SubscriptionsViewModel.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,22 @@ import com.github.libretube.extensions.TAG
1313
import com.github.libretube.extensions.toID
1414
import com.github.libretube.extensions.toastFromMainDispatcher
1515
import com.github.libretube.helpers.PreferenceHelper
16+
import com.github.libretube.repo.FeedProgress
1617
import kotlinx.coroutines.Dispatchers
1718
import kotlinx.coroutines.launch
1819

1920
class SubscriptionsViewModel : ViewModel() {
2021
var videoFeed = MutableLiveData<List<StreamItem>?>()
2122

2223
var subscriptions = MutableLiveData<List<Subscription>?>()
24+
val feedProgress = MutableLiveData<FeedProgress?>()
2325

2426
fun fetchFeed(context: Context, forceRefresh: Boolean) {
2527
viewModelScope.launch(Dispatchers.IO) {
2628
val videoFeed = try {
27-
SubscriptionHelper.getFeed(forceRefresh = forceRefresh)
29+
SubscriptionHelper.getFeed(forceRefresh = forceRefresh) { feedProgress ->
30+
this@SubscriptionsViewModel.feedProgress.postValue(feedProgress)
31+
}
2832
} catch (e: Exception) {
2933
context.toastFromMainDispatcher(R.string.server_error)
3034
Log.e(TAG(), e.toString())

app/src/main/res/layout/fragment_subscriptions.xml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,45 @@
145145

146146
</HorizontalScrollView>
147147

148+
<LinearLayout
149+
android:visibility="gone"
150+
android:id="@+id/feed_progress_container"
151+
android:layout_width="match_parent"
152+
android:layout_height="wrap_content"
153+
android:layout_marginBottom="10dp"
154+
android:layout_marginTop="6dp"
155+
android:layout_marginHorizontal="12dp"
156+
android:orientation="vertical"
157+
tools:visibility="visible">
158+
159+
<com.google.android.material.progressindicator.LinearProgressIndicator
160+
android:id="@+id/feed_progress_bar"
161+
android:layout_width="match_parent"
162+
android:layout_height="wrap_content"
163+
android:layout_marginBottom="2dp"
164+
tools:progress="70" />
165+
166+
<LinearLayout
167+
android:layout_width="match_parent"
168+
android:layout_height="wrap_content"
169+
android:orientation="horizontal">
170+
171+
<TextView
172+
android:layout_width="0dp"
173+
android:layout_height="wrap_content"
174+
android:layout_weight="1"
175+
android:text="@string/updating_feed" />
176+
177+
<TextView
178+
android:id="@+id/feed_progress_text"
179+
android:layout_width="wrap_content"
180+
android:layout_height="wrap_content"
181+
tools:text="5/20" />
182+
183+
</LinearLayout>
184+
185+
</LinearLayout>
186+
148187
</LinearLayout>
149188

150189
</com.google.android.material.appbar.CollapsingToolbarLayout>

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@
531531
<string name="local_feed_extraction">Local feed extraction</string>
532532
<string name="local_feed_extraction_summary">Directly fetch the feed from YouTube. This may be significantly slower.</string>
533533
<string name="show_upcoming_videos">Show upcoming videos</string>
534+
<string name="updating_feed">Updating feed …</string>
534535

535536
<!-- Notification channel strings -->
536537
<string name="download_channel_name">Download Service</string>

0 commit comments

Comments
 (0)