feat: merged Skip Credits

This commit is contained in:
nomadics9 2024-07-04 20:06:37 +03:00
commit 44b6e915ba
46 changed files with 1148 additions and 158 deletions

View file

@ -37,13 +37,15 @@ import dagger.hilt.android.AndroidEntryPoint
import com.nomadics9.ananas.databinding.ActivityPlayerBinding
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
import com.nomadics9.ananas.dialogs.TrackSelectionDialogFragment
import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.utils.PlayerGestureHelper
import com.nomadics9.ananas.utils.PreviewScrubListener
import com.nomadics9.ananas.viewmodels.PlayerActivityViewModel
import com.nomadics9.ananas.viewmodels.PlayerEvents
import kotlinx.coroutines.launch
import com.nomadics9.ananas.utils.PlayerGestureHelper
import com.nomadics9.ananas.utils.PreviewScrubListener
import timber.log.Timber
import javax.inject.Inject
import com.nomadics9.ananas.core.R as CoreR
var isControlsLocked: Boolean = false
@ -58,6 +60,8 @@ class PlayerActivity : BasePlayerActivity() {
override val viewModel: PlayerActivityViewModel by viewModels()
private var previewScrubListener: PreviewScrubListener? = null
private var wasZoom: Boolean = false
private var oldSegment: FindroidSegment? = null
private var buttonPressed: Boolean = false
private val isPipSupported by lazy {
// Check if device has PiP feature
@ -119,7 +123,8 @@ class PlayerActivity : BasePlayerActivity() {
val audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle)
val speedButton = binding.playerView.findViewById<ImageButton>(R.id.btn_speed)
val skipIntroButton = binding.playerView.findViewById<Button>(R.id.btn_skip_intro)
val skipButton = binding.playerView.findViewById<Button>(R.id.btn_skip_intro)
val watchCreditsButton = binding.playerView.findViewById<Button>(R.id.btn_watch_credits)
val pipButton = binding.playerView.findViewById<ImageButton>(R.id.btn_pip)
val lockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_lockview)
val unlockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_unlock)
@ -133,13 +138,80 @@ class PlayerActivity : BasePlayerActivity() {
// Title
videoNameTextView.text = currentItemTitle
// Skip Intro button
skipIntroButton.isVisible = !isInPictureInPictureMode && currentIntro != null
skipIntroButton.setOnClickListener {
currentIntro?.let {
binding.playerView.player?.seekTo((it.introEnd * 1000).toLong())
// Skip Button
if (currentSegment != oldSegment) buttonPressed = false
// Button Visibility and Text
when (currentSegment?.type) {
"intro" -> {
skipButton.text =
getString(CoreR.string.skip_intro_button)
skipButton.isVisible =
!isInPictureInPictureMode && !buttonPressed && (showSkip == true || (binding.playerView.isControllerFullyVisible && currentSegment?.skip == true))
watchCreditsButton.isVisible = false
}
"credit" -> {
skipButton.text =
if (binding.playerView.player?.hasNextMediaItem() == true) {
getString(CoreR.string.skip_credit_button)
} else {
getString(CoreR.string.skip_credit_button_last)
}
skipButton.isVisible =
!isInPictureInPictureMode && !buttonPressed && currentSegment?.skip == true && !binding.playerView.isControllerFullyVisible
watchCreditsButton.isVisible = skipButton.isVisible
}
else -> {
skipButton.isVisible = false
watchCreditsButton.isVisible = false
}
}
binding.playerView.setControllerVisibilityListener(
PlayerView.ControllerVisibilityListener { visibility ->
when (currentSegment?.type) {
"intro" -> {
skipButton.isVisible =
!buttonPressed && (showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
}
"credit" -> {
skipButton.isVisible =
!buttonPressed && currentSegment?.skip == true && visibility == View.GONE
watchCreditsButton.isVisible = skipButton.isVisible
}
}
},
)
// onClick
if (currentSegment?.type == "credit") {
watchCreditsButton.setOnClickListener {
buttonPressed = true
skipButton.isVisible = false
watchCreditsButton.isVisible = false
}
}
skipButton.setOnClickListener {
when (currentSegment?.type) {
"intro" -> {
currentSegment?.let {
binding.playerView.player?.seekTo((it.endTime * 1000).toLong())
}
}
"credit" -> {
if (binding.playerView.player?.hasNextMediaItem() == true) {
binding.playerView.player?.seekToNext()
} else {
finish()
}
}
}
buttonPressed = true
skipButton.isVisible = false
watchCreditsButton.isVisible = false
}
oldSegment = currentSegment
// Trickplay
previewScrubListener?.let {

View file

@ -9,12 +9,12 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.nomadics9.ananas.bindCardItemImage
import com.nomadics9.ananas.core.R
import com.nomadics9.ananas.databinding.HomeEpisodeItemBinding
import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.isDownloaded
import com.nomadics9.ananas.core.R as CoreR
class HomeEpisodeListAdapter(private val onClickListener: (item: FindroidItem) -> Unit) : ListAdapter<FindroidItem, HomeEpisodeListAdapter.EpisodeViewHolder>(DiffCallback) {
class EpisodeViewHolder(
@ -42,9 +42,9 @@ class HomeEpisodeListAdapter(private val onClickListener: (item: FindroidItem) -
is FindroidEpisode -> {
binding.primaryName.text = item.seriesName
binding.secondaryName.text = if (item.indexNumberEnd == null) {
parent.resources.getString(R.string.episode_name_extended, item.parentIndexNumber, item.indexNumber, item.name)
parent.resources.getString(CoreR.string.episode_name_extended, item.parentIndexNumber, item.indexNumber, item.name)
} else {
parent.resources.getString(R.string.episode_name_extended_with_end, item.parentIndexNumber, item.indexNumber, item.indexNumberEnd, item.name)
parent.resources.getString(CoreR.string.episode_name_extended_with_end, item.parentIndexNumber, item.indexNumber, item.indexNumberEnd, item.name)
}
}
}

View file

@ -8,11 +8,11 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.nomadics9.ananas.bindItemImage
import com.nomadics9.ananas.core.R
import com.nomadics9.ananas.databinding.BaseItemBinding
import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.isDownloaded
import com.nomadics9.ananas.core.R as CoreR
class ViewItemListAdapter(
private val onClickListener: (item: FindroidItem) -> Unit,
@ -27,7 +27,7 @@ class ViewItemListAdapter(
if (item.unplayedItemCount != null && item.unplayedItemCount!! > 0) View.VISIBLE else View.GONE
if (fixedWidth) {
binding.itemLayout.layoutParams.width =
parent.resources.getDimension(R.dimen.overview_media_width).toInt()
parent.resources.getDimension(CoreR.dimen.overview_media_width).toInt()
(binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
}
@ -67,4 +67,4 @@ class ViewItemListAdapter(
}
holder.bind(item, fixedWidth)
}
}
}

View file

@ -8,11 +8,11 @@ import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.nomadics9.ananas.bindItemImage
import com.nomadics9.ananas.core.R
import com.nomadics9.ananas.databinding.BaseItemBinding
import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.isDownloaded
import com.nomadics9.ananas.core.R as CoreR
class ViewItemPagingAdapter(
private val onClickListener: (item: FindroidItem) -> Unit,
@ -28,7 +28,7 @@ class ViewItemPagingAdapter(
if (item.unplayedItemCount != null && item.unplayedItemCount!! > 0) View.VISIBLE else View.GONE
if (fixedWidth) {
binding.itemLayout.layoutParams.width =
parent.resources.getDimension(R.dimen.overview_media_width).toInt()
parent.resources.getDimension(CoreR.dimen.overview_media_width).toInt()
(binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
}

View file

@ -48,24 +48,6 @@
android:textColor="@android:color/white"
android:textSize="14sp" />
<Button
android:id="@+id/btn_skip_intro"
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginEnd="24dp"
android:layout_marginBottom="64dp"
android:text="@string/player_controls_skip_intro"
android:textColor="@android:color/white"
android:visibility="gone"
app:backgroundTint="@color/player_background"
app:icon="@drawable/ic_skip_forward"
app:iconGravity="end"
app:iconTint="@android:color/white"
app:strokeColor="@android:color/white"
tools:visibility="visible" />
</androidx.media3.ui.AspectRatioFrameLayout>
<androidx.media3.ui.SubtitleView
@ -89,4 +71,33 @@
android:layout_height="match_parent"
app:animation_enabled="false"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end|bottom"
android:layout_marginEnd="24dp"
android:layout_marginBottom="64dp">
<Button
android:id="@+id/btn_watch_credits"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:text="@string/watch_credits"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/btn_skip_intro"
style="@style/Widget.Material3.Button.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:icon="@drawable/ic_skip_forward"
tools:visibility="visible" />
</LinearLayout>
</merge>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="watch_credits">Watch credits</string>
</resources>

View file

@ -20,11 +20,11 @@ import com.nomadics9.ananas.models.toFindroidEpisodeDto
import com.nomadics9.ananas.models.toFindroidMediaStreamDto
import com.nomadics9.ananas.models.toFindroidMovieDto
import com.nomadics9.ananas.models.toFindroidSeasonDto
import com.nomadics9.ananas.models.toFindroidSegmentsDto
import com.nomadics9.ananas.models.toFindroidShowDto
import com.nomadics9.ananas.models.toFindroidSourceDto
import com.nomadics9.ananas.models.toFindroidTrickplayInfoDto
import com.nomadics9.ananas.models.toFindroidUserDataDto
import com.nomadics9.ananas.models.toIntroDto
import com.nomadics9.ananas.repository.JellyfinRepository
import java.io.File
import java.util.UUID
@ -47,7 +47,7 @@ class DownloaderImpl(
): Pair<Long, UiText?> {
try {
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
val intro = jellyfinRepository.getIntroTimestamps(item.id)
val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
val trickplayInfo = if (item is FindroidSources) {
item.trickplayInfo?.get(sourceId)
} else {
@ -79,8 +79,8 @@ class DownloaderImpl(
if (trickplayInfo != null) {
downloadTrickplayData(item.id, sourceId, trickplayInfo)
}
if (intro != null) {
database.insertIntro(intro.toIntroDto(item.id))
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name)
@ -108,8 +108,8 @@ class DownloaderImpl(
if (trickplayInfo != null) {
downloadTrickplayData(item.id, sourceId, trickplayInfo)
}
if (intro != null) {
database.insertIntro(intro.toIntroDto(item.id))
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name)
@ -171,7 +171,7 @@ class DownloaderImpl(
database.deleteUserData(item.id)
database.deleteIntro(item.id)
database.deleteSegments(item.id)
File(context.filesDir, "trickplay/${item.id}").deleteRecursively()
}

View file

@ -135,7 +135,7 @@
<string name="add">Aggiungi</string>
<string name="quick_connect">Connessione Rapida</string>
<string name="pref_player_intro_skipper">Salta intro</string>
<string name="pref_player_intro_skipper_summary">Richiede il plugin Intro Skipper di ConfusedPolarBear installato sul server</string>
<string name="pref_player_intro_skipper_summary">Richiede il plugin <b>Intro Skipper</b> di <i>jumoog</i> installato sul server</string>
<string name="player_gestures_seek_summary">Scorri orizzontalmente per posizionarti avanti o indietro</string>
<string name="player_gestures_seek">Gesto posizionamento</string>
<string name="audio">Audio</string>
@ -147,6 +147,8 @@
<string name="extra_info">Mostra più informazioni</string>
<string name="amoled_theme">Tema scuro AMOLED</string>
<string name="amoled_theme_summary">Usa il tema AMOLED con lo sfondo nero</string>
<string name="pref_player_trickplay">Anteprima</string>
<string name="pref_player_trickplay_summary">Richiede il plugin Jellyscrub di nicknsy installato sul server</string>
<string name="size">Dimensione</string>
<string name="privacy_policy_notice">Utilizzando Findroid accetti l\'<a href="https://raw.githubusercontent.com/nomadics9/ananas/main/PRIVACY">informativa sulla privacy</a> che afferma che non raccogliamo alcun dato</string>
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
@ -183,6 +185,9 @@
<string name="live_tv">Diretta TV</string>
<string name="play">Riproduci</string>
<string name="remove_from_favorites">Rimuovi dai preferiti</string>
<string name="skip_intro_button">Salta intro</string>
<string name="skip_credit_button">Prossimo episodio</string>
<string name="skip_credit_button_last">Chiudi player</string>
<string name="player_gestures_chapter_skip">Gesto per le scene</string>
<string name="pref_player_chapter_markers">Marcatori delle scene</string>
<string name="pref_player_chapter_markers_summary">Mostra i marcatori delle scene sulla timebar</string>

View file

@ -200,4 +200,7 @@
<string name="download_season_dialog_question">Which episodes do you want to download?</string>
<string name="download_season_dialog_download_all">All Episodes</string>
<string name="download_season_dialog_download_unwatched">Unwatched Episodes</string>
<string name="skip_intro_button">Skip Intro</string>
<string name="skip_credit_button">Next episode</string>
<string name="skip_credit_button_last">Close player</string>
</resources>

View file

@ -0,0 +1,855 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "8b765f00961c1833893fc376339db699",
"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": "segments",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `segments` TEXT NOT NULL, PRIMARY KEY(`itemId`))",
"fields": [
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "segments",
"columnName": "segments",
"affinity": "TEXT",
"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": []
},
{
"tableName": "trickplayInfos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` TEXT NOT NULL, `width` INTEGER NOT NULL, `height` INTEGER NOT NULL, `tileWidth` INTEGER NOT NULL, `tileHeight` INTEGER NOT NULL, `thumbnailCount` INTEGER NOT NULL, `interval` INTEGER NOT NULL, `bandwidth` INTEGER NOT NULL, PRIMARY KEY(`sourceId`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "sourceId",
"columnName": "sourceId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "width",
"columnName": "width",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tileWidth",
"columnName": "tileWidth",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tileHeight",
"columnName": "tileHeight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "thumbnailCount",
"columnName": "thumbnailCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "interval",
"columnName": "interval",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bandwidth",
"columnName": "bandwidth",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"sourceId"
]
},
"indices": [],
"foreignKeys": [
{
"table": "sources",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"sourceId"
],
"referencedColumns": [
"id"
]
}
]
}
],
"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, '8b765f00961c1833893fc376339db699')"
]
}
}

View file

@ -2,6 +2,7 @@ package com.nomadics9.ananas.database
import androidx.room.TypeConverter
import com.nomadics9.ananas.models.FindroidChapter
import com.nomadics9.ananas.models.FindroidSegment
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jellyfin.sdk.model.DateTime
@ -38,4 +39,14 @@ class Converters {
fun fromStringToFindroidChapters(value: String?): List<FindroidChapter>? {
return value?.let { Json.decodeFromString(value) }
}
@TypeConverter
fun fromFindroidSegmentsToString(value: List<FindroidSegment>?): String? {
return value?.let { Json.encodeToString(value) }
}
@TypeConverter
fun fromStringToFindroidSegments(value: String?): List<FindroidSegment>? {
return value?.let { Json.decodeFromString(value) }
}
}

View file

@ -10,22 +10,23 @@ import com.nomadics9.ananas.models.FindroidEpisodeDto
import com.nomadics9.ananas.models.FindroidMediaStreamDto
import com.nomadics9.ananas.models.FindroidMovieDto
import com.nomadics9.ananas.models.FindroidSeasonDto
import com.nomadics9.ananas.models.FindroidSegmentsDto
import com.nomadics9.ananas.models.FindroidShowDto
import com.nomadics9.ananas.models.FindroidSourceDto
import com.nomadics9.ananas.models.FindroidTrickplayInfoDto
import com.nomadics9.ananas.models.FindroidUserDataDto
import com.nomadics9.ananas.models.IntroDto
import com.nomadics9.ananas.models.Server
import com.nomadics9.ananas.models.ServerAddress
import com.nomadics9.ananas.models.User
@Database(
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, IntroDto::class, FindroidUserDataDto::class, FindroidTrickplayInfoDto::class],
version = 5,
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, FindroidSegmentsDto::class, FindroidUserDataDto::class, FindroidTrickplayInfoDto::class],
version = 6,
autoMigrations = [
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5, spec = ServerDatabase.TrickplayMigration::class),
AutoMigration(from = 5, to = 6, spec = ServerDatabase.IntrosMigration::class),
],
)
@TypeConverters(Converters::class)
@ -34,4 +35,7 @@ abstract class ServerDatabase : RoomDatabase() {
@DeleteTable(tableName = "trickPlayManifests")
class TrickplayMigration : AutoMigrationSpec
@DeleteTable(tableName = "intros")
class IntrosMigration : AutoMigrationSpec
}

View file

@ -10,11 +10,11 @@ import com.nomadics9.ananas.models.FindroidEpisodeDto
import com.nomadics9.ananas.models.FindroidMediaStreamDto
import com.nomadics9.ananas.models.FindroidMovieDto
import com.nomadics9.ananas.models.FindroidSeasonDto
import com.nomadics9.ananas.models.FindroidSegmentsDto
import com.nomadics9.ananas.models.FindroidShowDto
import com.nomadics9.ananas.models.FindroidSourceDto
import com.nomadics9.ananas.models.FindroidTrickplayInfoDto
import com.nomadics9.ananas.models.FindroidUserDataDto
import com.nomadics9.ananas.models.IntroDto
import com.nomadics9.ananas.models.Server
import com.nomadics9.ananas.models.ServerAddress
import com.nomadics9.ananas.models.ServerWithAddressAndUser
@ -205,13 +205,13 @@ interface ServerDatabaseDao {
fun deleteEpisodesBySeasonId(seasonId: UUID)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertIntro(intro: IntroDto)
fun insertSegments(segment: FindroidSegmentsDto)
@Query("SELECT * FROM intros WHERE itemId = :itemId")
fun getIntro(itemId: UUID): IntroDto?
@Query("SELECT * FROM segments WHERE itemId = :itemId")
fun getSegments(itemId: UUID): FindroidSegmentsDto?
@Query("DELETE FROM intros WHERE itemId = :itemId")
fun deleteIntro(itemId: UUID)
@Query("DELETE FROM segments WHERE itemId = :itemId")
fun deleteSegments(itemId: UUID)
@Query("SELECT * FROM seasons")
fun getSeasons(): List<FindroidSeasonDto>

View file

@ -0,0 +1,39 @@
package com.nomadics9.ananas.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class FindroidSegments(
@SerialName("Introduction")
val intro: FindroidSegment?,
@SerialName("Credits")
val credit: FindroidSegment?,
)
@Serializable
data class FindroidSegment(
val type: String = "none",
val skip: Boolean = false,
@SerialName("IntroStart")
val startTime: Double,
@SerialName("IntroEnd")
val endTime: Double,
@SerialName("ShowSkipPromptAt")
val showAt: Double,
@SerialName("HideSkipPromptAt")
val hideAt: Double,
)
fun FindroidSegmentsDto.toFindroidSegments(): List<FindroidSegment> {
return segments.map { segment ->
FindroidSegment(
type = segment.type,
skip = segment.skip,
startTime = segment.startTime,
endTime = segment.endTime,
showAt = segment.showAt,
hideAt = segment.hideAt,
)
}
}

View file

@ -0,0 +1,19 @@
package com.nomadics9.ananas.models
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "segments")
data class FindroidSegmentsDto(
@PrimaryKey
val itemId: UUID,
val segments: List<FindroidSegment>,
)
fun List<FindroidSegment>.toFindroidSegmentsDto(itemId: UUID): FindroidSegmentsDto {
return FindroidSegmentsDto(
itemId = itemId,
segments = this,
)
}

View file

@ -1,25 +0,0 @@
package com.nomadics9.ananas.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Intro(
@SerialName("IntroStart")
val introStart: Double,
@SerialName("IntroEnd")
val introEnd: Double,
@SerialName("ShowSkipPromptAt")
val showSkipPromptAt: Double,
@SerialName("HideSkipPromptAt")
val hideSkipPromptAt: Double,
)
fun IntroDto.toIntro(): Intro {
return Intro(
introStart = start,
introEnd = end,
showSkipPromptAt = showAt,
hideSkipPromptAt = hideAt,
)
}

View file

@ -1,25 +0,0 @@
package com.nomadics9.ananas.models
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "intros")
data class IntroDto(
@PrimaryKey
val itemId: UUID,
val start: Double,
val end: Double,
val showAt: Double,
val hideAt: Double,
)
fun Intro.toIntroDto(itemId: UUID): IntroDto {
return IntroDto(
itemId = itemId,
start = introStart,
end = introEnd,
showAt = showSkipPromptAt,
hideAt = hideSkipPromptAt,
)
}

View file

@ -6,9 +6,9 @@ import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.FindroidSeason
import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.models.FindroidShow
import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.Intro
import com.nomadics9.ananas.models.SortBy
import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.model.api.BaseItemDto
@ -83,7 +83,7 @@ interface JellyfinRepository {
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String
suspend fun getIntroTimestamps(itemId: UUID): Intro?
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?

View file

@ -12,18 +12,19 @@ import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.FindroidSeason
import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.models.FindroidSegments
import com.nomadics9.ananas.models.FindroidShow
import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.Intro
import com.nomadics9.ananas.models.SortBy
import com.nomadics9.ananas.models.toFindroidCollection
import com.nomadics9.ananas.models.toFindroidEpisode
import com.nomadics9.ananas.models.toFindroidItem
import com.nomadics9.ananas.models.toFindroidMovie
import com.nomadics9.ananas.models.toFindroidSeason
import com.nomadics9.ananas.models.toFindroidSegments
import com.nomadics9.ananas.models.toFindroidShow
import com.nomadics9.ananas.models.toFindroidSource
import com.nomadics9.ananas.models.toIntro
import io.ktor.util.toByteArray
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -336,12 +337,12 @@ class JellyfinRepositoryImpl(
}
}
override suspend fun getIntroTimestamps(itemId: UUID): Intro? =
override suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>? =
withContext(Dispatchers.IO) {
val intro = database.getIntro(itemId)?.toIntro()
val segments = database.getSegments(itemId)?.toFindroidSegments()
if (intro != null) {
return@withContext intro
if (segments != null) {
return@withContext segments
}
// https://github.com/ConfusedPolarBear/intro-skipper/blob/master/docs/api.md
@ -349,10 +350,37 @@ class JellyfinRepositoryImpl(
pathParameters["itemId"] = itemId
try {
return@withContext jellyfinApi.api.get<Intro>(
"/Episode/{itemId}/IntroTimestamps/v1",
val segmentToConvert = jellyfinApi.api.get<FindroidSegments>(
"/Episode/{itemId}/IntroSkipperSegments",
pathParameters,
).content
val segmentConverted = mutableListOf(
segmentToConvert.intro!!.let {
FindroidSegment(
type = "intro",
skip = true,
startTime = it.startTime,
endTime = it.endTime,
showAt = it.showAt,
hideAt = it.hideAt,
)
},
segmentToConvert.credit!!.let {
FindroidSegment(
type = "credit",
skip = true,
startTime = it.startTime,
endTime = it.endTime,
showAt = it.showAt,
hideAt = it.hideAt,
)
},
)
Timber.tag("SegmentInfo").d("segmentToConvert: %s", segmentToConvert)
Timber.tag("SegmentInfo").d("segmentConverted: %s", segmentConverted)
return@withContext segmentConverted.toList()
} catch (e: Exception) {
return@withContext null
}

View file

@ -10,16 +10,16 @@ import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.FindroidSeason
import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.models.FindroidShow
import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.Intro
import com.nomadics9.ananas.models.SortBy
import com.nomadics9.ananas.models.toFindroidEpisode
import com.nomadics9.ananas.models.toFindroidMovie
import com.nomadics9.ananas.models.toFindroidSeason
import com.nomadics9.ananas.models.toFindroidSegments
import com.nomadics9.ananas.models.toFindroidShow
import com.nomadics9.ananas.models.toFindroidSource
import com.nomadics9.ananas.models.toIntro
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
@ -177,9 +177,9 @@ class JellyfinRepositoryOfflineImpl(
TODO("Not yet implemented")
}
override suspend fun getIntroTimestamps(itemId: UUID): Intro? =
override suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>? =
withContext(Dispatchers.IO) {
database.getIntro(itemId)?.toIntro()
database.getSegments(itemId)?.toFindroidSegments()
}
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? =

View file

@ -20,7 +20,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel
import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.models.Intro
import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.models.PlayerChapter
import com.nomadics9.ananas.models.PlayerItem
import com.nomadics9.ananas.models.Trickplay
@ -57,7 +57,8 @@ constructor(
private val _uiState = MutableStateFlow(
UiState(
currentItemTitle = "",
currentIntro = null,
currentSegment = null,
showSkip = false,
currentTrickplay = null,
currentChapters = null,
fileLoaded = false,
@ -68,11 +69,12 @@ constructor(
private val eventsChannel = Channel<PlayerEvents>()
val eventsChannelFlow = eventsChannel.receiveAsFlow()
private val intros: MutableMap<UUID, Intro> = mutableMapOf()
private val segments: MutableMap<UUID, List<FindroidSegment>> = mutableMapOf()
data class UiState(
val currentItemTitle: String,
val currentIntro: Intro?,
val currentSegment: FindroidSegment?,
val showSkip: Boolean?,
val currentTrickplay: Trickplay?,
val currentChapters: List<PlayerChapter>?,
val fileLoaded: Boolean,
@ -152,9 +154,10 @@ constructor(
}
if (appPreferences.playerIntroSkipper) {
jellyfinRepository.getIntroTimestamps(item.itemId)?.let { intro ->
intros[item.itemId] = intro
jellyfinRepository.getSegmentsTimestamps(item.itemId)?.let { segment ->
segments[item.itemId] = segment
}
Timber.tag("SegmentInfo").d("Segments: %s", segments)
}
Timber.d("Stream url: $streamUrl")
@ -239,24 +242,28 @@ constructor(
handler.postDelayed(this, 5000L)
}
}
val introCheckRunnable = object : Runnable {
val segmentCheckRunnable = object : Runnable {
override fun run() {
if (player.currentMediaItem != null && player.currentMediaItem!!.mediaId.isNotEmpty()) {
val itemId = UUID.fromString(player.currentMediaItem!!.mediaId)
intros[itemId]?.let { intro ->
val seconds = player.currentPosition / 1000.0
if (seconds > intro.showSkipPromptAt && seconds < intro.hideSkipPromptAt) {
_uiState.update { it.copy(currentIntro = intro) }
return@let
}
_uiState.update { it.copy(currentIntro = null) }
val currentMediaItem = player.currentMediaItem
if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) {
val itemId = UUID.fromString(currentMediaItem.mediaId)
val seconds = player.currentPosition / 1000.0
val currentSegment = segments[itemId]?.find { segment -> seconds in segment.startTime..<segment.endTime }
_uiState.update { it.copy(currentSegment = currentSegment) }
Timber.tag("SegmentInfo").d("currentSegment: %s", currentSegment)
if (currentSegment?.type == "intro") {
val showSkip =
currentSegment.let { it.skip && seconds in it.showAt..<it.hideAt }
_uiState.update { it.copy(showSkip = showSkip) }
}
}
handler.postDelayed(this, 1000L)
}
}
handler.post(playbackProgressRunnable)
if (intros.isNotEmpty()) handler.post(introCheckRunnable)
if (segments.isNotEmpty()) handler.post(segmentCheckRunnable)
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
@ -275,7 +282,14 @@ constructor(
} else {
item.name
}
_uiState.update { it.copy(currentItemTitle = itemTitle, currentChapters = item.chapters, fileLoaded = false) }
_uiState.update {
it.copy(
currentItemTitle = itemTitle,
currentSegment = null,
currentChapters = item.chapters,
fileLoaded = false,
)
}
jellyfinRepository.postPlaybackStart(item.itemId)

View file

@ -7,7 +7,6 @@
<string name="player_controls_rewind">Rebobinar</string>&gt;
<string name="player_controls_exit">Salir de reproductor</string>
<string name="player_controls_fast_forward">Avanzar</string>
<string name="player_controls_skip_intro">Saltar intro</string>
<string name="external">Externo</string>
<string name="player_controls_skip_back">Saltar atrás</string>
<string name="player_controls_play_pause">Reproducir pausar</string>

View file

@ -14,7 +14,6 @@
<string name="player_trickplay">Trickplay</string>
<string name="player_controls_play_pause">Начало пауза</string>
<string name="player_controls_exit">Излез от плейъра</string>
<string name="player_controls_skip_intro">Пропусни интро</string>
<string name="player_controls_picture_in_picture">Влез в режим картина-в-картина</string>
<string name="none">Нищо</string>
</resources>

View file

@ -9,7 +9,6 @@
<string name="player_controls_skip_back">Přeskočit zpět</string>
<string name="player_controls_play_pause">Přehrát pauza</string>
<string name="player_controls_rewind">Přetočit</string>
<string name="player_controls_skip_intro">Přeskočit úvod</string>
<string name="player_controls_exit">Ukončit přehrávač</string>
<string name="player_controls_fast_forward">Rychlý posun vpřed</string>
<string name="player_controls_skip_forward">Přeskočit vpřed</string>

View file

@ -10,7 +10,6 @@
<string name="player_controls_lock">Låser afspilleren</string>
<string name="player_controls_skip_back">Hop tilbage</string>
<string name="player_controls_exit">Stop afspiller</string>
<string name="player_controls_skip_intro">Spring over intro</string>
<string name="player_controls_fast_forward">Spol frem</string>
<string name="player_controls_skip_forward">Spring frem</string>
<string name="player_trickplay">Spole afspille</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Player verlassen</string>
<string name="player_controls_rewind">Wiederholen</string>
<string name="player_controls_fast_forward">Vorspulen</string>
<string name="player_controls_skip_intro">Intro überspringen</string>
<string name="player_controls_lock">Player sperren</string>
<string name="player_controls_skip_back">Zurückspulen</string>
<string name="player_controls_play_pause">Starten/Anhalten</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Salir de reproductor</string>
<string name="player_controls_rewind">Atrasar</string>
<string name="player_controls_fast_forward">Avanzar</string>
<string name="player_controls_skip_intro">Saltar intro</string>
<string name="player_controls_skip_forward">Saltar adelante</string>
<string name="player_trickplay">Avance</string>
<string name="player_controls_lock">Bloquea el reproductor</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Salir del reproductor</string>
<string name="player_controls_rewind">Rebobinar</string>
<string name="player_controls_fast_forward">Avanzar</string>
<string name="player_controls_skip_intro">Saltar introducción</string>
<string name="player_controls_skip_forward">Saltar adelante</string>
<string name="player_controls_lock">Bloquea el reproductor</string>
<string name="player_controls_play_pause">Reproducir pausar</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Quitter le lecteur</string>
<string name="player_controls_rewind">Rembobiner</string>
<string name="player_controls_fast_forward">Avance rapide</string>
<string name="player_controls_skip_intro">Ignorer l\'introduction</string>
<string name="player_controls_lock">Verrouille le lecteur</string>
<string name="player_controls_play_pause">Lecture / Pause</string>
<string name="player_controls_skip_back">Retour en arrière</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Kilépés a lejátszóból</string>
<string name="player_controls_rewind">Visszatekerés</string>
<string name="player_controls_fast_forward">Előrepörgetés</string>
<string name="player_controls_skip_intro">Intro kihagyása</string>
<string name="player_controls_lock">Zárolja a lejátszót</string>
<string name="player_controls_skip_back">Ugrás vissza</string>
<string name="player_controls_skip_forward">Ugrás előre</string>

View file

@ -14,7 +14,6 @@
<string name="player_controls_rewind">Riavvolgi</string>
<string name="player_controls_lock">Blocca il player</string>
<string name="player_controls_exit">Esci dal player</string>
<string name="player_controls_skip_intro">Salta intro</string>
<string name="player_controls_picture_in_picture">Attiva picture in picture</string>
<string name="none">Nessuno</string>
</resources>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">צא מהנגן</string>
<string name="player_controls_rewind">הרצה אחורה</string>
<string name="player_controls_fast_forward">הרצה קדימה</string>
<string name="player_controls_skip_intro">דלג פתיח</string>
<string name="player_controls_skip_forward">דלג קדימה</string>
<string name="player_controls_lock">נועל את הנגן</string>
<string name="player_controls_skip_back">דלג אחורה</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_rewind">되감기</string>
<string name="player_controls_fast_forward">빨리 감기</string>
<string name="player_controls_exit">플레이어 나가기</string>
<string name="player_controls_skip_intro">오프닝 스킵</string>
<string name="player_controls_lock">플레이어 잠금</string>
<string name="player_trickplay">Trickplay</string>
<string name="player_controls_skip_back">뒤로 건너뛰기</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Sluit speler</string>
<string name="player_controls_rewind">Terugspoelen</string>
<string name="player_controls_fast_forward">Snel vooruit</string>
<string name="player_controls_skip_intro">Intro overslaan</string>
<string name="player_controls_progress">Voortgangsbalk</string>
<string name="none">Geen</string>
<string name="player_controls_picture_in_picture">Scherm-in-scherm openen</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Zamknij odtwarzacz</string>
<string name="player_controls_rewind">Przewiń</string>
<string name="player_controls_fast_forward">Przewiń do przodu</string>
<string name="player_controls_skip_intro">Pomiń czołówkę</string>
<string name="player_controls_lock">Zablokuj odtwarzacz</string>
<string name="player_controls_skip_back">Skocz do tyłu</string>
<string name="player_trickplay">Trickplay</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Sair do reprodutor</string>
<string name="player_controls_rewind">Retroceder</string>
<string name="player_controls_fast_forward">Avanço rápido</string>
<string name="player_controls_skip_intro">Pular introdução</string>
<string name="player_controls_lock">Bloqueia o reprodutor</string>
<string name="player_controls_skip_back">Saltar para trás</string>
<string name="player_trickplay">Miniatura de pré-visualização</string>

View file

@ -14,7 +14,6 @@
<string name="player_controls_progress">Barra de progresso</string>
<string name="player_controls_skip_forward">Avançar</string>
<string name="player_controls_skip_back">Pular para trás</string>
<string name="player_controls_skip_intro">Pular introdução</string>
<string name="player_controls_picture_in_picture">Insira imagem em imagem</string>
<string name="none">Nenhum</string>
</resources>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Выйти из проигрывателя</string>
<string name="player_controls_rewind">Перемотка</string>
<string name="player_controls_fast_forward">Быстрая перемотка</string>
<string name="player_controls_skip_intro">Пропустить заставку</string>
<string name="player_controls_lock">Блокировка</string>
<string name="player_controls_skip_back">Перейти назад</string>
<string name="player_controls_play_pause">Плей пауза</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Zavrieť prehrávač</string>
<string name="player_controls_rewind">Pretočiť dozadu</string>
<string name="player_controls_fast_forward">Pretočiť dopredu</string>
<string name="player_controls_skip_intro">Preskočiť úvodnú zvučku</string>
<string name="player_controls_lock">Zamkne prehrávač</string>
<string name="player_controls_skip_back">Preskočiť späť</string>
<string name="player_controls_skip_forward">Preskočiť dopredu</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Izhod iz predvajalnika</string>
<string name="player_controls_rewind">Previj nazaj</string>
<string name="player_controls_fast_forward">Navijaj naprej</string>
<string name="player_controls_skip_intro">Preskoči uvod</string>
<string name="player_controls_lock">Zaklene predvajalnik</string>
<string name="player_controls_skip_back">Preskoči nazaj</string>
<string name="player_controls_play_pause">Predvajaj ustavi</string>

View file

@ -8,5 +8,4 @@
<string name="player_controls_exit">Avsluta spelare</string>
<string name="player_controls_rewind">Spola tillbaka</string>
<string name="player_controls_fast_forward">Spola framåt</string>
<string name="player_controls_skip_intro">Hoppa över intro</string>
</resources>

View file

@ -8,5 +8,4 @@
<string name="player_controls_rewind">Відмотка</string>
<string name="player_controls_fast_forward">Швидке перемотування</string>
<string name="player_controls_exit">Вийти з плеєра</string>
<string name="player_controls_skip_intro">Пропустити вступ</string>
</resources>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Thoát khỏi trình xem</string>
<string name="player_controls_rewind">Tua lùi</string>
<string name="player_controls_fast_forward">Tua tới</string>
<string name="player_controls_skip_intro">Bỏ qua đoạn mở đầu</string>
<string name="player_controls_skip_back">Bỏ qua / Trở về</string>
<string name="player_controls_lock">Khoá trình phát</string>
<string name="player_controls_play_pause">Phát / Tạm dừng</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">退出播放器</string>
<string name="player_controls_rewind">快退</string>
<string name="player_controls_fast_forward">快进</string>
<string name="player_controls_skip_intro">跳过片头</string>
<string name="player_controls_lock">锁定播放器</string>
<string name="player_controls_skip_back">跳回</string>
<string name="player_controls_play_pause">播放暂停</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">關閉播放器</string>
<string name="player_controls_rewind">倒帶</string>
<string name="player_controls_fast_forward">快轉</string>
<string name="player_controls_skip_intro">跳過片頭</string>
<string name="player_controls_lock">鎖定播放器</string>
<string name="player_controls_skip_back">跳回</string>
<string name="player_controls_play_pause">播放暫停</string>

View file

@ -11,7 +11,6 @@
<string name="player_controls_play_pause">Play pause</string>
<string name="player_controls_rewind">Rewind</string>
<string name="player_controls_exit">Exit player</string>
<string name="player_controls_skip_intro">Skip Intro</string>
<string name="player_controls_fast_forward">Fast forward</string>
<string name="player_controls_skip_forward">Skip forward</string>
<string name="player_trickplay">Trickplay</string>