Skip to content

Commit 4312efd

Browse files
committed
feat: store sponsorblock segments in database
1 parent 3c778c0 commit 4312efd

8 files changed

Lines changed: 894 additions & 14 deletions

File tree

app/schemas/com.github.libretube.db.AppDatabase/22.json

Lines changed: 765 additions & 0 deletions
Large diffs are not rendered by default.

app/src/main/java/com/github/libretube/api/obj/Segment.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package com.github.libretube.api.obj
22

33
import android.os.Parcelable
44
import androidx.collection.FloatFloatPair
5+
import com.github.libretube.db.obj.DownloadSponsorBlockSegment
56
import kotlinx.parcelize.IgnoredOnParcel
67
import kotlinx.parcelize.Parcelize
78
import kotlinx.serialization.SerialName
89
import kotlinx.serialization.Serializable
910
import kotlinx.serialization.Transient
1011

12+
// see https://wiki.sponsor.ajay.app/w/API_Docs#GET_/api/skipSegments
1113
@Serializable
1214
@Parcelize
1315
data class Segment(
@@ -26,6 +28,21 @@ data class Segment(
2628
@IgnoredOnParcel
2729
val segmentStartAndEnd = FloatFloatPair(segment[0], segment[1])
2830

31+
// reminder: all the attributed that are asserted as non-null here are declared non-null by the
32+
// SponsorBlock API, so it's safe to assert they're not null (if we trust the SponsorBlock API docs)
33+
fun toDownloadSegment(videoId: String): DownloadSponsorBlockSegment = DownloadSponsorBlockSegment(
34+
uuid = uuid!!,
35+
videoId = videoId,
36+
startTime = segmentStartAndEnd.first,
37+
endTime = segmentStartAndEnd.second,
38+
actionType = actionType!!,
39+
category = category!!,
40+
description = description,
41+
locked = locked!!,
42+
videoDuration = videoDuration!!.toFloat(),
43+
votes = votes!!
44+
)
45+
2946
companion object {
3047
const val TYPE_FULL = "full"
3148
}

app/src/main/java/com/github/libretube/db/AppDatabase.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.github.libretube.db.obj.DownloadChapter
2020
import com.github.libretube.db.obj.DownloadItem
2121
import com.github.libretube.db.obj.DownloadPlaylist
2222
import com.github.libretube.db.obj.DownloadPlaylistVideosCrossRef
23+
import com.github.libretube.db.obj.DownloadSponsorBlockSegment
2324
import com.github.libretube.db.obj.LocalPlaylist
2425
import com.github.libretube.db.obj.LocalPlaylistItem
2526
import com.github.libretube.db.obj.LocalSubscription
@@ -43,12 +44,13 @@ import com.github.libretube.db.obj.WatchPosition
4344
Download::class,
4445
DownloadItem::class,
4546
DownloadChapter::class,
47+
DownloadSponsorBlockSegment::class,
4648
DownloadPlaylist::class,
4749
DownloadPlaylistVideosCrossRef::class,
4850
SubscriptionGroup::class,
4951
SubscriptionsFeedItem::class
5052
],
51-
version = 21,
53+
version = 22,
5254
autoMigrations = [
5355
AutoMigration(from = 7, to = 8),
5456
AutoMigration(from = 8, to = 9),

app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,24 @@ object DatabaseHolder {
5656
}
5757
}
5858

59+
private val MIGRATION_21_22 = object : Migration(21, 22) {
60+
override fun migrate(db: SupportSQLiteDatabase) {
61+
db.execSQL("CREATE TABLE 'downloadSponsorBlockSegment' (" +
62+
"uuid TEXT PRIMARY KEY NOT NULL, " +
63+
"videoId TEXT NOT NULL, " +
64+
"actionType TEXT NOT NULL, " +
65+
"category TEXT NOT NULL, " +
66+
"description TEXT, " +
67+
"locked INTEGER NOT NULL, " +
68+
"startTime REAL NOT NULL, " +
69+
"endTime REAL NOT NULL, " +
70+
"videoDuration REAL NOT NULL, " +
71+
"votes INTEGER NOT NULL, " +
72+
"CONSTRAINT parentDownload FOREIGN KEY (videoId) REFERENCES download (videoId) ON DELETE CASCADE" +
73+
")")
74+
}
75+
}
76+
5977
val Database by lazy {
6078
Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME)
6179
.addMigrations(
@@ -64,7 +82,8 @@ object DatabaseHolder {
6482
MIGRATION_13_14,
6583
MIGRATION_14_15,
6684
MIGRATION_15_16,
67-
MIGRATION_17_18
85+
MIGRATION_17_18,
86+
MIGRATION_21_22
6887
)
6988
.build()
7089
}

app/src/main/java/com/github/libretube/db/dao/DownloadDao.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.github.libretube.db.obj.DownloadPlaylist
1414
import com.github.libretube.db.obj.DownloadPlaylistVideosCrossRef
1515
import com.github.libretube.db.obj.DownloadPlaylistWithDownload
1616
import com.github.libretube.db.obj.DownloadPlaylistWithDownloadWithItems
17+
import com.github.libretube.db.obj.DownloadSponsorBlockSegment
1718
import com.github.libretube.db.obj.DownloadWithItems
1819

1920
@Dao
@@ -88,4 +89,7 @@ interface DownloadDao {
8889

8990
@Query("SELECT * FROM downloadplaylistvideoscrossref WHERE playlistId = :playlistId")
9091
suspend fun getVideoIdsFromPlaylist(playlistId: String): List<DownloadPlaylistVideosCrossRef>
92+
93+
@Insert
94+
suspend fun insertSponsorBlockSegments(segments: List<DownloadSponsorBlockSegment>)
9195
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.github.libretube.db.obj
2+
3+
import androidx.room.Entity
4+
import androidx.room.ForeignKey
5+
import androidx.room.PrimaryKey
6+
7+
@Entity(
8+
tableName = "downloadSponsorBlockSegment",
9+
foreignKeys = [
10+
ForeignKey(
11+
entity = Download::class,
12+
parentColumns = ["videoId"],
13+
childColumns = ["videoId"],
14+
onDelete = ForeignKey.CASCADE
15+
)
16+
]
17+
)
18+
data class DownloadSponsorBlockSegment(
19+
@PrimaryKey
20+
val uuid: String,
21+
val videoId: String,
22+
23+
val actionType: String,
24+
val category: String,
25+
val description: String? = null,
26+
val locked: Int,
27+
val startTime: Float,
28+
val endTime: Float,
29+
val videoDuration: Float,
30+
val votes: Int,
31+
)

app/src/main/java/com/github/libretube/db/obj/DownloadWithItems.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ data class DownloadWithItems(
1616
parentColumn = "videoId",
1717
entityColumn = "videoId"
1818
)
19-
val downloadChapters: List<DownloadChapter> = emptyList()
19+
val downloadChapters: List<DownloadChapter> = emptyList(),
20+
@Relation(
21+
parentColumn = "videoId",
22+
entityColumn = "videoId"
23+
)
24+
val downloadSponsorBlockSegments: List<DownloadSponsorBlockSegment> = emptyList()
2025
)
2126

2227
fun List<DownloadWithItems>.filterByTab(tab: DownloadTab) = filter { dl ->

app/src/main/java/com/github/libretube/services/DownloadService.kt

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.github.libretube.helpers.DownloadHelper
4444
import com.github.libretube.helpers.DownloadHelper.getNotificationId
4545
import com.github.libretube.helpers.ImageHelper
4646
import com.github.libretube.helpers.NetworkHelper
47+
import com.github.libretube.helpers.PlayerHelper
4748
import com.github.libretube.helpers.ProxyHelper
4849
import com.github.libretube.obj.DownloadStatus
4950
import com.github.libretube.parcelable.DownloadData
@@ -56,6 +57,7 @@ import kotlinx.coroutines.Dispatchers
5657
import kotlinx.coroutines.Job
5758
import kotlinx.coroutines.SupervisorJob
5859
import kotlinx.coroutines.asCoroutineDispatcher
60+
import kotlinx.coroutines.coroutineScope
5961
import kotlinx.coroutines.flow.MutableSharedFlow
6062
import kotlinx.coroutines.flow.SharedFlow
6163
import kotlinx.coroutines.launch
@@ -192,17 +194,52 @@ class DownloadService : LifecycleService() {
192194
Database.downloadDao().insertDownloadChapter(downloadChapter)
193195
}
194196

195-
try {
196-
ImageHelper.downloadImage(
197-
this@DownloadService,
198-
ProxyHelper.rewriteUrlUsingProxyPreference(streams.thumbnailUrl),
199-
thumbnailTargetPath
200-
)
201-
} catch (e: Exception) {
202-
Log.e(
203-
this@DownloadService::class.java.name,
204-
"failed to download image ${streams.thumbnailUrl}"
205-
)
197+
// asynchronously load the remaining metadata
198+
// this allows the main thread to already start the actual download items (i.e. video/audio)
199+
// while the thumbnail and SponsorBlock segments are loaded in the background
200+
coroutineScope {
201+
launch(Dispatchers.IO) {
202+
downloadExtraVideoMetadata(videoId, streams.thumbnailUrl, thumbnailTargetPath)
203+
}
204+
}
205+
}
206+
207+
/**
208+
* Download the thumbnail and SponsorBlock segments for the given [videoId].
209+
*/
210+
private suspend fun downloadExtraVideoMetadata(
211+
videoId: String,
212+
thumbnailUrl: String,
213+
thumbnailTargetPath: Path
214+
) {
215+
coroutineScope {
216+
launch {
217+
val segmentData = try {
218+
val categories = PlayerHelper.getSponsorBlockCategories()
219+
MediaServiceRepository.instance.getSegments(videoId, categories.map { it.key })
220+
} catch (e: Exception) {
221+
Log.e(TAG(), "failed to download SponsorBlock segments for $videoId")
222+
Log.e(TAG(), e.stackTraceToString())
223+
return@launch
224+
}
225+
226+
Database.downloadDao().insertSponsorBlockSegments(
227+
segmentData.segments.map { it.toDownloadSegment(videoId) }
228+
)
229+
}
230+
231+
launch {
232+
try {
233+
ImageHelper.downloadImage(
234+
this@DownloadService,
235+
ProxyHelper.rewriteUrlUsingProxyPreference(thumbnailUrl),
236+
thumbnailTargetPath
237+
)
238+
} catch (e: Exception) {
239+
Log.e(TAG(), "failed to download image $thumbnailUrl")
240+
Log.e(TAG(), e.stackTraceToString())
241+
}
242+
}
206243
}
207244
}
208245

0 commit comments

Comments
 (0)