Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: chapters support for downloaded videos #5871

Merged
merged 1 commit into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
577 changes: 577 additions & 0 deletions app/schemas/com.github.libretube.db.AppDatabase/18.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion app/src/main/java/com/github/libretube/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.github.libretube.db.dao.WatchHistoryDao
import com.github.libretube.db.dao.WatchPositionDao
import com.github.libretube.db.obj.CustomInstance
import com.github.libretube.db.obj.Download
import com.github.libretube.db.obj.DownloadChapter
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.db.obj.LocalPlaylist
import com.github.libretube.db.obj.LocalPlaylistItem
Expand All @@ -37,9 +38,10 @@ import com.github.libretube.db.obj.WatchPosition
LocalPlaylistItem::class,
Download::class,
DownloadItem::class,
DownloadChapter::class,
SubscriptionGroup::class
],
version = 17,
version = 18,
autoMigrations = [
AutoMigration(from = 7, to = 8),
AutoMigration(from = 8, to = 9),
Expand Down
15 changes: 14 additions & 1 deletion app/src/main/java/com/github/libretube/db/DatabaseHolder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,27 @@
}
}

private val MIGRATION_17_18 = object : Migration(17, 18) {

Check failure on line 47 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 A multiline expression should start on a new line Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:47:35: error: A multiline expression should start on a new line (standard:multiline-expression-wrapping)
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE 'downloadChapters' (" +

Check failure on line 49 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 A multiline expression should start on a new line Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:49:24: error: A multiline expression should start on a new line (standard:multiline-expression-wrapping)

Check failure on line 49 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Missing newline after "(" Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:49:24: error: Missing newline after "(" (standard:wrapping)

Check failure on line 49 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Argument should be on a separate line (unless all arguments can fit a single line) Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:49:24: error: Argument should be on a separate line (unless all arguments can fit a single line) (standard:argument-list-wrapping)
"id INTEGER PRIMARY KEY NOT NULL, " +

Check failure on line 50 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Unexpected indentation (20) (should be 16) Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:50:1: error: Unexpected indentation (20) (should be 16) (standard:indent)
"videoId TEXT NOT NULL, " +

Check failure on line 51 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Unexpected indentation (20) (should be 16) Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:51:1: error: Unexpected indentation (20) (should be 16) (standard:indent)
"name TEXT NOT NULL, " +

Check failure on line 52 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Unexpected indentation (20) (should be 16) Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:52:1: error: Unexpected indentation (20) (should be 16) (standard:indent)
"start INTEGER NOT NULL, " +

Check failure on line 53 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Unexpected indentation (20) (should be 16) Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:53:1: error: Unexpected indentation (20) (should be 16) (standard:indent)
"thumbnailUrl TEXT NOT NULL" +

Check failure on line 54 in app/src/main/java/com/github/libretube/db/DatabaseHolder.kt

View workflow job for this annotation

GitHub Actions / Check Code Quality

[ktlint] reported by reviewdog 🐶 Unexpected indentation (20) (should be 16) Raw Output: app/src/main/java/com/github/libretube/db/DatabaseHolder.kt:54:1: error: Unexpected indentation (20) (should be 16) (standard:indent)
")")
}
}

val Database by lazy {
Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME)
.addMigrations(
MIGRATION_11_12,
MIGRATION_12_13,
MIGRATION_13_14,
MIGRATION_14_15,
MIGRATION_15_16
MIGRATION_15_16,
MIGRATION_17_18
)
.fallbackToDestructiveMigration()
.build()
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/github/libretube/db/dao/DownloadDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.github.libretube.db.obj.Download
import com.github.libretube.db.obj.DownloadChapter
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.db.obj.DownloadWithItems

Expand All @@ -30,6 +31,9 @@ interface DownloadDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertDownload(download: Download)

@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertDownloadChapter(downloadChapter: DownloadChapter)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDownloadItem(downloadItem: DownloadItem): Long

Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/github/libretube/db/obj/DownloadChapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.github.libretube.db.obj

import androidx.room.Entity
import androidx.room.PrimaryKey
import com.github.libretube.api.obj.ChapterSegment

