mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 16:00:31 +05:30
feat: support for DeArrow
This commit is contained in:
parent
5bb076c94f
commit
04b9a3a4c9
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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?
|
||||
)
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
89
app/src/main/java/com/github/libretube/util/DeArrowUtil.kt
Normal file
89
app/src/main/java/com/github/libretube/util/DeArrowUtil.kt
Normal 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)
|
@ -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>
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user