From 2497fa87022fb4cd1ea309eb93d095b56a40afe8 Mon Sep 17 00:00:00 2001 From: FineFindus Date: Thu, 13 Mar 2025 15:53:14 +0100 Subject: [PATCH 1/3] refactor(MediaServiceRepository): use lists for SponsorBlock categories According to the SponsorBlock API documentation categories and actionTypes are string arrays. --- .../github/libretube/api/MediaServiceRepository.kt | 4 ++-- .../libretube/api/PipedMediaServiceRepository.kt | 11 ++++++++--- .../github/libretube/services/OnlinePlayerService.kt | 4 ++-- .../libretube/ui/dialogs/SubmitSegmentDialog.kt | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/MediaServiceRepository.kt b/app/src/main/java/com/github/libretube/api/MediaServiceRepository.kt index c3123e2ae..f50707a16 100644 --- a/app/src/main/java/com/github/libretube/api/MediaServiceRepository.kt +++ b/app/src/main/java/com/github/libretube/api/MediaServiceRepository.kt @@ -17,8 +17,8 @@ interface MediaServiceRepository { suspend fun getComments(videoId: String): CommentsPage suspend fun getSegments( videoId: String, - category: String, - actionType: String? = null + category: List, + actionType: List? = null ): SegmentData suspend fun getDeArrowContent(videoIds: String): Map diff --git a/app/src/main/java/com/github/libretube/api/PipedMediaServiceRepository.kt b/app/src/main/java/com/github/libretube/api/PipedMediaServiceRepository.kt index c1b9c1a92..77d45fb4e 100644 --- a/app/src/main/java/com/github/libretube/api/PipedMediaServiceRepository.kt +++ b/app/src/main/java/com/github/libretube/api/PipedMediaServiceRepository.kt @@ -13,6 +13,7 @@ import com.github.libretube.api.obj.StreamItem import com.github.libretube.api.obj.Streams import com.github.libretube.constants.PreferenceKeys import com.github.libretube.helpers.PreferenceHelper +import kotlinx.serialization.encodeToString import retrofit2.HttpException open class PipedMediaServiceRepository : MediaServiceRepository { @@ -36,9 +37,13 @@ open class PipedMediaServiceRepository : MediaServiceRepository { override suspend fun getSegments( videoId: String, - category: String, - actionType: String? - ): SegmentData = api.getSegments(videoId, category, actionType) + category: List, + actionType: List? + ): SegmentData = api.getSegments( + videoId, + JsonHelper.json.encodeToString(category), + JsonHelper.json.encodeToString(actionType) + ) override suspend fun getDeArrowContent(videoIds: String): Map = api.getDeArrowContent(videoIds) diff --git a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt index 59fc5f8bc..938220638 100644 --- a/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt +++ b/app/src/main/java/com/github/libretube/services/OnlinePlayerService.kt @@ -209,8 +209,8 @@ open class OnlinePlayerService : AbstractPlayerService() { if (sponsorBlockConfig.isEmpty()) return@runCatching sponsorBlockSegments = MediaServiceRepository.instance.getSegments( videoId, - JsonHelper.json.encodeToString(sponsorBlockConfig.keys), - """["skip","mute","full","poi","chapter"]""" + sponsorBlockConfig.keys.toList(), + listOf("skip","mute","full","poi","chapter") ).segments withContext(Dispatchers.Main) { diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt index ce5fb7061..70d7349c3 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/SubmitSegmentDialog.kt @@ -147,7 +147,7 @@ class SubmitSegmentDialog : DialogFragment() { private suspend fun fetchSegments() { val categories = resources.getStringArray(R.array.sponsorBlockSegments).toList() segments = try { - MediaServiceRepository.instance.getSegments(videoId, JsonHelper.json.encodeToString(categories)).segments + MediaServiceRepository.instance.getSegments(videoId, categories).segments } catch (e: Exception) { Log.e(TAG(), e.toString()) return From 31613c8f840d20192b76fb83e537132db34d40ac Mon Sep 17 00:00:00 2001 From: FineFindus Date: Thu, 13 Mar 2025 15:55:54 +0100 Subject: [PATCH 2/3] feat(MediaService/Local): implement SponsorBlock support Allows users to use SponsorBlock when using full local mode. Closes: https://github.com/libre-tube/LibreTube/issues/7193 --- .../com/github/libretube/api/ExternalApi.kt | 9 +++++++++ .../api/NewPipeMediaServiceRepository.kt | 20 ++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/ExternalApi.kt b/app/src/main/java/com/github/libretube/api/ExternalApi.kt index b1958050c..f90de46a6 100644 --- a/app/src/main/java/com/github/libretube/api/ExternalApi.kt +++ b/app/src/main/java/com/github/libretube/api/ExternalApi.kt @@ -3,12 +3,14 @@ package com.github.libretube.api import com.github.libretube.api.obj.DeArrowBody import com.github.libretube.api.obj.PipedConfig import com.github.libretube.api.obj.PipedInstance +import com.github.libretube.api.obj.SegmentData import com.github.libretube.api.obj.SubmitSegmentResponse import com.github.libretube.api.obj.VoteInfo import com.github.libretube.obj.update.UpdateInfo import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Path import retrofit2.http.Query import retrofit2.http.Url @@ -43,6 +45,13 @@ interface ExternalApi { @Query("description") description: String = "" ): List + @GET("$SB_API_URL/api/skipSegments/{videoId}") + suspend fun getSegments( + @Path("videoId") videoId: String, + @Query("category") category: List, + @Query("actionType") actionType: List? = null + ): List + @POST("$SB_API_URL/api/branding") suspend fun submitDeArrow(@Body body: DeArrowBody) diff --git a/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt b/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt index d49121c6a..96445f9bb 100644 --- a/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt +++ b/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt @@ -48,6 +48,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.VideoStream +import java.security.MessageDigest private fun VideoStream.toPipedStream() = PipedStream( @@ -319,11 +320,24 @@ class NewPipeMediaServiceRepository : MediaServiceRepository { ) } + @OptIn(ExperimentalStdlibApi::class) override suspend fun getSegments( videoId: String, - category: String, - actionType: String? - ): SegmentData = SegmentData() + category: List, + actionType: List? + ): SegmentData { + // use hashed video id for privacy + // https://wiki.sponsor.ajay.app/w/API_Docs#GET_/api/skipSegments/:sha256HashPrefix + val hashedId = MessageDigest.getInstance("SHA-256") + .digest(videoId.toByteArray()) + .toHexString() + + return RetrofitInstance.externalApi.getSegments( + hashedId.substring(0..4), + category, + actionType + ).first { it.videoID == videoId } + } override suspend fun getDeArrowContent(videoIds: String): Map = emptyMap() From e91404bf679eeb9a4353b2d45bfc54b0a727f9cc Mon Sep 17 00:00:00 2001 From: FineFindus Date: Thu, 13 Mar 2025 16:47:42 +0100 Subject: [PATCH 3/3] feat(MediaService/Local): implement DeArrow support --- .../com/github/libretube/api/ExternalApi.kt | 4 ++ .../api/NewPipeMediaServiceRepository.kt | 45 ++++++++++++------- .../com/github/libretube/extensions/ShaSum.kt | 11 +++++ 3 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/extensions/ShaSum.kt diff --git a/app/src/main/java/com/github/libretube/api/ExternalApi.kt b/app/src/main/java/com/github/libretube/api/ExternalApi.kt index f90de46a6..84a0b596c 100644 --- a/app/src/main/java/com/github/libretube/api/ExternalApi.kt +++ b/app/src/main/java/com/github/libretube/api/ExternalApi.kt @@ -1,6 +1,7 @@ package com.github.libretube.api import com.github.libretube.api.obj.DeArrowBody +import com.github.libretube.api.obj.DeArrowContent import com.github.libretube.api.obj.PipedConfig import com.github.libretube.api.obj.PipedInstance import com.github.libretube.api.obj.SegmentData @@ -64,4 +65,7 @@ interface ExternalApi { @Query("userID") userID: String, @Query("type") score: Int ) + + @GET("$SB_API_URL/api/branding/{videoId}") + suspend fun getDeArrowContent(@Path("videoId") videoId: String): Map } diff --git a/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt b/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt index 96445f9bb..bd6fea3bd 100644 --- a/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt +++ b/app/src/main/java/com/github/libretube/api/NewPipeMediaServiceRepository.kt @@ -20,6 +20,8 @@ import com.github.libretube.api.obj.StreamItem.Companion.TYPE_PLAYLIST import com.github.libretube.api.obj.StreamItem.Companion.TYPE_STREAM import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Subtitle +import com.github.libretube.extensions.parallelMap +import com.github.libretube.extensions.sha256Sum import com.github.libretube.extensions.toID import com.github.libretube.helpers.NewPipeExtractorInstance import com.github.libretube.helpers.PlayerHelper @@ -48,7 +50,6 @@ import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.VideoStream -import java.security.MessageDigest private fun VideoStream.toPipedStream() = PipedStream( @@ -320,27 +321,33 @@ class NewPipeMediaServiceRepository : MediaServiceRepository { ) } - @OptIn(ExperimentalStdlibApi::class) override suspend fun getSegments( - videoId: String, - category: List, - actionType: List? - ): SegmentData { + videoId: String, category: List, actionType: List? + ): SegmentData = RetrofitInstance.externalApi.getSegments( // use hashed video id for privacy // https://wiki.sponsor.ajay.app/w/API_Docs#GET_/api/skipSegments/:sha256HashPrefix - val hashedId = MessageDigest.getInstance("SHA-256") - .digest(videoId.toByteArray()) - .toHexString() - - return RetrofitInstance.externalApi.getSegments( - hashedId.substring(0..4), - category, - actionType - ).first { it.videoID == videoId } - } + videoId.sha256Sum().substring(0, 4), category, actionType + ).first { it.videoID == videoId } override suspend fun getDeArrowContent(videoIds: String): Map = - emptyMap() + videoIds.split(',').chunked(25).flatMap { + it.parallelMap { videoId -> + runCatching { + RetrofitInstance.externalApi.getDeArrowContent( + // use hashed video id for privacy + // https://wiki.sponsor.ajay.app/w/API_Docs/DeArrow#GET_/api/branding/:sha256HashPrefix + videoId.sha256Sum().substring(0, 4) + ) + }.getOrNull() + } + }.filterNotNull().reduce { acc, map -> acc + map }.mapValues { (videoId, value) -> + value.copy( + thumbnails = value.thumbnails.map { thumbnail -> + thumbnail.takeIf { it.original } ?: thumbnail.copy( + thumbnail = "${DEARROW_THUMBNAIL_URL}?videoID=$videoId&time=${thumbnail.timestamp}" + ) + }) + } override suspend fun getSearchResults(searchQuery: String, filter: String): SearchResult { val queryHandler = NewPipeExtractorInstance.extractor.searchQHFactory.fromQuery( @@ -494,4 +501,8 @@ class NewPipeMediaServiceRepository : MediaServiceRepository { comments = commentsInfo.items.map { it.toComment() } ) } + + companion object { + private const val DEARROW_THUMBNAIL_URL = "https://dearrow-thumb.ajay.app/api/v1/getThumbnail" + } } diff --git a/app/src/main/java/com/github/libretube/extensions/ShaSum.kt b/app/src/main/java/com/github/libretube/extensions/ShaSum.kt new file mode 100644 index 000000000..7750f2ed9 --- /dev/null +++ b/app/src/main/java/com/github/libretube/extensions/ShaSum.kt @@ -0,0 +1,11 @@ +package com.github.libretube.extensions + +import java.security.MessageDigest + +/** + * Calculates the SHA-256 hash of the String and returns the result in hexadecimal. + */ +@OptIn(ExperimentalStdlibApi::class) +fun String.sha256Sum(): String = MessageDigest.getInstance("SHA-256") + .digest(this.toByteArray()) + .toHexString()