mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Fix duplicates being added to Android TV watch next channel (#588)
## Description Fixes the logic for determining which items need to be added to the watch next channel row Also adds some code to clean up existing duplicates. This code can be removed around the next release. Adds a channel to the home screen for recently added media. This is needed to ensure full integration with the OS to allow toggling off any of the Wholphin channels including watch next. ### Related issues #372 introduced the bug Fixes #587
This commit is contained in:
parent
ad64f9a136
commit
aa194f5a2d
1 changed files with 165 additions and 1 deletions
|
|
@ -1,29 +1,40 @@
|
|||
package com.github.damontecres.wholphin.services.tvprovider
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.tvprovider.media.tv.Channel
|
||||
import androidx.tvprovider.media.tv.PreviewProgram
|
||||
import androidx.tvprovider.media.tv.TvContractCompat
|
||||
import androidx.tvprovider.media.tv.TvContractCompat.Channels
|
||||
import androidx.tvprovider.media.tv.TvContractCompat.WatchNextPrograms
|
||||
import androidx.tvprovider.media.tv.WatchNextProgram
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.github.damontecres.wholphin.MainActivity
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import com.github.damontecres.wholphin.services.LatestNextUpService
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.exception.ApiClientException
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
|
|
@ -77,6 +88,21 @@ class TvProviderWorker
|
|||
val currentItems = getCurrentTvChannelNextUp()
|
||||
val currentItemIds = currentItems.map { it.internalProviderId }
|
||||
|
||||
// TODO Remove after v0.3.10 release
|
||||
// This cleans up duplicates added to the watch next due a bug in https://github.com/damontecres/Wholphin/pull/372
|
||||
currentItems.groupBy { it.internalProviderId }.forEach { (id, items) ->
|
||||
if (items.size > 1) {
|
||||
Timber.v("Duplicate ID %s", id)
|
||||
items
|
||||
.subList(1, items.size)
|
||||
.map { TvContractCompat.buildWatchNextProgramUri(it.id) }
|
||||
.forEach {
|
||||
context.contentResolver.delete(it, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
// End temporary clean up
|
||||
|
||||
val toRemove =
|
||||
currentItems.filterNot { it.internalProviderId in potentialItemsToAddIds }
|
||||
|
||||
|
|
@ -84,7 +110,7 @@ class TvProviderWorker
|
|||
val userRemovedIds = userRemoved.map { it.internalProviderId }
|
||||
Timber.v("toRemove (%s)=%s", toRemove.size, toRemove.map { it.internalProviderId })
|
||||
val toAdd =
|
||||
potentialItemsToAdd.filterNot { it.id.toString() in currentItemIds && it.id.toString() in userRemovedIds }
|
||||
potentialItemsToAdd.filter { it.id.toString() !in currentItemIds && it.id.toString() !in userRemovedIds }
|
||||
Timber.v("toAdd (%s)=%s", toAdd.size, toAdd.map { it.id })
|
||||
|
||||
// Remove existing items if they are no longer in the next up from server
|
||||
|
|
@ -104,6 +130,8 @@ class TvProviderWorker
|
|||
)
|
||||
Timber.v("Added %s", addedCount)
|
||||
|
||||
addOtherChannels()
|
||||
|
||||
Timber.d("Completed successfully")
|
||||
} catch (_: ApiClientException) {
|
||||
return Result.retry()
|
||||
|
|
@ -226,6 +254,142 @@ class TvProviderWorker
|
|||
)
|
||||
}.build()
|
||||
|
||||
private suspend fun addOtherChannels() {
|
||||
val preferences = preferences.data.firstOrNull()
|
||||
val channelsPrefs = context.getSharedPreferences("channels", Context.MODE_PRIVATE)
|
||||
|
||||
val latest =
|
||||
api.userLibraryApi
|
||||
.getLatestMedia(
|
||||
GetLatestMediaRequest(
|
||||
fields = SlimItemFields,
|
||||
imageTypeLimit = 1,
|
||||
parentId = null,
|
||||
groupItems = true,
|
||||
limit =
|
||||
preferences?.homePagePreferences?.maxItemsPerRow
|
||||
?: AppPreference.HomePageItems.defaultValue.toInt(),
|
||||
isPlayed = null, // Server will handle user's preference
|
||||
),
|
||||
).content
|
||||
.map { BaseItem(it, true) }
|
||||
|
||||
var channelId = channelsPrefs.getString("latest", null)?.toUri()
|
||||
if (channelId == null) {
|
||||
Timber.d("channelId for latest is null")
|
||||
val channel =
|
||||
Channel
|
||||
.Builder()
|
||||
.apply {
|
||||
setDisplayName(context.getString(R.string.recently_added))
|
||||
setType(Channels.TYPE_PREVIEW)
|
||||
setAppLinkIntent(Intent(context, MainActivity::class.java))
|
||||
}.build()
|
||||
channelId =
|
||||
context.contentResolver.insert(
|
||||
Channels.CONTENT_URI,
|
||||
channel.toContentValues(),
|
||||
)
|
||||
if (channelId != null) {
|
||||
channelsPrefs.edit(true) {
|
||||
putString("latest", channelId.toString())
|
||||
}
|
||||
TvContractCompat.requestChannelBrowsable(
|
||||
context,
|
||||
ContentUris.parseId(channelId),
|
||||
)
|
||||
} else {
|
||||
Timber.w("channelId was null")
|
||||
throw IllegalStateException("channelId was null")
|
||||
}
|
||||
}
|
||||
val programs = latest.map { convert(channelId, it).toContentValues() }.toTypedArray()
|
||||
|
||||
// Delete & replace
|
||||
context.contentResolver.delete(TvContractCompat.PreviewPrograms.CONTENT_URI, null, null)
|
||||
val count =
|
||||
context.contentResolver.bulkInsert(
|
||||
TvContractCompat.PreviewPrograms.CONTENT_URI,
|
||||
programs,
|
||||
)
|
||||
Timber.v("Inserted $count records")
|
||||
}
|
||||
|
||||
private fun convert(
|
||||
channelId: Uri,
|
||||
item: BaseItem,
|
||||
): PreviewProgram =
|
||||
PreviewProgram
|
||||
.Builder()
|
||||
.apply {
|
||||
setChannelId(ContentUris.parseId(channelId))
|
||||
|
||||
val dto = item.data
|
||||
setInternalProviderId(item.id.toString())
|
||||
|
||||
val type =
|
||||
when (item.type) {
|
||||
BaseItemKind.SERIES -> WatchNextPrograms.TYPE_TV_SERIES
|
||||
BaseItemKind.SEASON -> WatchNextPrograms.TYPE_TV_SEASON
|
||||
BaseItemKind.EPISODE -> WatchNextPrograms.TYPE_TV_EPISODE
|
||||
BaseItemKind.MOVIE -> WatchNextPrograms.TYPE_MOVIE
|
||||
else -> WatchNextPrograms.TYPE_CLIP
|
||||
}
|
||||
setType(type)
|
||||
|
||||
val resumePosition = dto.userData?.playbackPositionTicks?.ticks
|
||||
if (resumePosition != null && resumePosition >= 2.minutes) {
|
||||
// https://developer.android.com/training/tv/discovery/guidelines-app-developers#types-of-content
|
||||
setLastPlaybackPositionMillis(resumePosition.inWholeMilliseconds.toInt())
|
||||
}
|
||||
dto.runTimeTicks
|
||||
?.ticks
|
||||
?.inWholeMilliseconds
|
||||
?.toInt()
|
||||
?.let(::setDurationMillis)
|
||||
|
||||
setTitle(item.title)
|
||||
setDescription(dto.overview)
|
||||
if (item.type == BaseItemKind.EPISODE) {
|
||||
setEpisodeTitle(item.name)
|
||||
dto.indexNumber?.let(::setEpisodeNumber)
|
||||
dto.parentIndexNumber?.let(::setSeasonNumber)
|
||||
}
|
||||
|
||||
setPosterArtAspectRatio(TvContractCompat.PreviewProgramColumns.ASPECT_RATIO_16_9)
|
||||
setPosterArtUri(imageUrlService.getItemImageUrl(item, ImageType.THUMB)!!.toUri())
|
||||
|
||||
setIntent(
|
||||
Intent(context, MainActivity::class.java)
|
||||
.putExtra(MainActivity.INTENT_ITEM_ID, item.id.toString())
|
||||
.putExtra(MainActivity.INTENT_ITEM_TYPE, item.type.serialName)
|
||||
.apply {
|
||||
if (item.type == BaseItemKind.EPISODE) {
|
||||
putExtra(
|
||||
MainActivity.INTENT_SERIES_ID,
|
||||
dto.seriesId?.toString(),
|
||||
)
|
||||
putExtra(
|
||||
MainActivity.INTENT_SEASON_ID,
|
||||
dto.seasonId?.toString(),
|
||||
)
|
||||
dto.parentIndexNumber?.let {
|
||||
putExtra(
|
||||
MainActivity.INTENT_SEASON_NUMBER,
|
||||
it,
|
||||
)
|
||||
}
|
||||
dto.indexNumber?.let {
|
||||
putExtra(
|
||||
MainActivity.INTENT_EPISODE_NUMBER,
|
||||
it,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}.build()
|
||||
|
||||
companion object {
|
||||
const val WORK_NAME = "com.github.damontecres.wholphin.services.tvprovider.TvProviderWorker"
|
||||
const val PARAM_USER_ID = "userId"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue