feat: support for DeArrow

This commit is contained in:
Bnyro 2023-07-20 15:01:21 +02:00
parent 5bb076c94f
commit 04b9a3a4c9
22 changed files with 180 additions and 18 deletions

View File

@ -18,6 +18,7 @@ import com.github.libretube.api.obj.Subscribe
import com.github.libretube.api.obj.Subscribed
import com.github.libretube.api.obj.Subscription
import com.github.libretube.api.obj.Token
import kotlinx.serialization.json.JsonObject
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@ -45,6 +46,11 @@ interface PipedApi {
@Query("category") category: String
): SegmentData
@GET("dearrow")
suspend fun getDeArrowContent(
@Query("videoIds") videoIds: String
): JsonObject
@GET("nextpage/comments/{videoId}")
suspend fun getCommentsNextPage(
@Path("videoId") videoId: String,

View File

@ -16,6 +16,7 @@ import com.github.libretube.helpers.ProxyHelper
import com.github.libretube.obj.FreeTubeImportPlaylist
import com.github.libretube.obj.FreeTubeVideo
import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -26,9 +27,7 @@ object PlaylistsHelper {
"[\\da-fA-F]{8}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{4}-[\\da-fA-F]{12}".toRegex()
private val token get() = PreferenceHelper.getToken()
val loggedIn: Boolean get() = token.isNotEmpty()
private fun Message.isOk() = this.message == "ok"
suspend fun getPlaylists(): List<Playlists> = withContext(Dispatchers.IO) {
@ -64,6 +63,8 @@ object PlaylistsHelper {
relatedStreams = relation.videos.map { it.toStreamItem() }
)
}
}.apply {
relatedStreams = relatedStreams.deArrow()
}
}

View File

@ -11,6 +11,7 @@ import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.LocalSubscription
import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.util.deArrow
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@ -137,6 +138,6 @@ object SubscriptionHelper {
subscriptions.joinToString(",")
)
}
}
}.deArrow()
}
}

View File

@ -12,6 +12,6 @@ data class Channel(
val nextpage: String? = null,
val subscriberCount: Long = 0,
val verified: Boolean = false,
val relatedStreams: List<StreamItem> = emptyList(),
var relatedStreams: List<StreamItem> = emptyList(),
val tabs: List<ChannelTab> = emptyList()
)

View File

@ -4,6 +4,6 @@ import kotlinx.serialization.Serializable
@Serializable
data class ChannelTabResponse(
val content: List<ContentItem> = emptyList(),
var content: List<ContentItem> = emptyList(),
val nextpage: String? = null
)

View File

