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.Subscribed
import com.github.libretube.api.obj.Subscription import com.github.libretube.api.obj.Subscription
import com.github.libretube.api.obj.Token import com.github.libretube.api.obj.Token
import kotlinx.serialization.json.JsonObject
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
@ -45,6 +46,11 @@ interface PipedApi {
@Query("category") category: String @Query("category") category: String
): SegmentData ): SegmentData
@GET("dearrow")
suspend fun getDeArrowContent(
@Query("videoIds") videoIds: String
): JsonObject
@GET("nextpage/comments/{videoId}") @GET("nextpage/comments/{videoId}")
suspend fun getCommentsNextPage( suspend fun getCommentsNextPage(
@Path("videoId") videoId: String, @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.FreeTubeImportPlaylist
import com.github.libretube.obj.FreeTubeVideo import com.github.libretube.obj.FreeTubeVideo
import com.github.libretube.obj.PipedImportPlaylist import com.github.libretube.obj.PipedImportPlaylist
import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll 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() "[\\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() private val token get() = PreferenceHelper.getToken()
val loggedIn: Boolean get() = token.isNotEmpty() val loggedIn: Boolean get() = token.isNotEmpty()
private fun Message.isOk() = this.message == "ok" private fun Message.isOk() = this.message == "ok"
suspend fun getPlaylists(): List<Playlists> = withContext(Dispatchers.IO) { suspend fun getPlaylists(): List<Playlists> = withContext(Dispatchers.IO) {
@ -64,6 +63,8 @@ object PlaylistsHelper {
relatedStreams = relation.videos.map { it.toStreamItem() } 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.db.obj.LocalSubscription
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.util.deArrow
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -137,6 +138,6 @@ object SubscriptionHelper {
subscriptions.joinToString(",") subscriptions.joinToString(",")
) )
} }
} }.deArrow()
} }
} }

View File

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

View File

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

View File

