Merge pull request #4590 from Bnyro/submit-sb-segments

feat: support for submitting SponsorBlock segments
This commit is contained in:
Bnyro 2023-08-25 19:39:44 +02:00 committed by GitHub
commit fc1260ce4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 268 additions and 17 deletions

View File

@ -1,9 +1,13 @@
package com.github.libretube.api
import com.github.libretube.api.obj.Instances
import com.github.libretube.api.obj.SubmitSegmentResponse
import com.github.libretube.constants.GITHUB_API_URL
import com.github.libretube.constants.SB_SUBMIT_API_URL
import com.github.libretube.obj.update.UpdateInfo
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
import retrofit2.http.Url
interface ExternalApi {
@ -14,4 +18,16 @@ interface ExternalApi {
// fetch latest version info
@GET(GITHUB_API_URL)
suspend fun getUpdateInfo(): UpdateInfo
@POST(SB_SUBMIT_API_URL)
suspend fun submitSegment(
@Query("videoID") videoId: String,
@Query("startTime") startTime: Float,
@Query("endTime") endTime: Float,
@Query("category") category: String,
@Query("userAgent") userAgent: String,
@Query("userID") userID: String,
@Query("duration") duration: Float? = null,
@Query("description") description: String = ""
): List<SubmitSegmentResponse>
}

View File

@ -1,10 +1,11 @@
package com.github.libretube.api.obj
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class DeArrowTitle(
val UUID: String,
@SerialName("UUID") val uuid: String,
val locked: Boolean,
val original: Boolean,
val title: String,

View File

@ -0,0 +1,11 @@
package com.github.libretube.api.obj
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SubmitSegmentResponse(
@SerialName("UUID") val uuid: String,
val category: String,
val segment: List<Float>
)

View File

@ -4,6 +4,7 @@ package com.github.libretube.constants
* API link for the update checker
*/
const val GITHUB_API_URL = "https://api.github.com/repos/libre-tube/LibreTube/releases/latest"
const val SB_SUBMIT_API_URL = "https://sponsor.ajay.app/api/skipSegments"
/**
* Links for the about fragment

View File

@ -154,4 +154,9 @@ object PreferenceKeys {
* Error logs
*/
const val ERROR_LOG = "error_log"
/**
* SponsorBlock UUID
*/
const val SB_USER_ID = "sb_user_id"
}

View File

@ -24,6 +24,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.ui.CaptionStyleCompat
import com.github.libretube.LibreTubeApp
import com.github.libretube.R
import com.github.libretube.api.obj.ChapterSegment
import com.github.libretube.api.obj.Segment
@ -41,17 +42,6 @@ import kotlinx.coroutines.runBlocking
object PlayerHelper {
private const val ACTION_MEDIA_CONTROL = "media_control"
const val CONTROL_TYPE = "control_type"
private val SPONSOR_CATEGORIES =
arrayOf(
"intro",
"selfpromo",
"interaction",
"sponsor",
"outro",
"filler",
"music_offtopic",
"preview"
)
const val SPONSOR_HIGHLIGHT_CATEGORY = "poi_highlight"
const val ROLE_FLAG_AUTO_GEN_SUBTITLE = C.ROLE_FLAG_SUPPLEMENTARY
@ -463,7 +453,9 @@ object PlayerHelper {
fun getSponsorBlockCategories(): MutableMap<String, SbSkipOptions> {
val categories: MutableMap<String, SbSkipOptions> = mutableMapOf()
for (category in SPONSOR_CATEGORIES) {
for (category in LibreTubeApp.instance.resources.getStringArray(
R.array.sponsorBlockSegments
)) {
val state = PreferenceHelper.getString(category + "_category", "off").uppercase()
if (SbSkipOptions.valueOf(state) != SbSkipOptions.OFF) {
categories[category] = SbSkipOptions.valueOf(state)
@ -496,7 +488,7 @@ object PlayerHelper {
if (sponsorBlockConfig[segment.category] == SbSkipOptions.AUTOMATIC ||
(
sponsorBlockConfig[segment.category] == SbSkipOptions.AUTOMATIC_ONCE &&
segment.skipped == false
!segment.skipped
)
) {
if (sponsorBlockNotifications) {
@ -510,7 +502,7 @@ object PlayerHelper {
} else if (sponsorBlockConfig[segment.category] == SbSkipOptions.MANUAL ||
(
sponsorBlockConfig[segment.category] == SbSkipOptions.AUTOMATIC_ONCE &&
segment.skipped == true
segment.skipped
)
) {
return segment

View File

@ -6,6 +6,7 @@ import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.github.libretube.constants.PreferenceKeys
import java.time.Instant
import java.util.UUID
object PreferenceHelper {
/**
@ -18,6 +19,11 @@ object PreferenceHelper {
*/
private lateinit var authSettings: SharedPreferences
/**
* Possible chars to use for the SB User ID
*/
private const val USER_ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
/**
* set the context that is being used to access the shared preferences
*/
@ -129,6 +135,16 @@ object PreferenceHelper {
}
}
fun getSponsorBlockUserID(): String {
var uuid = getString(PreferenceKeys.SB_USER_ID, "")
if (uuid.isEmpty()) {
// generate a new user id to use for submitting SponsorBlock segments
uuid = (0 until 30).map { USER_ID_CHARS.random() }.joinToString("")
putString(PreferenceKeys.SB_USER_ID, uuid)
}
return uuid
}
private fun getDefaultSharedPreferences(context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}

View File

@ -1,13 +1,13 @@
package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputFilter
import android.text.format.Formatter
import android.util.Log
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
@ -65,7 +65,7 @@ class DownloadDialog(
.setPositiveButton(R.string.download, null)
.show()
.apply {
getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
onDownloadConfirm.invoke()
}
}

View File

@ -0,0 +1,97 @@
package com.github.libretube.ui.dialogs
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.widget.ArrayAdapter
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.github.libretube.BuildConfig
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.databinding.DialogSubmitSegmentBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.Exception
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubmitSegmentDialog(
private val videoId: String,
private val currentPosition: Long,
private val duration: Long?
) : DialogFragment() {
private var _binding: DialogSubmitSegmentBinding? = null
private val binding = _binding!!
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogSubmitSegmentBinding.inflate(layoutInflater)
binding.startTime.setText((currentPosition.toFloat() / 1000).toString())
val categoryNames = resources.getStringArray(R.array.sponsorBlockSegmentNames)
binding.segmentCategory.adapter = ArrayAdapter(
requireContext(),
R.layout.dropdown_item,
categoryNames
)
return MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.sb_create_segment))
.setView(binding.root)
.setPositiveButton(R.string.okay, null)
.setNegativeButton(R.string.cancel, null)
.show()
.apply {
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
requireDialog().hide()
lifecycleScope.launch {
submitSegment()
dismiss()
}
}
}
}
private suspend fun submitSegment() {
val context = requireContext().applicationContext
val startTime = binding.startTime.text.toString().toFloatOrNull()
var endTime = binding.endTime.text.toString().toFloatOrNull()
if (endTime == null || startTime == null || startTime > endTime) {
context.toastFromMainDispatcher(R.string.sb_invalid_segment)
return
}
if (duration != null) {
// the end time can't be greater than the video duration
endTime = minOf(endTime, duration.toFloat())
}
val categories = resources.getStringArray(R.array.sponsorBlockSegments)
val category = categories[binding.segmentCategory.selectedItemPosition]
val userAgent = "${context.packageName}/${BuildConfig.VERSION_NAME}"
val uuid = PreferenceHelper.getSponsorBlockUserID()
val duration = duration?.let { it.toFloat() / 1000 }
try {
withContext(Dispatchers.IO) {
RetrofitInstance.externalApi
.submitSegment(videoId, startTime, endTime, category, userAgent, uuid, duration)
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
context.toastFromMainDispatcher(e.localizedMessage.orEmpty())
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View File

@ -5,16 +5,22 @@ import android.content.res.Configuration
import android.util.AttributeSet
import android.view.View
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.C
import androidx.media3.exoplayer.trackselection.TrackSelector
import androidx.media3.ui.PlayerView.ControllerVisibilityListener
import com.github.libretube.R
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.helpers.WindowHelper
import com.github.libretube.obj.BottomSheetItem
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.dialogs.SubmitSegmentDialog
import com.github.libretube.ui.extensions.toggleSystemBars
import com.github.libretube.ui.interfaces.OnlinePlayerOptions
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.util.PlayingQueue
class OnlinePlayerView(
context: Context,
@ -157,6 +163,15 @@ class OnlinePlayerView(
binding.autoPlay.setOnCheckedChangeListener { _, isChecked ->
PlayerHelper.autoPlayEnabled = isChecked
}
binding.sbSubmit.isVisible = PlayerHelper.sponsorBlockEnabled
binding.sbSubmit.setOnClickListener {
val currentPosition = player?.currentPosition?.takeIf { it != C.TIME_UNSET }
val duration = player?.duration?.takeIf { it != C.TIME_UNSET }
val videoId = PlayingQueue.getCurrent()?.url?.toID() ?: return@setOnClickListener
SubmitSegmentDialog(videoId, currentPosition ?: 0, duration)
.show((context as BaseActivity).supportFragmentManager, null)
}
}
override fun hideController() {

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24.121029dp"
android:tint="@android:color/white"
android:viewportWidth="565.15"
android:viewportHeight="568">
<path
android:fillColor="#ffffff"
android:pathData="m282.58,568a65,65 0,0 1,-34.14 -9.66C95.41,463.94 2.54,300.46 0,121a64.91,64.91 0,0 1,34 -58.09,522.56 522.56,0 0,1 497.16,0 64.91,64.91 0,0 1,34 58.12c-2.53,179.43 -95.4,342.91 -248.42,437.3A65,65 0,0 1,282.58 568ZM282.58,19.69A502.24,502.24 0,0 0,43.4 80.22,45.27 45.27,0 0,0 19.7,120.75c2.44,172.67 91.81,330 239.07,420.83a46.19,46.19 0,0 0,47.61 0C453.64,450.73 543,293.42 545.45,120.75A45.26,45.26 0,0 0,521.75 80.21,502.26 502.26,0 0,0 282.58,19.69Z" />
<path
android:fillColor="#ffffff"
android:pathData="M284.71,42.69A479.9,479.9 0,0 0,54.37 100.42A22.53,22.53 0,0 0,42.67 120.42C45.07,290.26 135.67,438.64 270.83,522.01A22.48,22.48 0,0 0,294.32 522.01C429.48,438.64 520.08,290.26 522.48,120.42A22.53,22.53 0,0 0,510.78 100.42A479.9,479.9 0,0 0,284.71 42.69zM282.57,112.11L282.87,112.11L423.76,365.75L330.3,365.75L330.3,409.21L234.85,409.21L234.85,365.75L141.39,365.75L282.57,112.11z" />
</vector>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
<com.google.android.material.textfield.TextInputLayout
style="@style/CustomDialogTextInputLayout"
android:hint="@string/start_time"
android:layout_width="0dp"
android:layout_weight="1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/start_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/CustomDialogTextInputLayout"
android:hint="@string/end_time"
android:layout_width="0dp"
android:layout_weight="1">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/end_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<com.github.libretube.ui.views.DropdownMenu
android:id="@+id/segment_category"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
app:hint="@string/segment_type"
app:icon="@drawable/ic_frame" />
</LinearLayout>

View File

@ -81,6 +81,15 @@
app:track="@drawable/player_switch_track"
app:trackTint="#88ffffff" />
<ImageButton
android:id="@+id/sb_submit"
android:tooltipText="@string/sb_create_segment"
style="@style/PlayerControlTop"
android:layout_marginEnd="2dp"
android:src="@drawable/ic_upload_segment"
android:visibility="gone"
app:tint="@android:color/white" />
<ImageButton
android:id="@+id/sb_toggle"
android:tooltipText="@string/tooltip_sponsorblock"

View File

@ -469,4 +469,25 @@
<item>auto</item>
</string-array>
<string-array name="sponsorBlockSegments">
<item>sponsor</item>
<item>intro</item>
<item>selfpromo</item>
<item>interaction</item>
<item>outro</item>
<item>filler</item>
<item>music_offtopic</item>
<item>preview</item>
</string-array>
<string-array name="sponsorBlockSegmentNames">
<item>@string/category_sponsor</item>
<item>@string/category_intro</item>
<item>@string/category_selfpromo</item>
<item>@string/category_interaction</item>
<item>@string/category_outro</item>
<item>@string/category_filler</item>
<item>@string/category_music_offtopic</item>
<item>@string/category_preview</item>
</string-array>
</resources>

View File

@ -468,6 +468,10 @@
<string name="auto_generated">auto-generated</string>
<string name="resolution_limited">limited</string>
<string name="registration_disabled">Registration disabled</string>
<string name="sb_create_segment">Create segment</string>
<string name="segment_type">Segment type</string>
<string name="sb_invalid_segment">Invalid segment start or end</string>
<!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string>
<string name="download_channel_description">Shows a notification when downloading media.</string>