feat: chapters (#466)
* Add chapter markers and "skip chapter" on long press * Fix linting problems - Missing comma - Unused import - Comment block * Add preferences options * Drop chapter support for ExoPlayer * Fix linting * Remove Trailing spaces * Remove TODO from marker color * Move code to function * Optimize imports * Fix crash on episode skip * Disable player control view animation * Avoid crash when there are no chapters for media item * Skip to next episode when skipping last chapter * Load chapters from Jellyfin API instead of MPV Player * Remove chapter gesture * Fix build * Fix linting * Fix linting * Support chapters with offline media * Remove debug print * Add chapter skipping * Remove trailing spaces * fix(chapters): display correct chapter while seeking * refactor: faster and cleaner `getCurrentChapterIndex` * refactor: seek to start of current chapter if player position is more than 5 seconds past start of chapter * refactor: change "Matroska chapters" to just "Chapters" The chapters feature also works for MP4 files so just make it generic * Bump database version * Add auto-migration for database version bump * Save database schema * chore: clean up --------- Co-authored-by: Jarne Demeulemeester <jarnedemeulemeester@gmail.com>
This commit is contained in:
parent
06a24568fa
commit
c39bdce845
26 changed files with 1107 additions and 17 deletions
|
@ -7,6 +7,7 @@ import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -29,6 +30,7 @@ import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.ui.DefaultTimeBar
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
|
import androidx.media3.ui.PlayerControlView
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
@ -144,6 +146,18 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
it.currentTrickPlay = currentTrickPlay
|
it.currentTrickPlay = currentTrickPlay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chapters
|
||||||
|
if (appPreferences.showChapterMarkers && currentChapters != null) {
|
||||||
|
currentChapters?.let { chapters ->
|
||||||
|
val playerControlView = findViewById<PlayerControlView>(R.id.exo_controller)
|
||||||
|
val numOfChapters = chapters.size
|
||||||
|
playerControlView.setExtraAdGroupMarkers(
|
||||||
|
LongArray(numOfChapters) { index -> chapters[index].startPosition },
|
||||||
|
BooleanArray(numOfChapters) { false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// File Loaded
|
// File Loaded
|
||||||
if (fileLoaded) {
|
if (fileLoaded) {
|
||||||
audioButton.isEnabled = true
|
audioButton.isEnabled = true
|
||||||
|
@ -239,9 +253,12 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
pictureInPicture()
|
pictureInPicture()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set marker color
|
||||||
|
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||||
|
timeBar.setAdMarkerColor(Color.WHITE)
|
||||||
|
|
||||||
if (appPreferences.playerTrickPlay) {
|
if (appPreferences.playerTrickPlay) {
|
||||||
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||||
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
|
||||||
previewScrubListener = PreviewScrubListener(
|
previewScrubListener = PreviewScrubListener(
|
||||||
imagePreview,
|
imagePreview,
|
||||||
timeBar,
|
timeBar,
|
||||||
|
|
|
@ -23,6 +23,7 @@ import dev.jdtech.jellyfin.AppPreferences
|
||||||
import dev.jdtech.jellyfin.Constants
|
import dev.jdtech.jellyfin.Constants
|
||||||
import dev.jdtech.jellyfin.PlayerActivity
|
import dev.jdtech.jellyfin.PlayerActivity
|
||||||
import dev.jdtech.jellyfin.isControlsLocked
|
import dev.jdtech.jellyfin.isControlsLocked
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -74,7 +75,6 @@ class PlayerGestureHelper(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun onLongPress(e: MotionEvent) {
|
override fun onLongPress(e: MotionEvent) {
|
||||||
// Disables long press gesture if view is locked
|
// Disables long press gesture if view is locked
|
||||||
if (isControlsLocked) return
|
if (isControlsLocked) return
|
||||||
|
@ -82,13 +82,12 @@ class PlayerGestureHelper(
|
||||||
// Stop long press gesture when more than 1 pointer
|
// Stop long press gesture when more than 1 pointer
|
||||||
if (currentNumberOfPointers > 1) return
|
if (currentNumberOfPointers > 1) return
|
||||||
|
|
||||||
playerView.player?.let {
|
// This is a temporary solution for chapter skipping.
|
||||||
if (it.isPlaying) {
|
// TODO: Remove this after implementing #636
|
||||||
lastPlaybackSpeed = it.playbackParameters.speed
|
if (appPreferences.playerGesturesChapterSkip) {
|
||||||
it.setPlaybackSpeed(playbackSpeedIncrease)
|
handleChapterSkip(e)
|
||||||
activity.binding.gestureSpeedText.text = playbackSpeedIncrease.toString() + "x"
|
} else {
|
||||||
activity.binding.gestureSpeedLayout.visibility = View.VISIBLE
|
enableSpeedIncrease()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +122,55 @@ class PlayerGestureHelper(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private fun enableSpeedIncrease() {
|
||||||
|
playerView.player?.let {
|
||||||
|
if (it.isPlaying) {
|
||||||
|
lastPlaybackSpeed = it.playbackParameters.speed
|
||||||
|
it.setPlaybackSpeed(playbackSpeedIncrease)
|
||||||
|
activity.binding.gestureSpeedText.text = playbackSpeedIncrease.toString() + "x"
|
||||||
|
activity.binding.gestureSpeedLayout.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleChapterSkip(e: MotionEvent) {
|
||||||
|
if (isControlsLocked) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewWidth = playerView.measuredWidth
|
||||||
|
val areaWidth = viewWidth / 5 // Divide the view into 5 parts: 2:1:2
|
||||||
|
|
||||||
|
// Define the areas and their boundaries
|
||||||
|
val leftmostAreaStart = 0
|
||||||
|
val middleAreaStart = areaWidth * 2
|
||||||
|
val rightmostAreaStart = middleAreaStart + areaWidth
|
||||||
|
|
||||||
|
when (e.x.toInt()) {
|
||||||
|
in leftmostAreaStart until middleAreaStart -> {
|
||||||
|
activity.viewModel.seekToPreviousChapter()?.let { chapter ->
|
||||||
|
displayChapter(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in rightmostAreaStart until viewWidth -> {
|
||||||
|
if (activity.viewModel.isLastChapter() == true) {
|
||||||
|
playerView.player?.seekToNextMediaItem()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activity.viewModel.seekToNextChapter()?.let { chapter ->
|
||||||
|
displayChapter(chapter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun displayChapter(chapter: PlayerChapter) {
|
||||||
|
activity.binding.progressScrubberLayout.visibility = View.VISIBLE
|
||||||
|
activity.binding.progressScrubberText.text = chapter.name ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
private fun fastForward() {
|
private fun fastForward() {
|
||||||
val currentPosition = playerView.player?.currentPosition ?: 0
|
val currentPosition = playerView.player?.currentPosition ?: 0
|
||||||
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
|
val fastForwardPosition = currentPosition + appPreferences.playerSeekForwardIncrement
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/exo_controller">
|
||||||
|
|
||||||
<androidx.media3.ui.AspectRatioFrameLayout
|
<androidx.media3.ui.AspectRatioFrameLayout
|
||||||
android:id="@id/exo_content_frame"
|
android:id="@id/exo_content_frame"
|
||||||
|
@ -82,9 +83,10 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<View
|
<androidx.media3.ui.PlayerControlView
|
||||||
android:id="@id/exo_controller_placeholder"
|
android:id="@id/exo_controller"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent"
|
||||||
|
app:animation_enabled="false"/>
|
||||||
|
|
||||||
</merge>
|
</merge>
|
|
@ -55,6 +55,7 @@ val dummyEpisode = FindroidEpisode(
|
||||||
seasonId = UUID.randomUUID(),
|
seasonId = UUID.randomUUID(),
|
||||||
communityRating = 9.2f,
|
communityRating = 9.2f,
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val dummyEpisodes = listOf(
|
val dummyEpisodes = listOf(
|
||||||
|
|
|
@ -55,6 +55,7 @@ val dummyMovie = FindroidMovie(
|
||||||
endDate = null,
|
endDate = null,
|
||||||
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
|
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val dummyMovies = listOf(
|
val dummyMovies = listOf(
|
||||||
|
|
|
@ -103,8 +103,10 @@
|
||||||
<string name="player_gestures_vb">Volume and brightness gestures</string>
|
<string name="player_gestures_vb">Volume and brightness gestures</string>
|
||||||
<string name="player_gestures_zoom">Zoom gesture</string>
|
<string name="player_gestures_zoom">Zoom gesture</string>
|
||||||
<string name="player_gestures_seek">Seek gesture</string>
|
<string name="player_gestures_seek">Seek gesture</string>
|
||||||
|
<string name="player_gestures_chapter_skip">Chapter gesture</string>
|
||||||
<string name="player_gestures_vb_summary">Swipe up and down on the right side of the screen to change the volume and on the left side to change the brightness</string>
|
<string name="player_gestures_vb_summary">Swipe up and down on the right side of the screen to change the volume and on the left side to change the brightness</string>
|
||||||
<string name="player_gestures_zoom_summary">Pinch to fill the screen with the video</string>
|
<string name="player_gestures_zoom_summary">Pinch to fill the screen with the video</string>
|
||||||
|
<string name="player_gestures_chapter_skip_summary">Long press on Left / Right side to skip chapters (overrides 2x speed gesture)</string>
|
||||||
<string name="player_gestures_seek_summary">Swipe horizontally to seek forwards or backwards</string>
|
<string name="player_gestures_seek_summary">Swipe horizontally to seek forwards or backwards</string>
|
||||||
<string name="player_brightness_remember">Remember brightness level</string>
|
<string name="player_brightness_remember">Remember brightness level</string>
|
||||||
<string name="player_start_maximized">Start maximized</string>
|
<string name="player_start_maximized">Start maximized</string>
|
||||||
|
@ -145,6 +147,8 @@
|
||||||
<string name="pref_player_intro_skipper_summary">Requires ConfusedPolarBear\'s Intro Skipper plugin to be installed on the server</string>
|
<string name="pref_player_intro_skipper_summary">Requires ConfusedPolarBear\'s Intro Skipper plugin to be installed on the server</string>
|
||||||
<string name="pref_player_trick_play">Trick Play</string>
|
<string name="pref_player_trick_play">Trick Play</string>
|
||||||
<string name="pref_player_trick_play_summary">Requires nicknsy\'s Jellyscrub plugin to be installed on the server</string>
|
<string name="pref_player_trick_play_summary">Requires nicknsy\'s Jellyscrub plugin to be installed on the server</string>
|
||||||
|
<string name="pref_player_chapter_markers">Chapter markers</string>
|
||||||
|
<string name="pref_player_chapter_markers_summary">Display chapter markers on the timebar</string>
|
||||||
<string name="addresses">Addresses</string>
|
<string name="addresses">Addresses</string>
|
||||||
<string name="add_address">Add address</string>
|
<string name="add_address">Add address</string>
|
||||||
<string name="add_server_address">Add server address</string>
|
<string name="add_server_address">Add server address</string>
|
||||||
|
|
|
@ -59,6 +59,12 @@
|
||||||
app:key="pref_player_gestures_seek"
|
app:key="pref_player_gestures_seek"
|
||||||
app:summary="@string/player_gestures_seek_summary"
|
app:summary="@string/player_gestures_seek_summary"
|
||||||
app:title="@string/player_gestures_seek" />
|
app:title="@string/player_gestures_seek" />
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:defaultValue="true"
|
||||||
|
app:dependency="pref_player_gestures"
|
||||||
|
app:key="pref_player_gestures_chapter_skip"
|
||||||
|
app:summary="@string/player_gestures_chapter_skip_summary"
|
||||||
|
app:title="@string/player_gestures_chapter_skip" />
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:dependency="pref_player_gestures_vb"
|
app:dependency="pref_player_gestures_vb"
|
||||||
app:key="pref_player_brightness_remember"
|
app:key="pref_player_brightness_remember"
|
||||||
|
@ -97,6 +103,13 @@
|
||||||
app:title="@string/pref_player_trick_play"
|
app:title="@string/pref_player_trick_play"
|
||||||
app:widgetLayout="@layout/preference_material3_switch" />
|
app:widgetLayout="@layout/preference_material3_switch" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:defaultValue="true"
|
||||||
|
app:key="pref_player_chapter_markers"
|
||||||
|
app:summary="@string/pref_player_chapter_markers_summary"
|
||||||
|
app:title="@string/pref_player_chapter_markers"
|
||||||
|
app:widgetLayout="@layout/preference_material3_switch" />
|
||||||
|
|
||||||
<PreferenceCategory app:title="@string/picture_in_picture">
|
<PreferenceCategory app:title="@string/picture_in_picture">
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
app:key="pref_player_picture_in_picture_gesture"
|
app:key="pref_player_picture_in_picture_gesture"
|
||||||
|
|
831
data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/4.json
Normal file
831
data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/4.json
Normal file
|
@ -0,0 +1,831 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "45100d543bb99759e0a8886a70d5caa2",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "servers",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `currentServerAddressId` TEXT, `currentUserId` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentServerAddressId",
|
||||||
|
"columnName": "currentServerAddressId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "currentUserId",
|
||||||
|
"columnName": "currentUserId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "serverAddresses",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "address",
|
||||||
|
"columnName": "address",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_serverAddresses_serverId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_serverAddresses_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "servers",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "users",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` TEXT NOT NULL, `accessToken` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_users_serverId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "servers",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"serverId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "movies",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `name` TEXT NOT NULL, `originalTitle` TEXT, `overview` TEXT NOT NULL, `runtimeTicks` INTEGER NOT NULL, `premiereDate` INTEGER, `communityRating` REAL, `officialRating` TEXT, `status` TEXT NOT NULL, `productionYear` INTEGER, `endDate` INTEGER, `chapters` TEXT, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "originalTitle",
|
||||||
|
"columnName": "originalTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "runtimeTicks",
|
||||||
|
"columnName": "runtimeTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "premiereDate",
|
||||||
|
"columnName": "premiereDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "communityRating",
|
||||||
|
"columnName": "communityRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "officialRating",
|
||||||
|
"columnName": "officialRating",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "productionYear",
|
||||||
|
"columnName": "productionYear",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "endDate",
|
||||||
|
"columnName": "endDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapters",
|
||||||
|
"columnName": "chapters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "shows",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `name` TEXT NOT NULL, `originalTitle` TEXT, `overview` TEXT NOT NULL, `runtimeTicks` INTEGER NOT NULL, `communityRating` REAL, `officialRating` TEXT, `status` TEXT NOT NULL, `productionYear` INTEGER, `endDate` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "originalTitle",
|
||||||
|
"columnName": "originalTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "runtimeTicks",
|
||||||
|
"columnName": "runtimeTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "communityRating",
|
||||||
|
"columnName": "communityRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "officialRating",
|
||||||
|
"columnName": "officialRating",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "productionYear",
|
||||||
|
"columnName": "productionYear",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "endDate",
|
||||||
|
"columnName": "endDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "seasons",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `name` TEXT NOT NULL, `seriesName` TEXT NOT NULL, `overview` TEXT NOT NULL, `indexNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`seriesId`) REFERENCES `shows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesId",
|
||||||
|
"columnName": "seriesId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesName",
|
||||||
|
"columnName": "seriesName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "indexNumber",
|
||||||
|
"columnName": "indexNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_seasons_seriesId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_seasons_seriesId` ON `${TABLE_NAME}` (`seriesId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "shows",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "episodes",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `seasonId` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `name` TEXT NOT NULL, `seriesName` TEXT NOT NULL, `overview` TEXT NOT NULL, `indexNumber` INTEGER NOT NULL, `indexNumberEnd` INTEGER, `parentIndexNumber` INTEGER NOT NULL, `runtimeTicks` INTEGER NOT NULL, `premiereDate` INTEGER, `communityRating` REAL, `chapters` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`seasonId`) REFERENCES `seasons`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`seriesId`) REFERENCES `shows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seasonId",
|
||||||
|
"columnName": "seasonId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesId",
|
||||||
|
"columnName": "seriesId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "seriesName",
|
||||||
|
"columnName": "seriesName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "overview",
|
||||||
|
"columnName": "overview",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "indexNumber",
|
||||||
|
"columnName": "indexNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "indexNumberEnd",
|
||||||
|
"columnName": "indexNumberEnd",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "parentIndexNumber",
|
||||||
|
"columnName": "parentIndexNumber",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "runtimeTicks",
|
||||||
|
"columnName": "runtimeTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "premiereDate",
|
||||||
|
"columnName": "premiereDate",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "communityRating",
|
||||||
|
"columnName": "communityRating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapters",
|
||||||
|
"columnName": "chapters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_episodes_seasonId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"seasonId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_seasonId` ON `${TABLE_NAME}` (`seasonId`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_episodes_seriesId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_seriesId` ON `${TABLE_NAME}` (`seriesId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "seasons",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"seasonId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "shows",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"seriesId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itemId` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `path` TEXT NOT NULL, `downloadId` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadId",
|
||||||
|
"columnName": "downloadId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "mediastreams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `sourceId` TEXT NOT NULL, `title` TEXT NOT NULL, `displayTitle` TEXT, `language` TEXT NOT NULL, `type` TEXT NOT NULL, `codec` TEXT NOT NULL, `isExternal` INTEGER NOT NULL, `path` TEXT NOT NULL, `channelLayout` TEXT, `videoRangeType` TEXT, `height` INTEGER, `width` INTEGER, `videoDoViTitle` TEXT, `downloadId` INTEGER, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sourceId",
|
||||||
|
"columnName": "sourceId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayTitle",
|
||||||
|
"columnName": "displayTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "language",
|
||||||
|
"columnName": "language",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "type",
|
||||||
|
"columnName": "type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "codec",
|
||||||
|
"columnName": "codec",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isExternal",
|
||||||
|
"columnName": "isExternal",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "path",
|
||||||
|
"columnName": "path",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "channelLayout",
|
||||||
|
"columnName": "channelLayout",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoRangeType",
|
||||||
|
"columnName": "videoRangeType",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "height",
|
||||||
|
"columnName": "height",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "width",
|
||||||
|
"columnName": "width",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "videoDoViTitle",
|
||||||
|
"columnName": "videoDoViTitle",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "downloadId",
|
||||||
|
"columnName": "downloadId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "trickPlayManifests",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `version` TEXT NOT NULL, `resolution` INTEGER NOT NULL, PRIMARY KEY(`itemId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "resolution",
|
||||||
|
"columnName": "resolution",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "intros",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `start` REAL NOT NULL, `end` REAL NOT NULL, `showAt` REAL NOT NULL, `hideAt` REAL NOT NULL, PRIMARY KEY(`itemId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "start",
|
||||||
|
"columnName": "start",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "end",
|
||||||
|
"columnName": "end",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "showAt",
|
||||||
|
"columnName": "showAt",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "hideAt",
|
||||||
|
"columnName": "hideAt",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "userdata",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `itemId` TEXT NOT NULL, `played` INTEGER NOT NULL, `favorite` INTEGER NOT NULL, `playbackPositionTicks` INTEGER NOT NULL, `toBeSynced` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "userId",
|
||||||
|
"columnName": "userId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "itemId",
|
||||||
|
"columnName": "itemId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "played",
|
||||||
|
"columnName": "played",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favorite",
|
||||||
|
"columnName": "favorite",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "playbackPositionTicks",
|
||||||
|
"columnName": "playbackPositionTicks",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "toBeSynced",
|
||||||
|
"columnName": "toBeSynced",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"userId",
|
||||||
|
"itemId"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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, '45100d543bb99759e0a8886a70d5caa2')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
package dev.jdtech.jellyfin.database
|
package dev.jdtech.jellyfin.database
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidChapter
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.jellyfin.sdk.model.DateTime
|
import org.jellyfin.sdk.model.DateTime
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
@ -25,4 +28,14 @@ class Converters {
|
||||||
fun fromLongToDatetime(value: Long?): DateTime? {
|
fun fromLongToDatetime(value: Long?): DateTime? {
|
||||||
return value?.let { DateTime.ofEpochSecond(it, 0, ZoneOffset.UTC) }
|
return value?.let { DateTime.ofEpochSecond(it, 0, ZoneOffset.UTC) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromFindroidChaptersToString(value: List<FindroidChapter>?): String? {
|
||||||
|
return value?.let { Json.encodeToString(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromStringToFindroidChapters(value: String?): List<FindroidChapter>? {
|
||||||
|
return value?.let { Json.decodeFromString(value) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,10 @@ import dev.jdtech.jellyfin.models.User
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, IntroDto::class, FindroidUserDataDto::class],
|
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, IntroDto::class, FindroidUserDataDto::class],
|
||||||
version = 3,
|
version = 4,
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 2, to = 3),
|
AutoMigration(from = 2, to = 3),
|
||||||
|
AutoMigration(from = 3, to = 4),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
|
|
@ -18,6 +18,7 @@ data class FindroidBoxSet(
|
||||||
override val playbackPositionTicks: Long = 0L,
|
override val playbackPositionTicks: Long = 0L,
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidBoxSet(
|
fun BaseItemDto.toFindroidBoxSet(
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FindroidChapter(
|
||||||
|
/**
|
||||||
|
* The start position.
|
||||||
|
*/
|
||||||
|
val startPosition: Long,
|
||||||
|
/**
|
||||||
|
* The name.
|
||||||
|
*/
|
||||||
|
val name: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun BaseItemDto.toFindroidChapters(): List<FindroidChapter>? {
|
||||||
|
return chapters?.map { chapter ->
|
||||||
|
FindroidChapter(
|
||||||
|
startPosition = chapter.startPositionTicks / 10000,
|
||||||
|
name = chapter.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ data class FindroidCollection(
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
val type: CollectionType,
|
val type: CollectionType,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidCollection(
|
fun BaseItemDto.toFindroidCollection(
|
||||||
|
|
|
@ -31,6 +31,7 @@ data class FindroidEpisode(
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
val missing: Boolean = false,
|
val missing: Boolean = false,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>?,
|
||||||
) : FindroidItem, FindroidSources
|
) : FindroidItem, FindroidSources
|
||||||
|
|
||||||
suspend fun BaseItemDto.toFindroidEpisode(
|
suspend fun BaseItemDto.toFindroidEpisode(
|
||||||
|
@ -65,6 +66,7 @@ suspend fun BaseItemDto.toFindroidEpisode(
|
||||||
communityRating = communityRating,
|
communityRating = communityRating,
|
||||||
missing = locationType == LocationType.VIRTUAL,
|
missing = locationType == LocationType.VIRTUAL,
|
||||||
images = toFindroidImages(jellyfinRepository),
|
images = toFindroidImages(jellyfinRepository),
|
||||||
|
chapters = toFindroidChapters(),
|
||||||
)
|
)
|
||||||
} catch (_: NullPointerException) {
|
} catch (_: NullPointerException) {
|
||||||
null
|
null
|
||||||
|
@ -94,5 +96,6 @@ fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UU
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
communityRating = communityRating,
|
communityRating = communityRating,
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ data class FindroidEpisodeDto(
|
||||||
val runtimeTicks: Long,
|
val runtimeTicks: Long,
|
||||||
val premiereDate: LocalDateTime?,
|
val premiereDate: LocalDateTime?,
|
||||||
val communityRating: Float?,
|
val communityRating: Float?,
|
||||||
|
val chapters: List<FindroidChapter>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpisodeDto {
|
fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpisodeDto {
|
||||||
|
@ -60,5 +61,6 @@ fun FindroidEpisode.toFindroidEpisodeDto(serverId: String? = null): FindroidEpis
|
||||||
runtimeTicks = runtimeTicks,
|
runtimeTicks = runtimeTicks,
|
||||||
premiereDate = premiereDate,
|
premiereDate = premiereDate,
|
||||||
communityRating = communityRating,
|
communityRating = communityRating,
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ interface FindroidItem {
|
||||||
val playbackPositionTicks: Long
|
val playbackPositionTicks: Long
|
||||||
val unplayedItemCount: Int?
|
val unplayedItemCount: Int?
|
||||||
val images: FindroidImages
|
val images: FindroidImages
|
||||||
|
val chapters: List<FindroidChapter>?
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun BaseItemDto.toFindroidItem(
|
suspend fun BaseItemDto.toFindroidItem(
|
||||||
|
|
|
@ -31,6 +31,7 @@ data class FindroidMovie(
|
||||||
val trailer: String?,
|
val trailer: String?,
|
||||||
override val unplayedItemCount: Int? = null,
|
override val unplayedItemCount: Int? = null,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>?,
|
||||||
) : FindroidItem, FindroidSources
|
) : FindroidItem, FindroidSources
|
||||||
|
|
||||||
suspend fun BaseItemDto.toFindroidMovie(
|
suspend fun BaseItemDto.toFindroidMovie(
|
||||||
|
@ -64,6 +65,7 @@ suspend fun BaseItemDto.toFindroidMovie(
|
||||||
endDate = endDate,
|
endDate = endDate,
|
||||||
trailer = remoteTrailers?.getOrNull(0)?.url,
|
trailer = remoteTrailers?.getOrNull(0)?.url,
|
||||||
images = toFindroidImages(jellyfinRepository),
|
images = toFindroidImages(jellyfinRepository),
|
||||||
|
chapters = toFindroidChapters(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,5 +93,6 @@ fun FindroidMovieDto.toFindroidMovie(database: ServerDatabaseDao, userId: UUID):
|
||||||
sources = database.getSources(id).map { it.toFindroidSource(database) },
|
sources = database.getSources(id).map { it.toFindroidSource(database) },
|
||||||
trailer = null,
|
trailer = null,
|
||||||
images = FindroidImages(),
|
images = FindroidImages(),
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ data class FindroidMovieDto(
|
||||||
val status: String,
|
val status: String,
|
||||||
val productionYear: Int?,
|
val productionYear: Int?,
|
||||||
val endDate: LocalDateTime?,
|
val endDate: LocalDateTime?,
|
||||||
|
val chapters: List<FindroidChapter>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto {
|
fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto {
|
||||||
|
@ -36,5 +37,6 @@ fun FindroidMovie.toFindroidMovieDto(serverId: String? = null): FindroidMovieDto
|
||||||
status = status,
|
status = status,
|
||||||
productionYear = productionYear,
|
productionYear = productionYear,
|
||||||
endDate = endDate,
|
endDate = endDate,
|
||||||
|
chapters = chapters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ data class FindroidSeason(
|
||||||
override val playbackPositionTicks: Long = 0L,
|
override val playbackPositionTicks: Long = 0L,
|
||||||
override val unplayedItemCount: Int?,
|
override val unplayedItemCount: Int?,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidSeason(
|
fun BaseItemDto.toFindroidSeason(
|
||||||
|
|
|
@ -31,6 +31,7 @@ data class FindroidShow(
|
||||||
val endDate: DateTime?,
|
val endDate: DateTime?,
|
||||||
val trailer: String?,
|
val trailer: String?,
|
||||||
override val images: FindroidImages,
|
override val images: FindroidImages,
|
||||||
|
override val chapters: List<FindroidChapter>? = null,
|
||||||
) : FindroidItem
|
) : FindroidItem
|
||||||
|
|
||||||
fun BaseItemDto.toFindroidShow(
|
fun BaseItemDto.toFindroidShow(
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package dev.jdtech.jellyfin.models
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class PlayerChapter(
|
||||||
|
/**
|
||||||
|
* The start position.
|
||||||
|
*/
|
||||||
|
val startPosition: Long,
|
||||||
|
/**
|
||||||
|
* The name.
|
||||||
|
*/
|
||||||
|
val name: String? = null,
|
||||||
|
) : Parcelable
|
|
@ -15,4 +15,5 @@ data class PlayerItem(
|
||||||
val indexNumber: Int? = null,
|
val indexNumber: Int? = null,
|
||||||
val indexNumberEnd: Int? = null,
|
val indexNumberEnd: Int? = null,
|
||||||
val externalSubtitles: List<ExternalSubtitle> = emptyList(),
|
val externalSubtitles: List<ExternalSubtitle> = emptyList(),
|
||||||
|
val chapters: List<PlayerChapter>? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.AppPreferences
|
import dev.jdtech.jellyfin.AppPreferences
|
||||||
import dev.jdtech.jellyfin.models.Intro
|
import dev.jdtech.jellyfin.models.Intro
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||||
import dev.jdtech.jellyfin.player.video.R
|
import dev.jdtech.jellyfin.player.video.R
|
||||||
|
@ -56,6 +57,7 @@ constructor(
|
||||||
currentItemTitle = "",
|
currentItemTitle = "",
|
||||||
currentIntro = null,
|
currentIntro = null,
|
||||||
currentTrickPlay = null,
|
currentTrickPlay = null,
|
||||||
|
currentChapters = null,
|
||||||
fileLoaded = false,
|
fileLoaded = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -72,12 +74,13 @@ constructor(
|
||||||
val currentItemTitle: String,
|
val currentItemTitle: String,
|
||||||
val currentIntro: Intro?,
|
val currentIntro: Intro?,
|
||||||
val currentTrickPlay: BifData?,
|
val currentTrickPlay: BifData?,
|
||||||
|
val currentChapters: List<PlayerChapter>?,
|
||||||
val fileLoaded: Boolean,
|
val fileLoaded: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
private var items: Array<PlayerItem> = arrayOf()
|
private var items: Array<PlayerItem> = arrayOf()
|
||||||
|
|
||||||
val trackSelector = DefaultTrackSelector(application)
|
private val trackSelector = DefaultTrackSelector(application)
|
||||||
var playWhenReady = true
|
var playWhenReady = true
|
||||||
private var currentMediaItemIndex = savedStateHandle["mediaItemIndex"] ?: 0
|
private var currentMediaItemIndex = savedStateHandle["mediaItemIndex"] ?: 0
|
||||||
private var playbackPosition: Long = savedStateHandle["position"] ?: 0
|
private var playbackPosition: Long = savedStateHandle["position"] ?: 0
|
||||||
|
@ -277,7 +280,7 @@ constructor(
|
||||||
} else {
|
} else {
|
||||||
item.name
|
item.name
|
||||||
}
|
}
|
||||||
_uiState.update { it.copy(currentItemTitle = itemTitle, fileLoaded = false) }
|
_uiState.update { it.copy(currentItemTitle = itemTitle, currentChapters = item.chapters, fileLoaded = false) }
|
||||||
|
|
||||||
jellyfinRepository.postPlaybackStart(item.itemId)
|
jellyfinRepository.postPlaybackStart(item.itemId)
|
||||||
|
|
||||||
|
@ -367,6 +370,89 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chapters of current item
|
||||||
|
* @return list of [PlayerChapter]
|
||||||
|
*/
|
||||||
|
private fun getChapters(): List<PlayerChapter>? {
|
||||||
|
return uiState.value.currentChapters
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the current chapter
|
||||||
|
* @return the index of the current chapter
|
||||||
|
*/
|
||||||
|
private fun getCurrentChapterIndex(): Int? {
|
||||||
|
val chapters = getChapters() ?: return null
|
||||||
|
|
||||||
|
for (i in chapters.indices.reversed()) {
|
||||||
|
if (chapters[i].startPosition < player.currentPosition) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the next chapter
|
||||||
|
* @return the index of the next chapter
|
||||||
|
*/
|
||||||
|
private fun getNextChapterIndex(): Int? {
|
||||||
|
val chapters = getChapters() ?: return null
|
||||||
|
val currentChapterIndex = getCurrentChapterIndex() ?: return null
|
||||||
|
|
||||||
|
return minOf(chapters.size - 1, currentChapterIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the index of the previous chapter.
|
||||||
|
* Only use this for seeking as it will return the current chapter when player position is more than 5 seconds past the start of the chapter
|
||||||
|
* @return the index of the previous chapter
|
||||||
|
*/
|
||||||
|
private fun getPreviousChapterIndex(): Int? {
|
||||||
|
val chapters = getChapters() ?: return null
|
||||||
|
val currentChapterIndex = getCurrentChapterIndex() ?: return null
|
||||||
|
|
||||||
|
// Return current chapter when more than 5 seconds past chapter start
|
||||||
|
if (player.currentPosition > chapters[currentChapterIndex].startPosition + 5000L) {
|
||||||
|
return currentChapterIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxOf(0, currentChapterIndex - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 }
|
||||||
|
fun isLastChapter(): Boolean? = getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to chapter
|
||||||
|
* @param [chapterIndex] the index of the chapter to seek to
|
||||||
|
* @return the [PlayerChapter] which has been sought to
|
||||||
|
*/
|
||||||
|
private fun seekToChapter(chapterIndex: Int): PlayerChapter? {
|
||||||
|
return getChapters()?.getOrNull(chapterIndex)?.also { chapter ->
|
||||||
|
player.seekTo(chapter.startPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to the next chapter
|
||||||
|
* @return the [PlayerChapter] which has been sought to
|
||||||
|
*/
|
||||||
|
fun seekToNextChapter(): PlayerChapter? {
|
||||||
|
return getNextChapterIndex()?.let { seekToChapter(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seek to the previous chapter
|
||||||
|
* Will seek to start of current chapter if player position is more than 5 seconds past start of chapter
|
||||||
|
* @return the [PlayerChapter] which has been sought to
|
||||||
|
*/
|
||||||
|
fun seekToPreviousChapter(): PlayerChapter? {
|
||||||
|
return getPreviousChapterIndex()?.let { seekToChapter(it) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
super.onIsPlayingChanged(isPlaying)
|
super.onIsPlayingChanged(isPlaying)
|
||||||
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
|
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
|
||||||
|
|
|
@ -6,12 +6,14 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.media3.common.MimeTypes
|
import androidx.media3.common.MimeTypes
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.models.ExternalSubtitle
|
import dev.jdtech.jellyfin.models.ExternalSubtitle
|
||||||
|
import dev.jdtech.jellyfin.models.FindroidChapter
|
||||||
import dev.jdtech.jellyfin.models.FindroidEpisode
|
import dev.jdtech.jellyfin.models.FindroidEpisode
|
||||||
import dev.jdtech.jellyfin.models.FindroidItem
|
import dev.jdtech.jellyfin.models.FindroidItem
|
||||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||||
import dev.jdtech.jellyfin.models.FindroidSeason
|
import dev.jdtech.jellyfin.models.FindroidSeason
|
||||||
import dev.jdtech.jellyfin.models.FindroidShow
|
import dev.jdtech.jellyfin.models.FindroidShow
|
||||||
import dev.jdtech.jellyfin.models.FindroidSourceType
|
import dev.jdtech.jellyfin.models.FindroidSourceType
|
||||||
|
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
@ -113,7 +115,7 @@ class PlayerViewModel @Inject internal constructor(
|
||||||
.getEpisodes(
|
.getEpisodes(
|
||||||
seriesId = item.seriesId,
|
seriesId = item.seriesId,
|
||||||
seasonId = item.seasonId,
|
seasonId = item.seasonId,
|
||||||
fields = listOf(ItemFields.MEDIA_SOURCES),
|
fields = listOf(ItemFields.MEDIA_SOURCES, ItemFields.CHAPTERS),
|
||||||
startItemId = item.id,
|
startItemId = item.id,
|
||||||
limit = if (userConfig?.enableNextEpisodeAutoPlay != false) null else 1,
|
limit = if (userConfig?.enableNextEpisodeAutoPlay != false) null else 1,
|
||||||
)
|
)
|
||||||
|
@ -166,8 +168,18 @@ class PlayerViewModel @Inject internal constructor(
|
||||||
indexNumber = if (this is FindroidEpisode) indexNumber else null,
|
indexNumber = if (this is FindroidEpisode) indexNumber else null,
|
||||||
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
|
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
|
||||||
externalSubtitles = externalSubtitles,
|
externalSubtitles = externalSubtitles,
|
||||||
|
chapters = chapters.toPlayerChapters(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<FindroidChapter>?.toPlayerChapters(): List<PlayerChapter>? {
|
||||||
|
return this?.map { chapter ->
|
||||||
|
PlayerChapter(
|
||||||
|
startPosition = chapter.startPosition,
|
||||||
|
name = chapter.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface PlayerItemsEvent {
|
sealed interface PlayerItemsEvent {
|
||||||
|
|
|
@ -47,6 +47,7 @@ constructor(
|
||||||
val playerGesturesVB get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_VB, true)
|
val playerGesturesVB get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_VB, true)
|
||||||
val playerGesturesZoom get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_ZOOM, true)
|
val playerGesturesZoom get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_ZOOM, true)
|
||||||
val playerGesturesSeek get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_SEEK, true)
|
val playerGesturesSeek get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_SEEK, true)
|
||||||
|
val playerGesturesChapterSkip get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_GESTURES_CHAPTER_SKIP, true)
|
||||||
|
|
||||||
val playerBrightnessRemember get() =
|
val playerBrightnessRemember get() =
|
||||||
sharedPreferences.getBoolean(Constants.PREF_PLAYER_BRIGHTNESS_REMEMBER, false)
|
sharedPreferences.getBoolean(Constants.PREF_PLAYER_BRIGHTNESS_REMEMBER, false)
|
||||||
|
@ -78,6 +79,7 @@ constructor(
|
||||||
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!
|
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!
|
||||||
val playerIntroSkipper get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_INTRO_SKIPPER, true)
|
val playerIntroSkipper get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_INTRO_SKIPPER, true)
|
||||||
val playerTrickPlay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICK_PLAY, true)
|
val playerTrickPlay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICK_PLAY, true)
|
||||||
|
val showChapterMarkers get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_CHAPTER_MARKERS, true)
|
||||||
|
|
||||||
val playerPipGesture get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_PIP_GESTURE, false)
|
val playerPipGesture get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_PIP_GESTURE, false)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ object Constants {
|
||||||
const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb"
|
const val PREF_PLAYER_GESTURES_VB = "pref_player_gestures_vb"
|
||||||
const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom"
|
const val PREF_PLAYER_GESTURES_ZOOM = "pref_player_gestures_zoom"
|
||||||
const val PREF_PLAYER_GESTURES_SEEK = "pref_player_gestures_seek"
|
const val PREF_PLAYER_GESTURES_SEEK = "pref_player_gestures_seek"
|
||||||
|
const val PREF_PLAYER_GESTURES_CHAPTER_SKIP = "pref_player_gestures_chapter_skip"
|
||||||
const val PREF_PLAYER_BRIGHTNESS_REMEMBER = "pref_player_brightness_remember"
|
const val PREF_PLAYER_BRIGHTNESS_REMEMBER = "pref_player_brightness_remember"
|
||||||
const val PREF_PLAYER_START_MAXIMIZED = "pref_player_start_maximized"
|
const val PREF_PLAYER_START_MAXIMIZED = "pref_player_start_maximized"
|
||||||
const val PREF_PLAYER_BRIGHTNESS = "pref_player_brightness"
|
const val PREF_PLAYER_BRIGHTNESS = "pref_player_brightness"
|
||||||
|
@ -27,6 +28,7 @@ object Constants {
|
||||||
const val PREF_PLAYER_MPV_AO = "pref_player_mpv_ao"
|
const val PREF_PLAYER_MPV_AO = "pref_player_mpv_ao"
|
||||||
const val PREF_PLAYER_INTRO_SKIPPER = "pref_player_intro_skipper"
|
const val PREF_PLAYER_INTRO_SKIPPER = "pref_player_intro_skipper"
|
||||||
const val PREF_PLAYER_TRICK_PLAY = "pref_player_trick_play"
|
const val PREF_PLAYER_TRICK_PLAY = "pref_player_trick_play"
|
||||||
|
const val PREF_PLAYER_CHAPTER_MARKERS = "pref_player_chapter_markers"
|
||||||
const val PREF_PLAYER_PIP_GESTURE = "pref_player_picture_in_picture_gesture"
|
const val PREF_PLAYER_PIP_GESTURE = "pref_player_picture_in_picture_gesture"
|
||||||
const val PREF_AUDIO_LANGUAGE = "pref_audio_language"
|
const val PREF_AUDIO_LANGUAGE = "pref_audio_language"
|
||||||
const val PREF_SUBTITLE_LANGUAGE = "pref_subtitle_language"
|
const val PREF_SUBTITLE_LANGUAGE = "pref_subtitle_language"
|
||||||
|
|
Loading…
Reference in a new issue