@Entity(tableName = "downloadChapters")
data class DownloadChapter(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val videoId: String,
val name: String,
val start: Long,
val thumbnailUrl: String
) {
fun toChapterSegment(): ChapterSegment {
return ChapterSegment(name, thumbnailUrl, start)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@ data class DownloadWithItems(
parentColumn = "videoId",
entityColumn = "videoId"
)
val downloadItems: List<DownloadItem>
val downloadItems: List<DownloadItem>,
@Relation(
parentColumn = "videoId",
entityColumn = "videoId"
)
val downloadChapters: List<DownloadChapter> = emptyList()
)
10 changes: 10 additions & 0 deletions app/src/main/java/com/github/libretube/services/DownloadService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.Download
import com.github.libretube.db.obj.DownloadChapter
import com.github.libretube.db.obj.DownloadItem
import com.github.libretube.enums.FileType
import com.github.libretube.enums.NotificationId
Expand Down Expand Up @@ -124,6 +125,15 @@ class DownloadService : LifecycleService() {
thumbnailTargetPath
)
Database.downloadDao().insertDownload(download)
for (chapter in streams.chapters) {
val downloadChapter = DownloadChapter(
videoId = videoId,
name = chapter.title,
start = chapter.start,
thumbnailUrl = chapter.image
)
Database.downloadDao().insertDownloadChapter(downloadChapter)
}
ImageHelper.downloadImage(
this@DownloadService,
streams.thumbnailUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.databinding.ActivityOfflinePlayerBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.db.obj.DownloadChapter
import com.github.libretube.enums.FileType
import com.github.libretube.extensions.toAndroidUri
import com.github.libretube.extensions.updateParameters
Expand Down Expand Up @@ -123,6 +124,7 @@ class OfflinePlayerActivity : BaseActivity() {
player = PlayerHelper.createPlayer(this, trackSelector, false)
player.setWakeMode(C.WAKE_MODE_LOCAL)
player.addListener(playerListener)
playerViewModel.player = player

playerView = binding.player
playerView.setShowSubtitleButton(true)
Expand All @@ -146,6 +148,10 @@ class OfflinePlayerActivity : BaseActivity() {
val downloadInfo = withContext(Dispatchers.IO) {
Database.downloadDao().findById(videoId)
}
val chapters = downloadInfo.downloadChapters.map(DownloadChapter::toChapterSegment)
playerViewModel.chaptersLiveData.value = chapters
binding.player.setChapters(chapters)

val downloadFiles = downloadInfo.downloadItems.filter { it.path.exists() }
playerBinding.exoTitle.text = downloadInfo.download.title
playerBinding.exoTitle.isVisible = true
Expand Down Expand Up @@ -247,6 +253,7 @@ class OfflinePlayerActivity : BaseActivity() {
override fun onDestroy() {
saveWatchPosition()

playerViewModel.player = null
player.release()
watchPositionTimer.destroy()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ import com.github.libretube.ui.listeners.SeekbarPreviewListener
import com.github.libretube.ui.models.CommentsViewModel
import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.sheets.BaseBottomSheet
import com.github.libretube.ui.sheets.ChaptersBottomSheet
import com.github.libretube.ui.sheets.CommentsSheet
import com.github.libretube.ui.sheets.PlayingQueueSheet
import com.github.libretube.ui.sheets.StatsSheet
Expand Down Expand Up @@ -166,8 +165,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
private val handler = Handler(Looper.getMainLooper())

private var seekBarPreviewListener: SeekbarPreviewListener? = null
private var scrubbingTimeBar = false
private var chaptersBottomSheet: ChaptersBottomSheet? = null

// True when the video was closed through the close button on PiP mode
private var closedVideo = false
Expand Down Expand Up @@ -441,7 +438,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
}
disableController()
commentsViewModel.setCommentSheetExpand(false)
chaptersBottomSheet?.dismiss()
transitionEndId = endId
transitionStartId = startId
}
Expand Down Expand Up @@ -971,21 +967,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// show the player notification
initializePlayerNotification()

// enable the chapters dialog in the player
playerBinding.chapterName.setOnClickListener {
updateMaxSheetHeight()
val sheet =
chaptersBottomSheet ?: ChaptersBottomSheet().also {
chaptersBottomSheet = it
}
if (sheet.isVisible) {
sheet.dismiss()
} else {
sheet.show(childFragmentManager)
}
}

setCurrentChapterName()
binding.player.setCurrentChapterName()

fetchSponsorBlockSegments()

Expand Down Expand Up @@ -1048,9 +1030,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {

// close comment bottom sheet if opened for next video
runCatching { commentsViewModel.commentsSheetDismiss?.invoke() }
// kill the chapters bottom sheet if opened
runCatching { chaptersBottomSheet?.dismiss() }
chaptersBottomSheet = null
}

@SuppressLint("SetTextI18n")
Expand Down Expand Up @@ -1202,36 +1181,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
)

withContext(Dispatchers.Main) {
setCurrentChapterName()
}
}

// set the name of the video chapter in the exoPlayerView
private fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
// return if fragment view got killed already to avoid crashes
if (_binding == null) return

// only show the chapters layout if there are some chapters available
playerBinding.chapterName.isInvisible = viewModel.chapters.isEmpty()

// the following logic to set the chapter title can be skipped if no chapters are available
if (viewModel.chapters.isEmpty()) return

// call the function again in 100ms
if (enqueueNew) binding.player.postDelayed(this::setCurrentChapterName, 100)

// if the user is scrubbing the time bar, don't update
if (scrubbingTimeBar && !forceUpdate) return

val chapterName =
PlayerHelper.getCurrentChapterIndex(exoPlayer.currentPosition, viewModel.chapters)
?.let {
viewModel.chapters[it].title.trim()
} ?: getString(R.string.no_chapter)

// change the chapter name textView text to the chapterName
if (chapterName != playerBinding.chapterName.text) {
playerBinding.chapterName.text = chapterName
binding.player.setCurrentChapterName()
}
}

Expand Down Expand Up @@ -1608,15 +1558,7 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
return SeekbarPreviewListener(
OnlineTimeFrameReceiver(requireContext(), streams.previewFrames),
playerBinding,
streams.duration * 1000,
onScrub = {
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
scrubbingTimeBar = true
},
onScrubEnd = {
scrubbingTimeBar = false
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
}
streams.duration * 1000
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import kotlinx.coroutines.withContext
class SeekbarPreviewListener(
private val timeFrameReceiver: TimeFrameReceiver,
private val playerBinding: ExoStyledPlayerControlViewBinding,
private val duration: Long,
private val onScrub: (position: Long) -> Unit = {},
private val onScrubEnd: (position: Long) -> Unit = {}
private val duration: Long
) : TimeBar.OnScrubListener {
private var scrubInProgress = false
private var lastPreviewPosition = Long.MAX_VALUE
Expand Down Expand Up @@ -52,10 +50,6 @@ class SeekbarPreviewListener(
CoroutineScope(Dispatchers.IO).launch {
processPreview(position)
}

runCatching {
onScrub.invoke(position)
}
}

/**
Expand All @@ -74,8 +68,6 @@ class SeekbarPreviewListener(
playerBinding.seekbarPreview.alpha = 1f
}
.start()

onScrubEnd.invoke(position)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

package com.github.libretube.ui.models

import android.content.Context
Expand All @@ -25,6 +24,7 @@ import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import retrofit2.HttpException

@UnstableApi
class PlayerViewModel : ViewModel() {
var player: ExoPlayer? = null
var trackSelector: DefaultTrackSelector? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.databinding.BottomSheetBinding
import com.github.libretube.helpers.PlayerHelper
import com.github.libretube.ui.adapters.ChaptersAdapter
import com.github.libretube.ui.models.PlayerViewModel

@UnstableApi
class ChaptersBottomSheet : UndimmedBottomSheet() {
private var _binding: BottomSheetBinding? = null
private val binding get() = _binding!!
Expand Down
Loading
Loading