@ -6,9 +6,9 @@ import kotlinx.serialization.Serializable
data class ContentItem(
val url: String,
val type: String,
val thumbnail: String,
var thumbnail: String,
// Video only attributes
val title: String? = null,
var title: String? = null,
val uploaderUrl: String? = null,
val uploaderAvatar: String? = null,
val duration: Long = -1,

View File

@ -0,0 +1,11 @@
package com.github.libretube.api.obj
import kotlinx.serialization.Serializable
@Serializable
data class DeArrowContent(
val thumbnails: List<DeArrowThumbnail>,
val titles: List<DeArrowTitle>,
val randomTime: Float?,
val videoDuration: Float?
)

View File

@ -0,0 +1,13 @@
package com.github.libretube.api.obj
import kotlinx.serialization.Serializable
@Serializable
data class DeArrowThumbnail(
val UUID: String,
val locked: Boolean,
val original: Boolean,
val thumbnail: String? = null,
val timestamp: Float?,
val votes: Int
)

View File

@ -0,0 +1,12 @@
package com.github.libretube.api.obj
import kotlinx.serialization.Serializable
@Serializable
data class DeArrowTitle(
val UUID: String,
val locked: Boolean,
val original: Boolean,
val title: String,
val votes: Int
)

View File

@ -14,7 +14,7 @@ data class Playlist(
val uploaderUrl: String? = null,
val uploaderAvatar: String? = null,
val videos: Int = 0,
val relatedStreams: List<StreamItem> = emptyList()
var relatedStreams: List<StreamItem> = emptyList()
) {
fun toPlaylistBookmark(playlistId: String): PlaylistBookmark {
return PlaylistBookmark(

View File

@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable
@Serializable
data class SearchResult(
val items: List<ContentItem> = emptyList(),
var items: List<ContentItem> = emptyList(),
val nextpage: String? = null,
val suggestion: String? = null,
val corrected: Boolean? = null

View File

@ -8,8 +8,8 @@ import kotlinx.serialization.Serializable
data class StreamItem(
val url: String? = null,
val type: String? = null,
val title: String? = null,
val thumbnail: String? = null,
var title: String? = null,
var thumbnail: String? = null,
val uploaderName: String? = null,
val uploaderUrl: String? = null,
val uploaderAvatar: String? = null,

View File

@ -28,7 +28,7 @@ data class Streams(
val dislikes: Long = 0,
val audioStreams: List<PipedStream> = emptyList(),
val videoStreams: List<PipedStream> = emptyList(),
val relatedStreams: List<StreamItem> = emptyList(),
var relatedStreams: List<StreamItem> = emptyList(),
val subtitles: List<Subtitle> = emptyList(),
val livestream: Boolean = false,
val proxyUrl: String? = null,

View File

@ -82,6 +82,7 @@ object PreferenceKeys {
const val PICTURE_IN_PICTURE = "picture_in_picture"
const val PLAYER_RESIZE_MODE = "player_resize_mode"
const val SB_SHOW_MARKERS = "sb_show_markers"
const val DEARROW = "dearrow"
const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout"
const val USE_HLS_OVER_DASH = "use_hls"
const val QUEUE_AUTO_INSERT_RELATED = "queue_insert_related_videos"

View File

@ -29,6 +29,7 @@ import com.github.libretube.ui.adapters.SearchAdapter
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.util.deArrow
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -115,6 +116,8 @@ class ChannelFragment : Fragment() {
RetrofitInstance.api.getChannel(channelId!!)
} else {
RetrofitInstance.api.getChannelByName(channelName!!)
}.apply {
relatedStreams = relatedStreams.deArrow()
}
}
} catch (e: IOException) {
@ -240,6 +243,8 @@ class ChannelFragment : Fragment() {
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelTab(tab.data)
}.apply {
content = content.deArrow()
}
} catch (e: Exception) {
return@launch
@ -270,7 +275,9 @@ class ChannelFragment : Fragment() {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!)
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!).apply {
relatedStreams = relatedStreams.deArrow()
}
}
} catch (e: IOException) {
_binding?.channelRefresh?.isRefreshing = false
@ -301,6 +308,8 @@ class ChannelFragment : Fragment() {
val newContent = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelTab(tab.data, nextPage)
}.apply {
content = content.deArrow()
}
} catch (e: Exception) {
Log.e(TAG(), "Exception: $e")

View File

@ -30,6 +30,7 @@ import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
import com.github.libretube.ui.adapters.PlaylistsAdapter
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -116,7 +117,7 @@ class HomeFragment : Fragment() {
val region = LocaleHelper.getTrendingRegion(requireContext())
val trending = runCatching {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getTrending(region).take(10)
RetrofitInstance.api.getTrending(region).deArrow().take(10)
}
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return
val binding = _binding ?: return

View File

@ -114,6 +114,7 @@ import com.github.libretube.util.PlayingQueue
import com.github.libretube.util.TextUtils
import com.github.libretube.util.TextUtils.toTimeInSeconds
import com.github.libretube.util.YoutubeHlsPlaylistParser
import com.github.libretube.util.deArrow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -690,7 +691,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
lifecycleScope.launch(Dispatchers.IO) {
streams = try {
RetrofitInstance.api.getStreams(videoId)
RetrofitInstance.api.getStreams(videoId).apply {
relatedStreams = relatedStreams.deArrow()
}
} catch (e: IOException) {
context?.toastFromMainDispatcher(R.string.unknown_error, Toast.LENGTH_LONG)
return@launch

View File

@ -21,6 +21,7 @@ import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.hideKeyboard
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.adapters.SearchAdapter
import com.github.libretube.util.deArrow
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -94,7 +95,9 @@ class SearchResultFragment : Fragment() {
view?.let { context?.hideKeyboard(it) }
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
RetrofitInstance.api.getSearchResults(query, apiSearchFilter).apply {
items = items.deArrow()
}
}
} catch (e: IOException) {
println(e)
@ -124,7 +127,9 @@ class SearchResultFragment : Fragment() {
query,
apiSearchFilter,
nextPage!!
)
).apply {
items = items.deArrow()
}
}
} catch (e: IOException) {
println(e)

View File

@ -18,6 +18,7 @@ import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.util.deArrow
import com.google.android.material.snackbar.Snackbar
import java.io.IOException
import kotlinx.coroutines.Dispatchers
@ -59,7 +60,7 @@ class TrendsFragment : Fragment() {
val response = try {
withContext(Dispatchers.IO) {
val region = LocaleHelper.getTrendingRegion(requireContext())
RetrofitInstance.api.getTrending(region)
RetrofitInstance.api.getTrending(region).deArrow()
}
} catch (e: IOException) {
println(e)

View File

@ -0,0 +1,89 @@
package com.github.libretube.util
import android.util.Log
import com.github.libretube.api.JsonHelper
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.ContentItem
import com.github.libretube.api.obj.DeArrowContent
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PreferenceHelper
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
object DeArrowUtil {
private fun extractTitleAndThumbnail(data: JsonElement): Pair<String?, String?> {
val content = try {
JsonHelper.json.decodeFromJsonElement<DeArrowContent>(data)
} catch (e: Exception) {
return null to null
}
val newTitle = content.titles.maxByOrNull { it.votes }?.title
val newThumbnail =
content.thumbnails.filter { it.thumbnail != null }.maxByOrNull { it.votes }
?.takeIf { !it.original }?.thumbnail
return newTitle to newThumbnail
}
/**
* Apply the new titles and thumbnails generated by DeArrow to the stream items
*/
suspend fun deArrowStreamItems(streamItems: List<StreamItem>): List<StreamItem> {
if (!PreferenceHelper.getBoolean(PreferenceKeys.DEARROW, false)) return streamItems
val videoIds = streamItems.mapNotNull { it.url?.toID() }.joinToString(",")
val response = try {
RetrofitInstance.api.getDeArrowContent(videoIds)
} catch (e: Exception) {
Log.e(this::class.java.name, e.toString())
return streamItems
}
for ((videoId, data) in response.entries) {
val (newTitle, newThumbnail) = extractTitleAndThumbnail(data)
val streamItem = streamItems.firstOrNull { it.url?.toID() == videoId }
newTitle?.let { streamItem?.title = newTitle }
newThumbnail?.let { streamItem?.thumbnail = newThumbnail }
}
return streamItems
}
/**
* Apply the new titles and thumbnails generated by DeArrow to the stream items
*/
suspend fun deArrowContentItems(contentItems: List<ContentItem>): List<ContentItem> {
if (!PreferenceHelper.getBoolean(PreferenceKeys.DEARROW, false)) return contentItems
val videoIds = contentItems.filter { it.type == "stream" }
.joinToString(",") { it.url.toID() }
if (videoIds.isEmpty()) return contentItems
val response = try {
RetrofitInstance.api.getDeArrowContent(videoIds)
} catch (e: Exception) {
Log.e(this::class.java.name, e.toString())
return contentItems
}
for ((videoId, data) in response.entries) {
val (newTitle, newThumbnail) = extractTitleAndThumbnail(data)
val contentItem = contentItems.firstOrNull { it.url.toID() == videoId }
newTitle?.let { contentItem?.title = newTitle }
newThumbnail?.let { contentItem?.thumbnail = newThumbnail }
}
return contentItems
}
}
/**
* If enabled in the preferences, this overrides the video's thumbnail and title with the one
* provided by the DeArrow project
*/
@JvmName("deArrowStreamItems")
suspend fun List<StreamItem>.deArrow() = DeArrowUtil.deArrowStreamItems(this)
/**
* If enabled in the preferences, this overrides the video's thumbnail and title with the one
* provided by the DeArrow project
*/
@JvmName("deArrowContentItems")
suspend fun List<ContentItem>.deArrow() = DeArrowUtil.deArrowContentItems(this)

View File

@ -433,6 +433,9 @@
<string name="visible">Show in seek bar</string>
<string name="fallback_piped_proxy">Fallback to Piped proxy</string>
<string name="fallback_piped_proxy_desc">Load videos via the proxy if connecting to YouTube directly doesn\'t work for the current video (increases the initial loading times). If disabled, YouTube music content likely won\'t play due to YT restrictions.</string>
<string name="dearrow">Enable DeArrow</string>
<string name="dearrow_summary">Show more accurate and less sensationalist titles and thumbnails. Increases loading times.</string>
<!-- Backup & Restore Settings -->
<string name="import_subscriptions_from">Import subscriptions from</string>
<string name="export_subscriptions_to">Export subscriptions to</string>

View File

@ -24,6 +24,12 @@
app:key="sb_show_markers"
app:title="@string/sb_markers" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:key="dearrow"
app:summary="@string/dearrow_summary"
app:title="@string/dearrow" />
<SwitchPreferenceCompat
app:defaultValue="true"
app:key="sb_highlights"