Skip to content

Commit e48b5d7

Browse files
committed
update the feed item from the when video is played. Opening a video loads its view count and updates the play time which will now reflect in the feed item as well.
1 parent d0a0b4a commit e48b5d7

File tree

8 files changed

+159
-10
lines changed

8 files changed

+159
-10
lines changed

app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.reactivex.rxjava3.core.Flowable
1111
import io.reactivex.rxjava3.core.Maybe
1212
import java.time.OffsetDateTime
1313
import org.schabi.newpipe.database.BasicDAO
14+
import org.schabi.newpipe.database.stream.StreamWithState
1415
import org.schabi.newpipe.database.stream.model.StreamEntity
1516
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
1617
import org.schabi.newpipe.extractor.stream.StreamType
@@ -30,6 +31,17 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
3031
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
3132
abstract fun getStream(serviceId: Long, url: String): Maybe<StreamEntity>
3233

34+
@Query(
35+
"""
36+
SELECT s.*, sst.progress_time
37+
FROM streams s
38+
LEFT JOIN stream_state sst ON s.uid = sst.stream_id
39+
WHERE s.url = :url AND s.service_id = :serviceId
40+
LIMIT 1
41+
"""
42+
)
43+
abstract fun getStreamWithState(serviceId: Int, url: String): Maybe<StreamWithState>
44+
3345
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
3446
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
3547

app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import androidx.core.net.toUri
4444
import androidx.core.os.postDelayed
4545
import androidx.core.view.isGone
4646
import androidx.core.view.isVisible
47+
import androidx.lifecycle.ViewModelProvider
4748
import androidx.preference.PreferenceManager
4849
import coil3.util.CoilUtils
4950
import com.evernote.android.state.State
@@ -90,6 +91,7 @@ import org.schabi.newpipe.ktx.AnimationType
9091
import org.schabi.newpipe.ktx.animate
9192
import org.schabi.newpipe.ktx.animateRotation
9293
import org.schabi.newpipe.local.dialog.PlaylistDialog
94+
import org.schabi.newpipe.local.feed.StreamUpdateViewModel
9395
import org.schabi.newpipe.local.history.HistoryRecordManager
9496
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment
9597
import org.schabi.newpipe.player.Player
@@ -203,6 +205,7 @@ class VideoDetailFragment :
203205
private var currentWorker: Disposable? = null
204206
private val disposables = CompositeDisposable()
205207
private var positionSubscriber: Disposable? = null
208+
private var streamUpdateViewModel: StreamUpdateViewModel? = null
206209

207210
/*//////////////////////////////////////////////////////////////////////////
208211
// Service management
@@ -581,6 +584,8 @@ class VideoDetailFragment :
581584
override fun initViews(rootView: View?, savedInstanceState: Bundle?) {
582585
super.initViews(rootView, savedInstanceState)
583586

587+
streamUpdateViewModel = ViewModelProvider(requireActivity())[StreamUpdateViewModel::class]
588+
584589
pageAdapter = TabAdapter(getChildFragmentManager())
585590
binding.viewPager.setAdapter(pageAdapter)
586591
binding.tabLayout.setupWithViewPager(binding.viewPager)
@@ -1531,6 +1536,9 @@ class VideoDetailFragment :
15311536
binding.detailThumbnailPlayButton.setImageResource(
15321537
if (hasVideoStreams) R.drawable.ic_play_arrow_shadow else R.drawable.ic_headset_shadow
15331538
)
1539+
1540+
// Notify FeedFragment that this stream's data (including view count) has been updated
1541+
streamUpdateViewModel?.notifyStreamInfoUpdated(info.serviceId, info.url)
15341542
}
15351543

15361544
private fun displayUploaderAsSubChannel(info: StreamInfo) {

app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class FeedDatabaseManager(context: Context) {
5454
)
5555
}
5656

57+
fun getStreamWithState(serviceId: Int, url: String): Maybe<StreamWithState> = streamTable.getStreamWithState(serviceId, url)
58+
5759
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
5860

5961
fun outdatedSubscriptionsWithNotificationMode(

app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
109109

110110
private var lastNewItemsCount = 0
111111

112+
private lateinit var streamUpdateViewModel: StreamUpdateViewModel
113+
112114
init {
113115
setHasOptionsMenu(true)
114116
}
@@ -143,6 +145,20 @@ class FeedFragment : BaseStateFragment<FeedState>() {
143145
viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
144146
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
145147

148+
// Activity-scoped ViewModel shared with VideoDetailFragment
149+
streamUpdateViewModel = ViewModelProvider(requireActivity())[StreamUpdateViewModel::class.java]
150+
151+
streamUpdateViewModel.updatedStream.observe(viewLifecycleOwner) { (serviceId, url) ->
152+
refreshFeedItem(serviceId, url)
153+
}
154+
155+
disposables.add(
156+
StreamUpdateViewModel.globalProgressBus
157+
.onBackpressureLatest()
158+
.observeOn(AndroidSchedulers.mainThread())
159+
.subscribe({ (serviceId, url) -> refreshFeedItem(serviceId, url) }, { })
160+
)
161+
146162
groupAdapter = GroupieAdapter().apply {
147163
setOnItemClickListener(listenerStreamItem)
148164
setOnItemLongClickListener(listenerStreamItem)
@@ -184,6 +200,31 @@ class FeedFragment : BaseStateFragment<FeedState>() {
184200
}
185201
}
186202

203+
/**
204+
* Re-queries the DB for a single stream identified by [serviceId] + [url] and updates
205+
* only that item in the adapter (view count + watch progress), without triggering a full
206+
* list reload.
207+
*/
208+
private fun refreshFeedItem(serviceId: Int, url: String) {
209+
disposables.add(
210+
viewModel.refreshStreamWithState(serviceId, url)
211+
.observeOn(AndroidSchedulers.mainThread())
212+
.subscribe({ updatedStreamWithState ->
213+
for (i in 0 until groupAdapter.itemCount) {
214+
val item = groupAdapter.getItem(i)
215+
if (item is StreamItem &&
216+
item.streamWithState.stream.url == url &&
217+
item.streamWithState.stream.serviceId == serviceId
218+
) {
219+
item.streamWithState = updatedStreamWithState
220+
groupAdapter.notifyItemChanged(i, StreamItem.UPDATE_STREAM_DATA)
221+
break
222+
}
223+
}
224+
}, { /* ignore — feed refreshes on next full load */ })
225+
)
226+
}
227+
187228
private fun setupListViewMode() {
188229
// does everything needed to setup the layouts for grid or list modes
189230
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1

app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
1111
import androidx.preference.PreferenceManager
1212
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
1313
import io.reactivex.rxjava3.core.Flowable
14+
import io.reactivex.rxjava3.core.Maybe
1415
import io.reactivex.rxjava3.functions.Function6
1516
import io.reactivex.rxjava3.processors.BehaviorProcessor
1617
import io.reactivex.rxjava3.schedulers.Schedulers
@@ -153,6 +154,14 @@ class FeedViewModel(
153154

154155
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
155156

157+
/**
158+
* Returns a fresh [StreamWithState] for a single stream identified by [serviceId] and [url],
159+
* reading the latest view count and watch progress directly from the database.
160+
* Executes on the IO scheduler.
161+
*/
162+
fun refreshStreamWithState(serviceId: Int, url: String): Maybe<StreamWithState> = feedDatabaseManager.getStreamWithState(serviceId, url)
163+
.subscribeOn(Schedulers.io())
164+
156165
companion object {
157166
private fun getShowPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
158167
.getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.schabi.newpipe.local.feed
2+
3+
import androidx.lifecycle.LiveData
4+
import androidx.lifecycle.MutableLiveData
5+
import androidx.lifecycle.ViewModel
6+
import io.reactivex.rxjava3.processors.PublishProcessor
7+
8+
/**
9+
* Activity-scoped ViewModel used as a message bus between
10+
* [VideoDetailFragment][org.schabi.newpipe.fragments.detail.VideoDetailFragment]
11+
* and [FeedFragment].
12+
*
13+
* Two trigger points post here:
14+
* 1. `VideoDetailFragment.handleResult` — stream info (view count) written to DB.
15+
* 2. `HistoryRecordManager.saveStreamState` — watch progress written to DB via [globalProgressBus].
16+
*
17+
* [FeedFragment] observes [updatedStream] and re-queries only the affected item from the DB.
18+
*/
19+
class StreamUpdateViewModel : ViewModel() {
20+
21+
private val _updatedStream = MutableLiveData<Pair<Int, String>>()
22+
23+
/** Emits (serviceId, url) whenever a stream's DB record (view count or progress) is updated. */
24+
val updatedStream: LiveData<Pair<Int, String>> = _updatedStream
25+
26+
/** Called by VideoDetailFragment after the stream info (including view count) is stored. */
27+
fun notifyStreamInfoUpdated(serviceId: Int, url: String) {
28+
_updatedStream.postValue(Pair(serviceId, url))
29+
}
30+
31+
companion object {
32+
/**
33+
* Process-wide bus used by [org.schabi.newpipe.local.history.HistoryRecordManager] (which
34+
* has no Activity context) to publish progress-save events.
35+
* [FeedFragment] subscribes to this bus directly in [FeedFragment.onViewCreated].
36+
*/
37+
@JvmStatic
38+
val globalProgressBus: PublishProcessor<Pair<Int, String>> = PublishProcessor.create()
39+
40+
/**
41+
* Called by [org.schabi.newpipe.local.history.HistoryRecordManager] every time it saves playback progress.
42+
* Safe to call from any thread.
43+
*/
44+
@JvmStatic
45+
fun postProgressUpdate(serviceId: Int, url: String) {
46+
globalProgressBus.onNext(Pair(serviceId, url))
47+
}
48+
}
49+
}

app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@ import org.schabi.newpipe.util.StreamTypeUtil
2424
import org.schabi.newpipe.util.image.CoilHelper
2525

2626
data class StreamItem(
27-
val streamWithState: StreamWithState,
27+
var streamWithState: StreamWithState,
2828
var itemVersion: ItemVersion = ItemVersion.NORMAL
2929
) : BindableItem<ListStreamItemBinding>() {
3030
companion object {
3131
const val UPDATE_RELATIVE_TIME = 1
32+
const val UPDATE_STREAM_DATA = 2
3233
}
3334

34-
private val stream: StreamEntity = streamWithState.stream
35-
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
35+
private val stream: StreamEntity get() = streamWithState.stream
36+
private val stateProgressTime: Long? get() = streamWithState.stateProgressMillis
3637

3738
/**
3839
* Will be executed at the end of the [StreamItem.bind] (with (ListStreamItemBinding,Int)).
@@ -62,6 +63,27 @@ data class StreamItem(
6263
return
6364
}
6465

66+
if (payloads.contains(UPDATE_STREAM_DATA)) {
67+
// Rebind only the fields that may have changed: view count and watch progress
68+
if (itemVersion != ItemVersion.MINI) {
69+
viewBinding.itemAdditionalDetails.text =
70+
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
71+
}
72+
if (stream.duration > 0) {
73+
val progress = stateProgressTime
74+
if (progress != null) {
75+
viewBinding.itemProgressView.visibility = View.VISIBLE
76+
viewBinding.itemProgressView.max = stream.duration.toInt()
77+
viewBinding.itemProgressView.progress =
78+
TimeUnit.MILLISECONDS.toSeconds(progress).toInt()
79+
} else {
80+
viewBinding.itemProgressView.visibility = View.GONE
81+
}
82+
}
83+
execBindEnd?.accept(viewBinding)
84+
return
85+
}
86+
6587
super.bind(viewBinding, position, payloads)
6688
}
6789

@@ -82,7 +104,9 @@ data class StreamItem(
82104
if (stateProgressTime != null) {
83105
viewBinding.itemProgressView.visibility = View.VISIBLE
84106
viewBinding.itemProgressView.max = stream.duration.toInt()
85-
viewBinding.itemProgressView.progress = TimeUnit.MILLISECONDS.toSeconds(stateProgressTime).toInt()
107+
viewBinding.itemProgressView.progress = TimeUnit.MILLISECONDS.toSeconds(
108+
stateProgressTime!!
109+
).toInt()
86110
} else {
87111
viewBinding.itemProgressView.visibility = View.GONE
88112
}

app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.schabi.newpipe.extractor.stream.StreamInfo;
4848
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
4949
import org.schabi.newpipe.local.feed.FeedViewModel;
50+
import org.schabi.newpipe.local.feed.StreamUpdateViewModel;
5051
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
5152

5253
import java.time.OffsetDateTime;
@@ -243,12 +244,15 @@ public Maybe<StreamStateEntity> loadStreamState(final StreamInfo info) {
243244

244245
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
245246
return Completable.fromAction(() -> database.runInTransaction(() -> {
246-
final long streamId = streamTable.upsert(new StreamEntity(info));
247-
final var state = new StreamStateEntity(streamId, progressMillis);
248-
if (state.isValid(info.getDuration())) {
249-
streamStateTable.upsert(state);
250-
}
251-
})).subscribeOn(Schedulers.io());
247+
final long streamId = streamTable.upsert(new StreamEntity(info));
248+
final var state = new StreamStateEntity(streamId, progressMillis);
249+
if (state.isValid(info.getDuration())) {
250+
streamStateTable.upsert(state);
251+
}
252+
})).subscribeOn(Schedulers.io())
253+
.doOnComplete(() -> StreamUpdateViewModel.postProgressUpdate(
254+
info.getServiceId(), info.getUrl()
255+
));
252256
}
253257

254258
public Maybe<StreamStateEntity> loadStreamState(final InfoItem info) {

0 commit comments

Comments
 (0)