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.databinding.ActivityPlayerBinding
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
import com.nomadics9.ananas.dialogs.TrackSelectionDialogFragment 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.PlayerActivityViewModel
import com.nomadics9.ananas.viewmodels.PlayerEvents import com.nomadics9.ananas.viewmodels.PlayerEvents
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.nomadics9.ananas.utils.PlayerGestureHelper
import com.nomadics9.ananas.utils.PreviewScrubListener
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import com.nomadics9.ananas.core.R as CoreR
var isControlsLocked: Boolean = false var isControlsLocked: Boolean = false
@ -58,6 +60,8 @@ class PlayerActivity : BasePlayerActivity() {
override val viewModel: PlayerActivityViewModel by viewModels() override val viewModel: PlayerActivityViewModel by viewModels()
private var previewScrubListener: PreviewScrubListener? = null private var previewScrubListener: PreviewScrubListener? = null
private var wasZoom: Boolean = false private var wasZoom: Boolean = false
private var oldSegment: FindroidSegment? = null
private var buttonPressed: Boolean = false
private val isPipSupported by lazy { private val isPipSupported by lazy {
// Check if device has PiP feature // 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 audioButton = binding.playerView.findViewById<ImageButton>(R.id.btn_audio_track)
val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle) val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle)
val speedButton = binding.playerView.findViewById<ImageButton>(R.id.btn_speed) 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 pipButton = binding.playerView.findViewById<ImageButton>(R.id.btn_pip)
val lockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_lockview) val lockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_lockview)
val unlockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_unlock) val unlockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_unlock)
@ -133,13 +138,80 @@ class PlayerActivity : BasePlayerActivity() {
// Title // Title
videoNameTextView.text = currentItemTitle videoNameTextView.text = currentItemTitle
// Skip Intro button // Skip Button
skipIntroButton.isVisible = !isInPictureInPictureMode && currentIntro != null if (currentSegment != oldSegment) buttonPressed = false
skipIntroButton.setOnClickListener { // Button Visibility and Text
currentIntro?.let { when (currentSegment?.type) {
binding.playerView.player?.seekTo((it.introEnd * 1000).toLong()) "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 // Trickplay
previewScrubListener?.let { previewScrubListener?.let {

View file

@ -9,12 +9,12 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nomadics9.ananas.bindCardItemImage import com.nomadics9.ananas.bindCardItemImage
import com.nomadics9.ananas.core.R
import com.nomadics9.ananas.databinding.HomeEpisodeItemBinding import com.nomadics9.ananas.databinding.HomeEpisodeItemBinding
import com.nomadics9.ananas.models.FindroidEpisode import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.isDownloaded 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 HomeEpisodeListAdapter(private val onClickListener: (item: FindroidItem) -> Unit) : ListAdapter<FindroidItem, HomeEpisodeListAdapter.EpisodeViewHolder>(DiffCallback) {
class EpisodeViewHolder( class EpisodeViewHolder(
@ -42,9 +42,9 @@ class HomeEpisodeListAdapter(private val onClickListener: (item: FindroidItem) -
is FindroidEpisode -> { is FindroidEpisode -> {
binding.primaryName.text = item.seriesName binding.primaryName.text = item.seriesName
binding.secondaryName.text = if (item.indexNumberEnd == null) { 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 { } 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.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.nomadics9.ananas.bindItemImage import com.nomadics9.ananas.bindItemImage
import com.nomadics9.ananas.core.R
import com.nomadics9.ananas.databinding.BaseItemBinding import com.nomadics9.ananas.databinding.BaseItemBinding
import com.nomadics9.ananas.models.FindroidEpisode import com.nomadics9.ananas.models.FindroidEpisode
import com.nomadics9.ananas.models.FindroidItem import com.nomadics9.ananas.models.FindroidItem
import com.nomadics9.ananas.models.isDownloaded import com.nomadics9.ananas.models.isDownloaded
import com.nomadics9.ananas.core.R as CoreR
class ViewItemListAdapter( class ViewItemListAdapter(
private val onClickListener: (item: FindroidItem) -> Unit, 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 (item.unplayedItemCount != null && item.unplayedItemCount!! > 0) View.VISIBLE else View.GONE
if (fixedWidth) { if (fixedWidth) {
binding.itemLayout.layoutParams.width = 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 (binding.itemLayout.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin = 0
} }

View file

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

View file

@ -48,24 +48,6 @@
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="14sp" /> 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.AspectRatioFrameLayout>
<androidx.media3.ui.SubtitleView <androidx.media3.ui.SubtitleView
@ -89,4 +71,33 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:animation_enabled="false"/> 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> </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.toFindroidMediaStreamDto
import com.nomadics9.ananas.models.toFindroidMovieDto import com.nomadics9.ananas.models.toFindroidMovieDto
import com.nomadics9.ananas.models.toFindroidSeasonDto import com.nomadics9.ananas.models.toFindroidSeasonDto
import com.nomadics9.ananas.models.toFindroidSegmentsDto
import com.nomadics9.ananas.models.toFindroidShowDto import com.nomadics9.ananas.models.toFindroidShowDto
import com.nomadics9.ananas.models.toFindroidSourceDto import com.nomadics9.ananas.models.toFindroidSourceDto
import com.nomadics9.ananas.models.toFindroidTrickplayInfoDto import com.nomadics9.ananas.models.toFindroidTrickplayInfoDto
import com.nomadics9.ananas.models.toFindroidUserDataDto import com.nomadics9.ananas.models.toFindroidUserDataDto
import com.nomadics9.ananas.models.toIntroDto
import com.nomadics9.ananas.repository.JellyfinRepository import com.nomadics9.ananas.repository.JellyfinRepository
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
@ -47,7 +47,7 @@ class DownloaderImpl(
): Pair<Long, UiText?> { ): Pair<Long, UiText?> {
try { try {
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } 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) { val trickplayInfo = if (item is FindroidSources) {
item.trickplayInfo?.get(sourceId) item.trickplayInfo?.get(sourceId)
} else { } else {
@ -79,8 +79,8 @@ class DownloaderImpl(
if (trickplayInfo != null) { if (trickplayInfo != null) {
downloadTrickplayData(item.id, sourceId, trickplayInfo) downloadTrickplayData(item.id, sourceId, trickplayInfo)
} }
if (intro != null) { if (segments != null) {
database.insertIntro(intro.toIntroDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
val request = DownloadManager.Request(source.path.toUri()) val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name) .setTitle(item.name)
@ -108,8 +108,8 @@ class DownloaderImpl(
if (trickplayInfo != null) { if (trickplayInfo != null) {
downloadTrickplayData(item.id, sourceId, trickplayInfo) downloadTrickplayData(item.id, sourceId, trickplayInfo)
} }
if (intro != null) { if (segments != null) {
database.insertIntro(intro.toIntroDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
} }
val request = DownloadManager.Request(source.path.toUri()) val request = DownloadManager.Request(source.path.toUri())
.setTitle(item.name) .setTitle(item.name)
@ -171,7 +171,7 @@ class DownloaderImpl(
database.deleteUserData(item.id) database.deleteUserData(item.id)
database.deleteIntro(item.id) database.deleteSegments(item.id)
File(context.filesDir, "trickplay/${item.id}").deleteRecursively() File(context.filesDir, "trickplay/${item.id}").deleteRecursively()
} }

View file

@ -135,7 +135,7 @@
<string name="add">Aggiungi</string> <string name="add">Aggiungi</string>
<string name="quick_connect">Connessione Rapida</string> <string name="quick_connect">Connessione Rapida</string>
<string name="pref_player_intro_skipper">Salta intro</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_summary">Scorri orizzontalmente per posizionarti avanti o indietro</string>
<string name="player_gestures_seek">Gesto posizionamento</string> <string name="player_gestures_seek">Gesto posizionamento</string>
<string name="audio">Audio</string> <string name="audio">Audio</string>
@ -147,6 +147,8 @@
<string name="extra_info">Mostra più informazioni</string> <string name="extra_info">Mostra più informazioni</string>
<string name="amoled_theme">Tema scuro AMOLED</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="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="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="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> <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="live_tv">Diretta TV</string>
<string name="play">Riproduci</string> <string name="play">Riproduci</string>
<string name="remove_from_favorites">Rimuovi dai preferiti</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="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">Marcatori delle scene</string>
<string name="pref_player_chapter_markers_summary">Mostra i marcatori delle scene sulla timebar</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_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_all">All Episodes</string>
<string name="download_season_dialog_download_unwatched">Unwatched 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> </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 androidx.room.TypeConverter
import com.nomadics9.ananas.models.FindroidChapter import com.nomadics9.ananas.models.FindroidChapter
import com.nomadics9.ananas.models.FindroidSegment
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jellyfin.sdk.model.DateTime import org.jellyfin.sdk.model.DateTime
@ -38,4 +39,14 @@ class Converters {
fun fromStringToFindroidChapters(value: String?): List<FindroidChapter>? { fun fromStringToFindroidChapters(value: String?): List<FindroidChapter>? {
return value?.let { Json.decodeFromString(value) } 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.FindroidMediaStreamDto
import com.nomadics9.ananas.models.FindroidMovieDto import com.nomadics9.ananas.models.FindroidMovieDto
import com.nomadics9.ananas.models.FindroidSeasonDto import com.nomadics9.ananas.models.FindroidSeasonDto
import com.nomadics9.ananas.models.FindroidSegmentsDto
import com.nomadics9.ananas.models.FindroidShowDto import com.nomadics9.ananas.models.FindroidShowDto
import com.nomadics9.ananas.models.FindroidSourceDto import com.nomadics9.ananas.models.FindroidSourceDto
import com.nomadics9.ananas.models.FindroidTrickplayInfoDto import com.nomadics9.ananas.models.FindroidTrickplayInfoDto
import com.nomadics9.ananas.models.FindroidUserDataDto import com.nomadics9.ananas.models.FindroidUserDataDto
import com.nomadics9.ananas.models.IntroDto
import com.nomadics9.ananas.models.Server import com.nomadics9.ananas.models.Server
import com.nomadics9.ananas.models.ServerAddress import com.nomadics9.ananas.models.ServerAddress
import com.nomadics9.ananas.models.User import com.nomadics9.ananas.models.User
@Database( @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], 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 = 5, version = 6,
autoMigrations = [ autoMigrations = [
AutoMigration(from = 2, to = 3), AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4), AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5, spec = ServerDatabase.TrickplayMigration::class), AutoMigration(from = 4, to = 5, spec = ServerDatabase.TrickplayMigration::class),
AutoMigration(from = 5, to = 6, spec = ServerDatabase.IntrosMigration::class),
], ],
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@ -34,4 +35,7 @@ abstract class ServerDatabase : RoomDatabase() {
@DeleteTable(tableName = "trickPlayManifests") @DeleteTable(tableName = "trickPlayManifests")
class TrickplayMigration : AutoMigrationSpec 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.FindroidMediaStreamDto
import com.nomadics9.ananas.models.FindroidMovieDto import com.nomadics9.ananas.models.FindroidMovieDto
import com.nomadics9.ananas.models.FindroidSeasonDto import com.nomadics9.ananas.models.FindroidSeasonDto
import com.nomadics9.ananas.models.FindroidSegmentsDto
import com.nomadics9.ananas.models.FindroidShowDto import com.nomadics9.ananas.models.FindroidShowDto
import com.nomadics9.ananas.models.FindroidSourceDto import com.nomadics9.ananas.models.FindroidSourceDto
import com.nomadics9.ananas.models.FindroidTrickplayInfoDto import com.nomadics9.ananas.models.FindroidTrickplayInfoDto
import com.nomadics9.ananas.models.FindroidUserDataDto import com.nomadics9.ananas.models.FindroidUserDataDto
import com.nomadics9.ananas.models.IntroDto
import com.nomadics9.ananas.models.Server import com.nomadics9.ananas.models.Server
import com.nomadics9.ananas.models.ServerAddress import com.nomadics9.ananas.models.ServerAddress
import com.nomadics9.ananas.models.ServerWithAddressAndUser import com.nomadics9.ananas.models.ServerWithAddressAndUser
@ -205,13 +205,13 @@ interface ServerDatabaseDao {
fun deleteEpisodesBySeasonId(seasonId: UUID) fun deleteEpisodesBySeasonId(seasonId: UUID)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertIntro(intro: IntroDto) fun insertSegments(segment: FindroidSegmentsDto)
@Query("SELECT * FROM intros WHERE itemId = :itemId") @Query("SELECT * FROM segments WHERE itemId = :itemId")
fun getIntro(itemId: UUID): IntroDto? fun getSegments(itemId: UUID): FindroidSegmentsDto?
@Query("DELETE FROM intros WHERE itemId = :itemId") @Query("DELETE FROM segments WHERE itemId = :itemId")
fun deleteIntro(itemId: UUID) fun deleteSegments(itemId: UUID)
@Query("SELECT * FROM seasons") @Query("SELECT * FROM seasons")
fun getSeasons(): List<FindroidSeasonDto> 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.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.FindroidSeason import com.nomadics9.ananas.models.FindroidSeason
import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.models.FindroidShow import com.nomadics9.ananas.models.FindroidShow
import com.nomadics9.ananas.models.FindroidSource import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.Intro
import com.nomadics9.ananas.models.SortBy import com.nomadics9.ananas.models.SortBy
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.BaseItemDto
@ -83,7 +83,7 @@ interface JellyfinRepository {
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String 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? 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.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.FindroidSeason 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.FindroidShow
import com.nomadics9.ananas.models.FindroidSource import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.Intro
import com.nomadics9.ananas.models.SortBy import com.nomadics9.ananas.models.SortBy
import com.nomadics9.ananas.models.toFindroidCollection import com.nomadics9.ananas.models.toFindroidCollection
import com.nomadics9.ananas.models.toFindroidEpisode import com.nomadics9.ananas.models.toFindroidEpisode
import com.nomadics9.ananas.models.toFindroidItem import com.nomadics9.ananas.models.toFindroidItem
import com.nomadics9.ananas.models.toFindroidMovie import com.nomadics9.ananas.models.toFindroidMovie
import com.nomadics9.ananas.models.toFindroidSeason import com.nomadics9.ananas.models.toFindroidSeason
import com.nomadics9.ananas.models.toFindroidSegments
import com.nomadics9.ananas.models.toFindroidShow import com.nomadics9.ananas.models.toFindroidShow
import com.nomadics9.ananas.models.toFindroidSource import com.nomadics9.ananas.models.toFindroidSource
import com.nomadics9.ananas.models.toIntro
import io.ktor.util.toByteArray import io.ktor.util.toByteArray
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow 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) { withContext(Dispatchers.IO) {
val intro = database.getIntro(itemId)?.toIntro() val segments = database.getSegments(itemId)?.toFindroidSegments()
if (intro != null) { if (segments != null) {
return@withContext intro return@withContext segments
} }
// https://github.com/ConfusedPolarBear/intro-skipper/blob/master/docs/api.md // https://github.com/ConfusedPolarBear/intro-skipper/blob/master/docs/api.md
@ -349,10 +350,37 @@ class JellyfinRepositoryImpl(
pathParameters["itemId"] = itemId pathParameters["itemId"] = itemId
try { try {
return@withContext jellyfinApi.api.get<Intro>( val segmentToConvert = jellyfinApi.api.get<FindroidSegments>(
"/Episode/{itemId}/IntroTimestamps/v1", "/Episode/{itemId}/IntroSkipperSegments",
pathParameters, pathParameters,
).content ).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) { } catch (e: Exception) {
return@withContext null 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.FindroidItem
import com.nomadics9.ananas.models.FindroidMovie import com.nomadics9.ananas.models.FindroidMovie
import com.nomadics9.ananas.models.FindroidSeason import com.nomadics9.ananas.models.FindroidSeason
import com.nomadics9.ananas.models.FindroidSegment
import com.nomadics9.ananas.models.FindroidShow import com.nomadics9.ananas.models.FindroidShow
import com.nomadics9.ananas.models.FindroidSource import com.nomadics9.ananas.models.FindroidSource
import com.nomadics9.ananas.models.Intro
import com.nomadics9.ananas.models.SortBy import com.nomadics9.ananas.models.SortBy
import com.nomadics9.ananas.models.toFindroidEpisode import com.nomadics9.ananas.models.toFindroidEpisode
import com.nomadics9.ananas.models.toFindroidMovie import com.nomadics9.ananas.models.toFindroidMovie
import com.nomadics9.ananas.models.toFindroidSeason import com.nomadics9.ananas.models.toFindroidSeason
import com.nomadics9.ananas.models.toFindroidSegments
import com.nomadics9.ananas.models.toFindroidShow import com.nomadics9.ananas.models.toFindroidShow
import com.nomadics9.ananas.models.toFindroidSource import com.nomadics9.ananas.models.toFindroidSource
import com.nomadics9.ananas.models.toIntro
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -177,9 +177,9 @@ class JellyfinRepositoryOfflineImpl(
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override suspend fun getIntroTimestamps(itemId: UUID): Intro? = override suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
database.getIntro(itemId)?.toIntro() database.getSegments(itemId)?.toFindroidSegments()
} }
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? = 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 androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import com.nomadics9.ananas.AppPreferences 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.PlayerChapter
import com.nomadics9.ananas.models.PlayerItem import com.nomadics9.ananas.models.PlayerItem
import com.nomadics9.ananas.models.Trickplay import com.nomadics9.ananas.models.Trickplay
@ -57,7 +57,8 @@ constructor(
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
UiState( UiState(
currentItemTitle = "", currentItemTitle = "",
currentIntro = null, currentSegment = null,
showSkip = false,
currentTrickplay = null, currentTrickplay = null,
currentChapters = null, currentChapters = null,
fileLoaded = false, fileLoaded = false,
@ -68,11 +69,12 @@ constructor(
private val eventsChannel = Channel<PlayerEvents>() private val eventsChannel = Channel<PlayerEvents>()
val eventsChannelFlow = eventsChannel.receiveAsFlow() val eventsChannelFlow = eventsChannel.receiveAsFlow()
private val intros: MutableMap<UUID, Intro> = mutableMapOf() private val segments: MutableMap<UUID, List<FindroidSegment>> = mutableMapOf()
data class UiState( data class UiState(
val currentItemTitle: String, val currentItemTitle: String,
val currentIntro: Intro?, val currentSegment: FindroidSegment?,
val showSkip: Boolean?,
val currentTrickplay: Trickplay?, val currentTrickplay: Trickplay?,
val currentChapters: List<PlayerChapter>?, val currentChapters: List<PlayerChapter>?,
val fileLoaded: Boolean, val fileLoaded: Boolean,
@ -152,9 +154,10 @@ constructor(
} }
if (appPreferences.playerIntroSkipper) { if (appPreferences.playerIntroSkipper) {
jellyfinRepository.getIntroTimestamps(item.itemId)?.let { intro -> jellyfinRepository.getSegmentsTimestamps(item.itemId)?.let { segment ->
intros[item.itemId] = intro segments[item.itemId] = segment
} }
Timber.tag("SegmentInfo").d("Segments: %s", segments)
} }
Timber.d("Stream url: $streamUrl") Timber.d("Stream url: $streamUrl")
@ -239,24 +242,28 @@ constructor(
handler.postDelayed(this, 5000L) handler.postDelayed(this, 5000L)
} }
} }
val introCheckRunnable = object : Runnable { val segmentCheckRunnable = object : Runnable {
override fun run() { override fun run() {
if (player.currentMediaItem != null && player.currentMediaItem!!.mediaId.isNotEmpty()) { val currentMediaItem = player.currentMediaItem
val itemId = UUID.fromString(player.currentMediaItem!!.mediaId) if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) {
intros[itemId]?.let { intro -> val itemId = UUID.fromString(currentMediaItem.mediaId)
val seconds = player.currentPosition / 1000.0 val seconds = player.currentPosition / 1000.0
if (seconds > intro.showSkipPromptAt && seconds < intro.hideSkipPromptAt) {
_uiState.update { it.copy(currentIntro = intro) } val currentSegment = segments[itemId]?.find { segment -> seconds in segment.startTime..<segment.endTime }
return@let _uiState.update { it.copy(currentSegment = currentSegment) }
} Timber.tag("SegmentInfo").d("currentSegment: %s", currentSegment)
_uiState.update { it.copy(currentIntro = null) }
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.postDelayed(this, 1000L)
} }
} }
handler.post(playbackProgressRunnable) handler.post(playbackProgressRunnable)
if (intros.isNotEmpty()) handler.post(introCheckRunnable) if (segments.isNotEmpty()) handler.post(segmentCheckRunnable)
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
@ -275,7 +282,14 @@ constructor(
} else { } else {
item.name 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) jellyfinRepository.postPlaybackStart(item.itemId)

View file

@ -7,7 +7,6 @@
<string name="player_controls_rewind">Rebobinar</string>&gt; <string name="player_controls_rewind">Rebobinar</string>&gt;
<string name="player_controls_exit">Salir de reproductor</string> <string name="player_controls_exit">Salir de reproductor</string>
<string name="player_controls_fast_forward">Avanzar</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="external">Externo</string>
<string name="player_controls_skip_back">Saltar atrás</string> <string name="player_controls_skip_back">Saltar atrás</string>
<string name="player_controls_play_pause">Reproducir pausar</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_trickplay">Trickplay</string>
<string name="player_controls_play_pause">Начало пауза</string> <string name="player_controls_play_pause">Начало пауза</string>
<string name="player_controls_exit">Излез от плейъра</string> <string name="player_controls_exit">Излез от плейъра</string>
<string name="player_controls_skip_intro">Пропусни интро</string>
<string name="player_controls_picture_in_picture">Влез в режим картина-в-картина</string> <string name="player_controls_picture_in_picture">Влез в режим картина-в-картина</string>
<string name="none">Нищо</string> <string name="none">Нищо</string>
</resources> </resources>

View file

@ -9,7 +9,6 @@
<string name="player_controls_skip_back">Přeskočit zpět</string> <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_play_pause">Přehrát pauza</string>
<string name="player_controls_rewind">Přetočit</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_exit">Ukončit přehrávač</string>
<string name="player_controls_fast_forward">Rychlý posun vpřed</string> <string name="player_controls_fast_forward">Rychlý posun vpřed</string>
<string name="player_controls_skip_forward">Přeskočit 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_lock">Låser afspilleren</string>
<string name="player_controls_skip_back">Hop tilbage</string> <string name="player_controls_skip_back">Hop tilbage</string>
<string name="player_controls_exit">Stop afspiller</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_fast_forward">Spol frem</string>
<string name="player_controls_skip_forward">Spring frem</string> <string name="player_controls_skip_forward">Spring frem</string>
<string name="player_trickplay">Spole afspille</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_exit">Player verlassen</string>
<string name="player_controls_rewind">Wiederholen</string> <string name="player_controls_rewind">Wiederholen</string>
<string name="player_controls_fast_forward">Vorspulen</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_lock">Player sperren</string>
<string name="player_controls_skip_back">Zurückspulen</string> <string name="player_controls_skip_back">Zurückspulen</string>
<string name="player_controls_play_pause">Starten/Anhalten</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_exit">Salir de reproductor</string>
<string name="player_controls_rewind">Atrasar</string> <string name="player_controls_rewind">Atrasar</string>
<string name="player_controls_fast_forward">Avanzar</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_controls_skip_forward">Saltar adelante</string>
<string name="player_trickplay">Avance</string> <string name="player_trickplay">Avance</string>
<string name="player_controls_lock">Bloquea el reproductor</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_exit">Salir del reproductor</string>
<string name="player_controls_rewind">Rebobinar</string> <string name="player_controls_rewind">Rebobinar</string>
<string name="player_controls_fast_forward">Avanzar</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_skip_forward">Saltar adelante</string>
<string name="player_controls_lock">Bloquea el reproductor</string> <string name="player_controls_lock">Bloquea el reproductor</string>
<string name="player_controls_play_pause">Reproducir pausar</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_exit">Quitter le lecteur</string>
<string name="player_controls_rewind">Rembobiner</string> <string name="player_controls_rewind">Rembobiner</string>
<string name="player_controls_fast_forward">Avance rapide</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_lock">Verrouille le lecteur</string>
<string name="player_controls_play_pause">Lecture / Pause</string> <string name="player_controls_play_pause">Lecture / Pause</string>
<string name="player_controls_skip_back">Retour en arrière</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_exit">Kilépés a lejátszóból</string>
<string name="player_controls_rewind">Visszatekerés</string> <string name="player_controls_rewind">Visszatekerés</string>
<string name="player_controls_fast_forward">Előrepörgeté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_lock">Zárolja a lejátszót</string>
<string name="player_controls_skip_back">Ugrás vissza</string> <string name="player_controls_skip_back">Ugrás vissza</string>
<string name="player_controls_skip_forward">Ugrás előre</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_rewind">Riavvolgi</string>
<string name="player_controls_lock">Blocca il player</string> <string name="player_controls_lock">Blocca il player</string>
<string name="player_controls_exit">Esci dal 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="player_controls_picture_in_picture">Attiva picture in picture</string>
<string name="none">Nessuno</string> <string name="none">Nessuno</string>
</resources> </resources>

View file

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

View file

@ -8,7 +8,6 @@
<string name="player_controls_rewind">되감기</string> <string name="player_controls_rewind">되감기</string>
<string name="player_controls_fast_forward">빨리 감기</string> <string name="player_controls_fast_forward">빨리 감기</string>
<string name="player_controls_exit">플레이어 나가기</string> <string name="player_controls_exit">플레이어 나가기</string>
<string name="player_controls_skip_intro">오프닝 스킵</string>
<string name="player_controls_lock">플레이어 잠금</string> <string name="player_controls_lock">플레이어 잠금</string>
<string name="player_trickplay">Trickplay</string> <string name="player_trickplay">Trickplay</string>
<string name="player_controls_skip_back">뒤로 건너뛰기</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_exit">Sluit speler</string>
<string name="player_controls_rewind">Terugspoelen</string> <string name="player_controls_rewind">Terugspoelen</string>
<string name="player_controls_fast_forward">Snel vooruit</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="player_controls_progress">Voortgangsbalk</string>
<string name="none">Geen</string> <string name="none">Geen</string>
<string name="player_controls_picture_in_picture">Scherm-in-scherm openen</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_exit">Zamknij odtwarzacz</string>
<string name="player_controls_rewind">Przewiń</string> <string name="player_controls_rewind">Przewiń</string>
<string name="player_controls_fast_forward">Przewiń do przodu</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_lock">Zablokuj odtwarzacz</string>
<string name="player_controls_skip_back">Skocz do tyłu</string> <string name="player_controls_skip_back">Skocz do tyłu</string>
<string name="player_trickplay">Trickplay</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_exit">Sair do reprodutor</string>
<string name="player_controls_rewind">Retroceder</string> <string name="player_controls_rewind">Retroceder</string>
<string name="player_controls_fast_forward">Avanço rápido</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_lock">Bloqueia o reprodutor</string>
<string name="player_controls_skip_back">Saltar para trás</string> <string name="player_controls_skip_back">Saltar para trás</string>
<string name="player_trickplay">Miniatura de pré-visualização</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_progress">Barra de progresso</string>
<string name="player_controls_skip_forward">Avançar</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_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="player_controls_picture_in_picture">Insira imagem em imagem</string>
<string name="none">Nenhum</string> <string name="none">Nenhum</string>
</resources> </resources>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Выйти из проигрывателя</string> <string name="player_controls_exit">Выйти из проигрывателя</string>
<string name="player_controls_rewind">Перемотка</string> <string name="player_controls_rewind">Перемотка</string>
<string name="player_controls_fast_forward">Быстрая перемотка</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_lock">Блокировка</string>
<string name="player_controls_skip_back">Перейти назад</string> <string name="player_controls_skip_back">Перейти назад</string>
<string name="player_controls_play_pause">Плей пауза</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_exit">Zavrieť prehrávač</string>
<string name="player_controls_rewind">Pretočiť dozadu</string> <string name="player_controls_rewind">Pretočiť dozadu</string>
<string name="player_controls_fast_forward">Pretočiť dopredu</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_lock">Zamkne prehrávač</string>
<string name="player_controls_skip_back">Preskočiť späť</string> <string name="player_controls_skip_back">Preskočiť späť</string>
<string name="player_controls_skip_forward">Preskočiť dopredu</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_exit">Izhod iz predvajalnika</string>
<string name="player_controls_rewind">Previj nazaj</string> <string name="player_controls_rewind">Previj nazaj</string>
<string name="player_controls_fast_forward">Navijaj naprej</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_lock">Zaklene predvajalnik</string>
<string name="player_controls_skip_back">Preskoči nazaj</string> <string name="player_controls_skip_back">Preskoči nazaj</string>
<string name="player_controls_play_pause">Predvajaj ustavi</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_exit">Avsluta spelare</string>
<string name="player_controls_rewind">Spola tillbaka</string> <string name="player_controls_rewind">Spola tillbaka</string>
<string name="player_controls_fast_forward">Spola framåt</string> <string name="player_controls_fast_forward">Spola framåt</string>
<string name="player_controls_skip_intro">Hoppa över intro</string>
</resources> </resources>

View file

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

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">Thoát khỏi trình xem</string> <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_rewind">Tua lùi</string>
<string name="player_controls_fast_forward">Tua tớ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_skip_back">Bỏ qua / Trở về</string>
<string name="player_controls_lock">Khoá trình phát</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> <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_exit">退出播放器</string>
<string name="player_controls_rewind">快退</string> <string name="player_controls_rewind">快退</string>
<string name="player_controls_fast_forward">快进</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_lock">锁定播放器</string>
<string name="player_controls_skip_back">跳回</string> <string name="player_controls_skip_back">跳回</string>
<string name="player_controls_play_pause">播放暂停</string> <string name="player_controls_play_pause">播放暂停</string>

View file

@ -8,7 +8,6 @@
<string name="player_controls_exit">關閉播放器</string> <string name="player_controls_exit">關閉播放器</string>
<string name="player_controls_rewind">倒帶</string> <string name="player_controls_rewind">倒帶</string>
<string name="player_controls_fast_forward">快轉</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_lock">鎖定播放器</string>
<string name="player_controls_skip_back">跳回</string> <string name="player_controls_skip_back">跳回</string>
<string name="player_controls_play_pause">播放暫停</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_play_pause">Play pause</string>
<string name="player_controls_rewind">Rewind</string> <string name="player_controls_rewind">Rewind</string>
<string name="player_controls_exit">Exit player</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_fast_forward">Fast forward</string>
<string name="player_controls_skip_forward">Skip forward</string> <string name="player_controls_skip_forward">Skip forward</string>
<string name="player_trickplay">Trickplay</string> <string name="player_trickplay">Trickplay</string>