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
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
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 +46,13 @@ interface ExternalApi {
@Query("description") description: String = ""
): 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")
suspend fun submitDeArrow(@Body body: DeArrowBody)
@ -55,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<String, DeArrowContent>
}

View File

@ -17,8 +17,8 @@ interface MediaServiceRepository {
suspend fun getComments(videoId: String): CommentsPage
suspend fun getSegments(
videoId: String,
category: String,
actionType: String? = null
category: List<String>,
actionType: List<String>? = null
): SegmentData
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.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
@ -321,13 +323,32 @@ class NewPipeMediaServiceRepository : MediaServiceRepository {
}
override suspend fun getSegments(
videoId: String,
category: String,
actionType: String?
): SegmentData = SegmentData()
videoId: String, category: List<String>, actionType: List<String>?
): SegmentData = RetrofitInstance.externalApi.getSegments(
// use hashed video id for privacy
// 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> =
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(
@ -481,4 +502,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"
}
}

View File

@ -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<String>,
actionType: List<String>?
): SegmentData = api.getSegments(
videoId,
JsonHelper.json.encodeToString(category),
JsonHelper.json.encodeToString(actionType)
)
override suspend fun getDeArrowContent(videoIds: String): Map<String, DeArrowContent> =
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
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) {

View File

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