@ -6,9 +6,9 @@ import kotlinx.serialization.Serializable
data class ContentItem( data class ContentItem(
val url: String, val url: String,
val type: String, val type: String,
val thumbnail: String, var thumbnail: String,
// Video only attributes // Video only attributes
val title: String? = null, var title: String? = null,
val uploaderUrl: String? = null, val uploaderUrl: String? = null,
val uploaderAvatar: String? = null, val uploaderAvatar: String? = null,
val duration: Long = -1, 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 uploaderUrl: String? = null,
val uploaderAvatar: String? = null, val uploaderAvatar: String? = null,
val videos: Int = 0, val videos: Int = 0,
val relatedStreams: List<StreamItem> = emptyList() var relatedStreams: List<StreamItem> = emptyList()
) { ) {
fun toPlaylistBookmark(playlistId: String): PlaylistBookmark { fun toPlaylistBookmark(playlistId: String): PlaylistBookmark {
return PlaylistBookmark( return PlaylistBookmark(

View File

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

View File

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

View File

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

View File

@ -82,6 +82,7 @@ object PreferenceKeys {
const val PICTURE_IN_PICTURE = "picture_in_picture" const val PICTURE_IN_PICTURE = "picture_in_picture"
const val PLAYER_RESIZE_MODE = "player_resize_mode" const val PLAYER_RESIZE_MODE = "player_resize_mode"
const val SB_SHOW_MARKERS = "sb_show_markers" const val SB_SHOW_MARKERS = "sb_show_markers"
const val DEARROW = "dearrow"
const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout" const val ALTERNATIVE_PLAYER_LAYOUT = "alternative_player_layout"
const val USE_HLS_OVER_DASH = "use_hls" const val USE_HLS_OVER_DASH = "use_hls"
const val QUEUE_AUTO_INSERT_RELATED = "queue_insert_related_videos" 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.adapters.VideosAdapter
import com.github.libretube.ui.dialogs.ShareDialog import com.github.libretube.ui.dialogs.ShareDialog
import com.github.libretube.ui.extensions.setupSubscriptionButton import com.github.libretube.ui.extensions.setupSubscriptionButton
import com.github.libretube.util.deArrow
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -115,6 +116,8 @@ class ChannelFragment : Fragment() {
RetrofitInstance.api.getChannel(channelId!!) RetrofitInstance.api.getChannel(channelId!!)
} else { } else {
RetrofitInstance.api.getChannelByName(channelName!!) RetrofitInstance.api.getChannelByName(channelName!!)
}.apply {
relatedStreams = relatedStreams.deArrow()
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
@ -240,6 +243,8 @@ class ChannelFragment : Fragment() {
val response = try { val response = try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelTab(tab.data) RetrofitInstance.api.getChannelTab(tab.data)
}.apply {
content = content.deArrow()
} }
} catch (e: Exception) { } catch (e: Exception) {
return@launch return@launch
@ -270,7 +275,9 @@ class ChannelFragment : Fragment() {
repeatOnLifecycle(Lifecycle.State.CREATED) { repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try { val response = try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!) RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!).apply {
relatedStreams = relatedStreams.deArrow()
}
} }
} catch (e: IOException) { } catch (e: IOException) {
_binding?.channelRefresh?.isRefreshing = false _binding?.channelRefresh?.isRefreshing = false
@ -301,6 +308,8 @@ class ChannelFragment : Fragment() {
val newContent = try { val newContent = try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelTab(tab.data, nextPage) RetrofitInstance.api.getChannelTab(tab.data, nextPage)
}.apply {
content = content.deArrow()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), "Exception: $e") 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.PlaylistsAdapter
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.models.SubscriptionsViewModel import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.util.deArrow
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
@ -116,7 +117,7 @@ class HomeFragment : Fragment() {
val region = LocaleHelper.getTrendingRegion(requireContext()) val region = LocaleHelper.getTrendingRegion(requireContext())
val trending = runCatching { val trending = runCatching {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
RetrofitInstance.api.getTrending(region).take(10) RetrofitInstance.api.getTrending(region).deArrow().take(10)
} }
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return }.getOrNull()?.takeIf { it.isNotEmpty() } ?: return
val binding = _binding ?: 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
import com.github.libretube.util.TextUtils.toTimeInSeconds import com.github.libretube.util.TextUtils.toTimeInSeconds
import com.github.libretube.util.YoutubeHlsPlaylistParser import com.github.libretube.util.YoutubeHlsPlaylistParser
import com.github.libretube.util.deArrow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -690,7 +691,9 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
streams = try { streams = try {
RetrofitInstance.api.getStreams(videoId) RetrofitInstance.api.getStreams(videoId).apply {
relatedStreams = relatedStreams.deArrow()
}
} catch (e: IOException) { } catch (e: IOException) {
context?.toastFromMainDispatcher(R.string.unknown_error, Toast.LENGTH_LONG) context?.toastFromMainDispatcher(R.string.unknown_error, Toast.LENGTH_LONG)
return@launch return@launch

View File

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

View File

@ -18,6 +18,7 @@ import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.LocaleHelper import com.github.libretube.helpers.LocaleHelper
import com.github.libretube.ui.activities.SettingsActivity import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.util.deArrow
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -59,7 +60,7 @@ class TrendsFragment : Fragment() {
val response = try { val response = try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val region = LocaleHelper.getTrendingRegion(requireContext()) val region = LocaleHelper.getTrendingRegion(requireContext())
RetrofitInstance.api.getTrending(region) RetrofitInstance.api.getTrending(region).deArrow()
} }
} catch (e: IOException) { } catch (e: IOException) {
println(e) 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="visible">Show in seek bar</string>
<string name="fallback_piped_proxy">Fallback to Piped proxy</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="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 --> <!-- Backup & Restore Settings -->
<string name="import_subscriptions_from">Import subscriptions from</string> <string name="import_subscriptions_from">Import subscriptions from</string>
<string name="export_subscriptions_to">Export subscriptions to</string> <string name="export_subscriptions_to">Export subscriptions to</string>

View File

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