mirror of
https://github.com/libre-tube/LibreTube.git
synced 2024-12-14 14:20:30 +05:30
Merge pull request #5871 from Bnyro/master
feat: chapters support for downloaded videos
This commit is contained in:
commit
aef1d0c965
577
app/schemas/com.github.libretube.db.AppDatabase/18.json
Normal file
577
app/schemas/com.github.libretube.db.AppDatabase/18.json
Normal file
@ -0,0 +1,577 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 18,
|
||||
"identityHash": "e855c945119df154adaf1ecfe9e3b646",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "watchHistoryItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER, `isShort` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploadDate",
|
||||
"columnName": "uploadDate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderUrl",
|
||||
"columnName": "uploaderUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderAvatar",
|
||||
"columnName": "uploaderAvatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "isShort",
|
||||
"columnName": "isShort",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "watchPosition",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "position",
|
||||
"columnName": "position",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "searchHistoryItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`query` TEXT NOT NULL, PRIMARY KEY(`query`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "query",
|
||||
"columnName": "query",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"query"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "customInstance",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `apiUrl` TEXT NOT NULL, `frontendUrl` TEXT NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "apiUrl",
|
||||
"columnName": "apiUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "frontendUrl",
|
||||
"columnName": "frontendUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "localSubscription",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`channelId` TEXT NOT NULL, PRIMARY KEY(`channelId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "channelId",
|
||||
"columnName": "channelId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"channelId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "playlistBookmark",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` TEXT NOT NULL, `playlistName` TEXT, `thumbnailUrl` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `videos` INTEGER NOT NULL, PRIMARY KEY(`playlistId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "playlistId",
|
||||
"columnName": "playlistId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "playlistName",
|
||||
"columnName": "playlistName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderUrl",
|
||||
"columnName": "uploaderUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderAvatar",
|
||||
"columnName": "uploaderAvatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "videos",
|
||||
"columnName": "videos",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"playlistId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "LocalPlaylist",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL, `description` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "LocalPlaylistItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `videoId` TEXT NOT NULL, `title` TEXT, `uploadDate` TEXT, `uploader` TEXT, `uploaderUrl` TEXT, `uploaderAvatar` TEXT, `thumbnailUrl` TEXT, `duration` INTEGER)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "playlistId",
|
||||
"columnName": "playlistId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploadDate",
|
||||
"columnName": "uploadDate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderUrl",
|
||||
"columnName": "uploaderUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploaderAvatar",
|
||||
"columnName": "uploaderAvatar",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "download",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `title` TEXT NOT NULL, `description` TEXT NOT NULL, `uploader` TEXT NOT NULL, `duration` INTEGER DEFAULT NULL, `uploadDate` TEXT, `thumbnailPath` TEXT, PRIMARY KEY(`videoId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploader",
|
||||
"columnName": "uploader",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "duration",
|
||||
"columnName": "duration",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false,
|
||||
"defaultValue": "NULL"
|
||||
},
|
||||
{
|
||||
"fieldPath": "uploadDate",
|
||||
"columnName": "uploadDate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailPath",
|
||||
"columnName": "thumbnailPath",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"videoId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "downloadItem",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` TEXT NOT NULL, `videoId` TEXT NOT NULL, `fileName` TEXT NOT NULL, `path` TEXT NOT NULL, `url` TEXT, `format` TEXT, `quality` TEXT, `language` TEXT, `downloadSize` INTEGER NOT NULL, FOREIGN KEY(`videoId`) REFERENCES `download`(`videoId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fileName",
|
||||
"columnName": "fileName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "path",
|
||||
"columnName": "path",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "format",
|
||||
"columnName": "format",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "quality",
|
||||
"columnName": "quality",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "language",
|
||||
"columnName": "language",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "downloadSize",
|
||||
"columnName": "downloadSize",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_downloadItem_path",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"path"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_downloadItem_path` ON `${TABLE_NAME}` (`path`)"
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "download",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"videoId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"videoId"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "downloadChapters",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `videoId` TEXT NOT NULL, `name` TEXT NOT NULL, `start` INTEGER NOT NULL, `thumbnailUrl` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "videoId",
|
||||
"columnName": "videoId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "start",
|
||||
"columnName": "start",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailUrl",
|
||||
"columnName": "thumbnailUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "subscriptionGroups",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `channels` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "channels",
|
||||
"columnName": "channels",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "index",
|
||||
"columnName": "index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e855c945119df154adaf1ecfe9e3b646')"
|
||||
]
|
||||
}
|
||||
}
|
@ -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
|
||||
@ -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),
|
||||
|
@ -44,6 +44,18 @@ object DatabaseHolder {
|
||||
}
|
||||
}
|
||||
|
||||
private val MIGRATION_17_18 = object : Migration(17, 18) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE 'downloadChapters' (" +
|
||||
"id INTEGER PRIMARY KEY NOT NULL, " +
|
||||
"videoId TEXT NOT NULL, " +
|
||||
"name TEXT NOT NULL, " +
|
||||
"start INTEGER NOT NULL, " +
|
||||
"thumbnailUrl TEXT NOT NULL" +
|
||||
")")
|
||||
}
|
||||
}
|
||||
|
||||
val Database by lazy {
|
||||
Room.databaseBuilder(LibreTubeApp.instance, AppDatabase::class.java, DATABASE_NAME)
|
||||
.addMigrations(
|
||||
@ -51,7 +63,8 @@ object DatabaseHolder {
|
||||
MIGRATION_12_13,
|
||||
MIGRATION_13_14,
|
||||
MIGRATION_14_15,
|
||||
MIGRATION_15_16
|
||||
MIGRATION_15_16,
|
||||
MIGRATION_17_18
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
)
|
||||
|
@ -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
|
||||
@ -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,
|
||||
|
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
@ -247,6 +253,7 @@ class OfflinePlayerActivity : BaseActivity() {
|
||||
override fun onDestroy() {
|
||||
saveWatchPosition()
|
||||
|
||||
playerViewModel.player = null
|
||||
player.release()
|
||||
watchPositionTimer.destroy()
|
||||
|
||||
|
@ -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
|
||||
@ -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
|
||||
@ -441,7 +438,6 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
|
||||
}
|
||||
disableController()
|
||||
commentsViewModel.setCommentSheetExpand(false)
|
||||
chaptersBottomSheet?.dismiss()
|
||||
transitionEndId = endId
|
||||
transitionStartId = startId
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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")
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -52,10 +50,6 @@ class SeekbarPreviewListener(
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
processPreview(position)
|
||||
}
|
||||
|
||||
runCatching {
|
||||
onScrub.invoke(position)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -74,8 +68,6 @@ class SeekbarPreviewListener(
|
||||
playerBinding.seekbarPreview.alpha = 1f
|
||||
}
|
||||
.start()
|
||||
|
||||
onScrubEnd.invoke(position)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
package com.github.libretube.ui.models
|
||||
|
||||
import android.content.Context
|
||||
@ -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
|
||||
|
@ -9,6 +9,7 @@ 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
|
||||
@ -16,6 +17,7 @@ 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!!
|
||||
|
@ -22,6 +22,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginStart
|
||||
import androidx.core.view.updateLayoutParams
|
||||
@ -35,6 +36,7 @@ import androidx.media3.ui.PlayerView
|
||||
import androidx.media3.ui.SubtitleView
|
||||
import androidx.media3.ui.TimeBar
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.obj.ChapterSegment
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.databinding.DoubleTapOverlayBinding
|
||||
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
|
||||
@ -58,13 +60,14 @@ import com.github.libretube.ui.interfaces.PlayerGestureOptions
|
||||
import com.github.libretube.ui.interfaces.PlayerOptions
|
||||
import com.github.libretube.ui.listeners.PlayerGestureController
|
||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||
import com.github.libretube.ui.sheets.ChaptersBottomSheet
|
||||
import com.github.libretube.ui.sheets.PlaybackOptionsSheet
|
||||
import com.github.libretube.ui.sheets.SleepTimerSheet
|
||||
import com.github.libretube.util.PlayingQueue
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
open class CustomExoPlayerView(
|
||||
abstract class CustomExoPlayerView(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet? = null
|
||||
) : PlayerView(context, attributeSet), PlayerOptions, PlayerGestureOptions {
|
||||
@ -79,6 +82,8 @@ open class CustomExoPlayerView(
|
||||
private lateinit var brightnessHelper: BrightnessHelper
|
||||
private lateinit var audioHelper: AudioHelper
|
||||
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
|
||||
private var chaptersBottomSheet: ChaptersBottomSheet? = null
|
||||
private var scrubbingTimeBar = false
|
||||
|
||||
/**
|
||||
* Objects from the parent fragment
|
||||
@ -191,10 +196,16 @@ open class CustomExoPlayerView(
|
||||
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||
cancelHideControllerTask()
|
||||
|
||||
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
|
||||
scrubbingTimeBar = true
|
||||
}
|
||||
|
||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||
enqueueHideControllerTask()
|
||||
|
||||
setCurrentChapterName(forceUpdate = true, enqueueNew = false)
|
||||
scrubbingTimeBar = false
|
||||
}
|
||||
})
|
||||
|
||||
@ -217,6 +228,46 @@ open class CustomExoPlayerView(
|
||||
}
|
||||
|
||||
updateCurrentPosition()
|
||||
|
||||
// enable the chapters dialog in the player
|
||||
binding.chapterName.setOnClickListener {
|
||||
val sheet = chaptersBottomSheet ?: ChaptersBottomSheet().also {
|
||||
chaptersBottomSheet = it
|
||||
}
|
||||
|
||||
if (sheet.isVisible) {
|
||||
sheet.dismiss()
|
||||
} else {
|
||||
sheet.show(activity.supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set the name of the video chapter in the exoPlayerView
|
||||
fun setCurrentChapterName(forceUpdate: Boolean = false, enqueueNew: Boolean = true) {
|
||||
val player = player ?: return
|
||||
val chapters = getChapters()
|
||||
|
||||
binding.chapterName.isInvisible = chapters.isEmpty()
|
||||
|
||||
// the following logic to set the chapter title can be skipped if no chapters are available
|
||||
if (chapters.isEmpty()) return
|
||||
|
||||
// call the function again in 100ms
|
||||
if (enqueueNew) postDelayed(this::setCurrentChapterName, 100)
|
||||
|
||||
// if the user is scrubbing the time bar, don't update
|
||||
if (scrubbingTimeBar && !forceUpdate) return
|
||||
|
||||
val newChapterName =
|
||||
PlayerHelper.getCurrentChapterIndex(player.currentPosition, chapters)
|
||||
?.let { chapters[it].title.trim() }
|
||||
?: context.getString(R.string.no_chapter)
|
||||
|
||||
// change the chapter name textView text to the chapterName
|
||||
if (newChapterName != binding.chapterName.text) {
|
||||
binding.chapterName.text = newChapterName
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSystemBars(showBars: Boolean) {
|
||||
@ -771,6 +822,8 @@ open class CustomExoPlayerView(
|
||||
|
||||
open fun minimizeOrExitPlayer() = Unit
|
||||
|
||||
abstract fun getChapters(): List<ChapterSegment>
|
||||
|
||||
open fun getWindow(): Window = activity.window
|
||||
|
||||
companion object {
|
||||
|
@ -3,11 +3,14 @@ package com.github.libretube.ui.views
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.libretube.api.obj.ChapterSegment
|
||||
|
||||
class OfflinePlayerView(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet? = null
|
||||
) : CustomExoPlayerView(context, attributeSet) {
|
||||
private var chapters: List<ChapterSegment> = emptyList()
|
||||
|
||||
override fun hideController() {
|
||||
super.hideController()
|
||||
// hide the status bars when continuing to watch video
|
||||
@ -23,4 +26,11 @@ class OfflinePlayerView(
|
||||
override fun minimizeOrExitPlayer() {
|
||||
(context as AppCompatActivity).onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
fun setChapters(chapters: List<ChapterSegment>) {
|
||||
this.chapters = chapters
|
||||
setCurrentChapterName()
|
||||
}
|
||||
|
||||
override fun getChapters(): List<ChapterSegment> = chapters
|
||||
}
|
||||
|
@ -7,8 +7,10 @@ import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.trackselection.TrackSelector
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.api.obj.ChapterSegment
|
||||
import com.github.libretube.constants.IntentData
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.extensions.toID
|
||||
@ -22,6 +24,7 @@ import com.github.libretube.ui.interfaces.OnlinePlayerOptions
|
||||
import com.github.libretube.ui.models.PlayerViewModel
|
||||
import com.github.libretube.util.PlayingQueue
|
||||
|
||||
@UnstableApi
|
||||
class OnlinePlayerView(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet? = null
|
||||
@ -200,4 +203,8 @@ class OnlinePlayerView(
|
||||
override fun minimizeOrExitPlayer() {
|
||||
playerOptions?.exitFullscreen()
|
||||
}
|
||||
|
||||
override fun getChapters(): List<ChapterSegment> {
|
||||
return playerViewModel?.chapters.orEmpty()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user