Merge pull request #7195 from FineFindus/feat/local-sb

feat(local): implement support for SponsorBlock and DeArrow
This commit is contained in:
Bnyro 2025-03-16 17:04:22 +01:00 committed by GitHub
commit 69c4ab871f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 67 additions and 13 deletions

View File

@ -1,14 +1,17 @@
package com.github.libretube.api package com.github.libretube.api
import com.github.libretube.api.obj.DeArrowBody 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.PipedConfig
import com.github.libretube.api.obj.PipedInstance 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.SubmitSegmentResponse
import com.github.libretube.api.obj.VoteInfo import com.github.libretube.api.obj.VoteInfo
import com.github.libretube.obj.update.UpdateInfo import com.github.libretube.obj.update.UpdateInfo
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
import retrofit2.http.Url import retrofit2.http.Url
@ -43,6 +46,13 @@ interface ExternalApi {
@Query("description") description: String = "" @Query("description") description: String = ""
): List<SubmitSegmentResponse> ): List<SubmitSegmentResponse>
@GET("$SB_API_URL/api/skipSegments/{videoId}")
suspend fun getSegments(
@Path("videoId") videoId: String,
@Query("category") category: List<String>,
@Query("actionType") actionType: List<String>? = null
): List<SegmentData>
@POST("$SB_API_URL/api/branding") @POST("$SB_API_URL/api/branding")
suspend fun submitDeArrow(@Body body: DeArrowBody) suspend fun submitDeArrow(@Body body: DeArrowBody)
@ -55,4 +65,7 @@ interface ExternalApi {
@Query("userID") userID: String, @Query("userID") userID: String,
@Query("type") score: Int @Query("type") score: Int
) )
@GET("$SB_API_URL/api/branding/{videoId}")
suspend fun getDeArrowContent(@Path("videoId") videoId: String): Map<String, DeArrowContent>
} }

View File

@ -17,8 +17,8 @@ interface MediaServiceRepository {
suspend fun getComments(videoId: String): CommentsPage suspend fun getComments(videoId: String): CommentsPage
suspend fun getSegments( suspend fun getSegments(
videoId: String, videoId: String,
category: String, category: List<String>,
actionType: String? = null actionType: List<String>? = null
): SegmentData ): SegmentData
suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent>

View File

@ -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.StreamItem.Companion.TYPE_STREAM
import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Streams
import com.github.libretube.api.obj.Subtitle 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.extensions.toID
import com.github.libretube.helpers.NewPipeExtractorInstance import com.github.libretube.helpers.NewPipeExtractorInstance
import com.github.libretube.helpers.PlayerHelper import com.github.libretube.helpers.PlayerHelper
@ -321,13 +323,32 @@ class NewPipeMediaServiceRepository : MediaServiceRepository {
} }
override suspend fun getSegments( override suspend fun getSegments(
videoId: String, videoId: String, category: List<String>, actionType: List<String>?
category: String, ): SegmentData = RetrofitInstance.externalApi.getSegments(
actionType: String? // use hashed video id for privacy
): SegmentData = SegmentData() // https://wiki.sponsor.ajay.app/w/API_Docs#GET_/api/skipSegments/:sha256HashPrefix
videoId.sha256Sum().substring(0, 4), category, actionType
).first { it.videoID == videoId }
override suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> = override suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> =
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 { override suspend fun getSearchResults(searchQuery: String, filter: String): SearchResult {
val queryHandler = NewPipeExtractorInstance.extractor.searchQHFactory.fromQuery( val queryHandler = NewPipeExtractorInstance.extractor.searchQHFactory.fromQuery(
@ -481,4 +502,8 @@ class NewPipeMediaServiceRepository : MediaServiceRepository {
comments = commentsInfo.items.map { it.toComment() } comments = commentsInfo.items.map { it.toComment() }
) )
} }
companion object {
private const val DEARROW_THUMBNAIL_URL = "https://dearrow-thumb.ajay.app/api/v1/getThumbnail"
}
} }

View File

@ -13,6 +13,7 @@ import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Streams import com.github.libretube.api.obj.Streams
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import kotlinx.serialization.encodeToString
import retrofit2.HttpException import retrofit2.HttpException
open class PipedMediaServiceRepository : MediaServiceRepository { open class PipedMediaServiceRepository : MediaServiceRepository {
@ -36,9 +37,13 @@ open class PipedMediaServiceRepository : MediaServiceRepository {
override suspend fun getSegments( override suspend fun getSegments(
videoId: String, videoId: String,
category: String, category: List<String>,
actionType: String? actionType: List<String>?
): SegmentData = api.getSegments(videoId, category, actionType) ): SegmentData = api.getSegments(
videoId,
JsonHelper.json.encodeToString(category),
JsonHelper.json.encodeToString(actionType)
)
override suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> = override suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> =
api.getDeArrowContent(videoIds) api.getDeArrowContent(videoIds)

View File

@ -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()

View File

@ -209,8 +209,8 @@ open class OnlinePlayerService : AbstractPlayerService() {
if (sponsorBlockConfig.isEmpty()) return@runCatching if (sponsorBlockConfig.isEmpty()) return@runCatching
sponsorBlockSegments = MediaServiceRepository.instance.getSegments( sponsorBlockSegments = MediaServiceRepository.instance.getSegments(
videoId, videoId,
JsonHelper.json.encodeToString(sponsorBlockConfig.keys), sponsorBlockConfig.keys.toList(),
"""["skip","mute","full","poi","chapter"]""" listOf("skip","mute","full","poi","chapter")
).segments ).segments
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@ -147,7 +147,7 @@ class SubmitSegmentDialog : DialogFragment() {
private suspend fun fetchSegments() { private suspend fun fetchSegments() {
val categories = resources.getStringArray(R.array.sponsorBlockSegments).toList() val categories = resources.getStringArray(R.array.sponsorBlockSegments).toList()
segments = try { segments = try {
MediaServiceRepository.instance.getSegments(videoId, JsonHelper.json.encodeToString(categories)).segments MediaServiceRepository.instance.getSegments(videoId, categories).segments
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG(), e.toString()) Log.e(TAG(), e.toString())
return return