diff --git a/app/src/main/java/com/github/libretube/api/PipedApi.kt b/app/src/main/java/com/github/libretube/api/PipedApi.kt index e79411cc1d..4c1046bf8b 100644 --- a/app/src/main/java/com/github/libretube/api/PipedApi.kt +++ b/app/src/main/java/com/github/libretube/api/PipedApi.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt index ad11888a85..adbaabfc78 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -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 = withContext(Dispatchers.IO) { @@ -64,6 +63,8 @@ object PlaylistsHelper { relatedStreams = relation.videos.map { it.toStreamItem() } ) } + }.apply { + relatedStreams = relatedStreams.deArrow() } } diff --git a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt index fa18bf9b4f..ad31f0ab5f 100644 --- a/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt +++ b/app/src/main/java/com/github/libretube/api/SubscriptionHelper.kt @@ -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() } } diff --git a/app/src/main/java/com/github/libretube/api/obj/Channel.kt b/app/src/main/java/com/github/libretube/api/obj/Channel.kt index 67573ea8f5..db7cb3f1e6 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Channel.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Channel.kt @@ -12,6 +12,6 @@ data class Channel( val nextpage: String? = null, val subscriberCount: Long = 0, val verified: Boolean = false, - val relatedStreams: List = emptyList(), + var relatedStreams: List = emptyList(), val tabs: List = emptyList() ) diff --git a/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt b/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt index 8208239a0f..3e5b12a5c7 100644 --- a/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt +++ b/app/src/main/java/com/github/libretube/api/obj/ChannelTabResponse.kt @@ -4,6 +4,6 @@ import kotlinx.serialization.Serializable @Serializable data class ChannelTabResponse( - val content: List = emptyList(), + var content: List = emptyList(), val nextpage: String? = null ) diff --git a/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt b/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt index 2b2e2047a7..2a10679d0a 100644 --- a/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt +++ b/app/src/main/java/com/github/libretube/api/obj/ContentItem.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/api/obj/DeArrowContent.kt b/app/src/main/java/com/github/libretube/api/obj/DeArrowContent.kt new file mode 100644 index 0000000000..0b060109b8 --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/obj/DeArrowContent.kt @@ -0,0 +1,11 @@ +package com.github.libretube.api.obj + +import kotlinx.serialization.Serializable + +@Serializable +data class DeArrowContent( + val thumbnails: List, + val titles: List, + val randomTime: Float?, + val videoDuration: Float? +) \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/api/obj/DeArrowThumbnail.kt b/app/src/main/java/com/github/libretube/api/obj/DeArrowThumbnail.kt new file mode 100644 index 0000000000..84a17d198d --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/obj/DeArrowThumbnail.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/api/obj/DeArrowTitle.kt b/app/src/main/java/com/github/libretube/api/obj/DeArrowTitle.kt new file mode 100644 index 0000000000..76d392c2b6 --- /dev/null +++ b/app/src/main/java/com/github/libretube/api/obj/DeArrowTitle.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/api/obj/Playlist.kt b/app/src/main/java/com/github/libretube/api/obj/Playlist.kt index 3dddc927a3..0a2975f460 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Playlist.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Playlist.kt @@ -14,7 +14,7 @@ data class Playlist( val uploaderUrl: String? = null, val uploaderAvatar: String? = null, val videos: Int = 0, - val relatedStreams: List = emptyList() + var relatedStreams: List = emptyList() ) { fun toPlaylistBookmark(playlistId: String): PlaylistBookmark { return PlaylistBookmark( diff --git a/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt b/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt index 8cdb45d56a..723fff9bbd 100644 --- a/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt +++ b/app/src/main/java/com/github/libretube/api/obj/SearchResult.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable @Serializable data class SearchResult( - val items: List = emptyList(), + var items: List = emptyList(), val nextpage: String? = null, val suggestion: String? = null, val corrected: Boolean? = null diff --git a/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt b/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt index 44c3c965f9..06270187b7 100644 --- a/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt +++ b/app/src/main/java/com/github/libretube/api/obj/StreamItem.kt @@ -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, diff --git a/app/src/main/java/com/github/libretube/api/obj/Streams.kt b/app/src/main/java/com/github/libretube/api/obj/Streams.kt index e0c6639eb4..7fd123304e 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Streams.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Streams.kt @@ -28,7 +28,7 @@ data class Streams( val dislikes: Long = 0, val audioStreams: List = emptyList(), val videoStreams: List = emptyList(), - val relatedStreams: List = emptyList(), + var relatedStreams: List = emptyList(), val subtitles: List = emptyList(), val livestream: Boolean = false, val proxyUrl: String? = null, diff --git a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt index a187b7e0ee..0932367d4a 100644 --- a/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/github/libretube/constants/PreferenceKeys.kt @@ -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" diff --git a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt index 8389c754e3..8b1ba79503 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt @@ -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") diff --git a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt index ff1adc4a79..15b2f6d221 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt @@ -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 diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt index 63fe294c35..d9f4e0e465 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlayerFragment.kt @@ -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 diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt index 57a76a3b2a..543e3ae2df 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt @@ -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) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt index 192ea8328b..e2e85faef6 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt @@ -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) diff --git a/app/src/main/java/com/github/libretube/util/DeArrowUtil.kt b/app/src/main/java/com/github/libretube/util/DeArrowUtil.kt new file mode 100644 index 0000000000..9114edeec8 --- /dev/null +++ b/app/src/main/java/com/github/libretube/util/DeArrowUtil.kt @@ -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 { + val content = try { + JsonHelper.json.decodeFromJsonElement(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): List { + 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): List { + 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.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.deArrow() = DeArrowUtil.deArrowContentItems(this) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93406fc978..6b1087e14c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -433,6 +433,9 @@ Show in seek bar Fallback to Piped proxy 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. + Enable DeArrow + Show more accurate and less sensationalist titles and thumbnails. Increases loading times. + Import subscriptions from Export subscriptions to diff --git a/app/src/main/res/xml/sponsorblock_settings.xml b/app/src/main/res/xml/sponsorblock_settings.xml index 01b6c60d29..50758af0ca 100644 --- a/app/src/main/res/xml/sponsorblock_settings.xml +++ b/app/src/main/res/xml/sponsorblock_settings.xml @@ -24,6 +24,12 @@ app:key="sb_show_markers" app:title="@string/sb_markers" /> + +