FindroidSegment

This commit is contained in:
cd16b 2024-06-20 23:59:24 +02:00
parent 9f3be43eac
commit df984fb24b
20 changed files with 1011 additions and 297 deletions

View file

@ -135,23 +135,30 @@ class PlayerActivity : BasePlayerActivity() {
videoNameTextView.text = currentItemTitle
// Skip Intro button
skipIntroButton.isVisible = !isInPictureInPictureMode && (currentIntro != null || currentCredit != null)
skipIntroButton.text = if (currentCredit != null) {
if (binding.playerView.player?.hasNextMediaItem() == true) {
// Visibility
skipIntroButton.isVisible = !isInPictureInPictureMode && showSkip == true
// Text
when (currentSegment?.type) {
"intro" -> {
skipIntroButton.text = getString(CoreR.string.skip_intro_button)
}
"credit" -> {
skipIntroButton.text = if (binding.playerView.player?.hasNextMediaItem() == true) {
getString(CoreR.string.skip_credit_button)
} else {
getString(CoreR.string.skip_credit_button_last)
}
} else {
getString(CoreR.string.skip_intro_button)
}
}
// onClick
skipIntroButton.setOnClickListener {
if (currentIntro != null) {
currentIntro?.let {
binding.playerView.player?.seekTo((it.introEnd * 1000).toLong())
when (currentSegment?.type) {
"intro" -> {
currentSegment?.let {
binding.playerView.player?.seekTo((it.endTime * 1000).toLong())
}
skipIntroButton.isVisible = false
} else if (currentCredit != null) {
}
"credit" -> {
if (binding.playerView.player?.hasNextMediaItem() == true) {
binding.playerView.player?.seekToNext()
} else {
@ -159,6 +166,8 @@ class PlayerActivity : BasePlayerActivity() {
}
}
}
skipIntroButton.isVisible = false
}
// Trick Play
previewScrubListener?.let {

View file

@ -15,20 +15,18 @@ import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.TrickPlayManifest
import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.models.toCreditDto
import dev.jdtech.jellyfin.models.toFindroidEpisodeDto
import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto
import dev.jdtech.jellyfin.models.toFindroidMovieDto
import dev.jdtech.jellyfin.models.toFindroidSeasonDto
import dev.jdtech.jellyfin.models.toFindroidSegmentsDto
import dev.jdtech.jellyfin.models.toFindroidShowDto
import dev.jdtech.jellyfin.models.toFindroidSourceDto
import dev.jdtech.jellyfin.models.toFindroidUserDataDto
import dev.jdtech.jellyfin.models.toIntroDto
import dev.jdtech.jellyfin.models.toTrickPlayManifestDto
import dev.jdtech.jellyfin.repository.JellyfinRepository
import java.io.File
import java.util.UUID
import kotlin.Exception
import dev.jdtech.jellyfin.core.R as CoreR
class DownloaderImpl(
@ -46,8 +44,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 credit = jellyfinRepository.getCreditTimestamps(item.id)
val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
val trickPlayManifest = jellyfinRepository.getTrickPlayManifest(item.id)
val trickPlayData = if (trickPlayManifest != null) {
jellyfinRepository.getTrickPlayData(
@ -80,11 +77,8 @@ class DownloaderImpl(
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex)
if (intro != null) {
database.insertIntro(intro.toIntroDto(item.id))
}
if (credit != null) {
database.insertCredit(credit.toCreditDto(item.id))
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
if (trickPlayManifest != null && trickPlayData != null) {
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
@ -112,11 +106,8 @@ class DownloaderImpl(
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex)
if (intro != null) {
database.insertIntro(intro.toIntroDto(item.id))
}
if (credit != null) {
database.insertCredit(credit.toCreditDto(item.id))
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
if (trickPlayManifest != null && trickPlayData != null) {
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
@ -181,7 +172,7 @@ class DownloaderImpl(
database.deleteUserData(item.id)
database.deleteIntro(item.id)
database.deleteSegments(item.id)
database.deleteTrickPlayManifest(item.id)
File(context.filesDir, "trickplay/${item.id}.bif").delete()

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 <b>Intro Skipper</b> di <i>ConfusedPolarBear</i> installato sul server.\nInstalla <b>Intro Skipper v0.1.8.0 o maggiore</b> di <i>jumoog</i> per saltare anche i titoli di coda</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>

View file

@ -145,7 +145,7 @@
<string name="pref_player_mpv_vo">Video output</string>
<string name="pref_player_mpv_ao">Audio output</string>
<string name="pref_player_intro_skipper">Intro Skipper</string>
<string name="pref_player_intro_skipper_summary">Requires <i>ConfusedPolarBear\'s</i> <b>Intro Skipper</b> plugin to be installed on the server.\nInstall <i>jumoog\'s</i> <b>Intro Skipper v0.1.8.0 or higher</b> to skip end credits.</string>
<string name="pref_player_intro_skipper_summary">Requires <i>jumoog\'s</i> <b>Intro Skipper</b> plugin to be installed on the server.</string>
<string name="pref_player_trick_play">Trick Play</string>
<string name="pref_player_trick_play_summary">Requires <i>nicknsy\'s</i> <b>Jellyscrub</b> plugin to be installed on the server</string>
<string name="pref_player_chapter_markers">Chapter markers</string>

View file

@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "2611f255654b3d481be40f080a8b5401",
"identityHash": "3cb9aaa3295b9e461cb94dfc708258ed",
"entities": [
{
"tableName": "servers",
@ -758,50 +758,6 @@
"indices": [],
"foreignKeys": []
},
{
"tableName": "credits",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `start` REAL NOT NULL, `end` REAL NOT NULL, `showAt` REAL NOT NULL, `hideAt` REAL NOT NULL, PRIMARY KEY(`itemId`))",
"fields": [
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "start",
"columnName": "start",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "end",
"columnName": "end",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "showAt",
"columnName": "showAt",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "hideAt",
"columnName": "hideAt",
"affinity": "REAL",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"itemId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "userdata",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `itemId` TEXT NOT NULL, `played` INTEGER NOT NULL, `favorite` INTEGER NOT NULL, `playbackPositionTicks` INTEGER NOT NULL, `toBeSynced` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`))",
@ -857,7 +813,7 @@
"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, '2611f255654b3d481be40f080a8b5401')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3cb9aaa3295b9e461cb94dfc708258ed')"
]
}
}

View file

@ -0,0 +1,813 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "98335303560b91843defc6f7631873ab",
"entities": [
{
"tableName": "servers",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `currentServerAddressId` TEXT, `currentUserId` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "currentServerAddressId",
"columnName": "currentServerAddressId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "currentUserId",
"columnName": "currentUserId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "serverAddresses",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT NOT NULL, `address` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_serverAddresses_serverId",
"unique": false,
"columnNames": [
"serverId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_serverAddresses_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys": [
{
"table": "servers",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` TEXT NOT NULL, `accessToken` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_users_serverId",
"unique": false,
"columnNames": [
"serverId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys": [
{
"table": "servers",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"serverId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "movies",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `name` TEXT NOT NULL, `originalTitle` TEXT, `overview` TEXT NOT NULL, `runtimeTicks` INTEGER NOT NULL, `premiereDate` INTEGER, `communityRating` REAL, `officialRating` TEXT, `status` TEXT NOT NULL, `productionYear` INTEGER, `endDate` INTEGER, `chapters` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalTitle",
"columnName": "originalTitle",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "overview",
"columnName": "overview",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "runtimeTicks",
"columnName": "runtimeTicks",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "premiereDate",
"columnName": "premiereDate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "communityRating",
"columnName": "communityRating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "officialRating",
"columnName": "officialRating",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "productionYear",
"columnName": "productionYear",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "endDate",
"columnName": "endDate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "chapters",
"columnName": "chapters",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "shows",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `name` TEXT NOT NULL, `originalTitle` TEXT, `overview` TEXT NOT NULL, `runtimeTicks` INTEGER NOT NULL, `communityRating` REAL, `officialRating` TEXT, `status` TEXT NOT NULL, `productionYear` INTEGER, `endDate` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "originalTitle",
"columnName": "originalTitle",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "overview",
"columnName": "overview",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "runtimeTicks",
"columnName": "runtimeTicks",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "communityRating",
"columnName": "communityRating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "officialRating",
"columnName": "officialRating",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "productionYear",
"columnName": "productionYear",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "endDate",
"columnName": "endDate",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "seasons",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `name` TEXT NOT NULL, `seriesName` TEXT NOT NULL, `overview` TEXT NOT NULL, `indexNumber` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`seriesId`) REFERENCES `shows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "seriesId",
"columnName": "seriesId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "seriesName",
"columnName": "seriesName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "overview",
"columnName": "overview",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "indexNumber",
"columnName": "indexNumber",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_seasons_seriesId",
"unique": false,
"columnNames": [
"seriesId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_seasons_seriesId` ON `${TABLE_NAME}` (`seriesId`)"
}
],
"foreignKeys": [
{
"table": "shows",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"seriesId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "episodes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` TEXT, `seasonId` TEXT NOT NULL, `seriesId` TEXT NOT NULL, `name` TEXT NOT NULL, `seriesName` TEXT NOT NULL, `overview` TEXT NOT NULL, `indexNumber` INTEGER NOT NULL, `indexNumberEnd` INTEGER, `parentIndexNumber` INTEGER NOT NULL, `runtimeTicks` INTEGER NOT NULL, `premiereDate` INTEGER, `communityRating` REAL, `chapters` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`seasonId`) REFERENCES `seasons`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`seriesId`) REFERENCES `shows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "seasonId",
"columnName": "seasonId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "seriesId",
"columnName": "seriesId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "seriesName",
"columnName": "seriesName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "overview",
"columnName": "overview",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "indexNumber",
"columnName": "indexNumber",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "indexNumberEnd",
"columnName": "indexNumberEnd",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "parentIndexNumber",
"columnName": "parentIndexNumber",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "runtimeTicks",
"columnName": "runtimeTicks",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "premiereDate",
"columnName": "premiereDate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "communityRating",
"columnName": "communityRating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "chapters",
"columnName": "chapters",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_episodes_seasonId",
"unique": false,
"columnNames": [
"seasonId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_seasonId` ON `${TABLE_NAME}` (`seasonId`)"
},
{
"name": "index_episodes_seriesId",
"unique": false,
"columnNames": [
"seriesId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_episodes_seriesId` ON `${TABLE_NAME}` (`seriesId`)"
}
],
"foreignKeys": [
{
"table": "seasons",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"seasonId"
],
"referencedColumns": [
"id"
]
},
{
"table": "shows",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"seriesId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itemId` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `path` TEXT NOT NULL, `downloadId` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadId",
"columnName": "downloadId",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "mediastreams",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `sourceId` TEXT NOT NULL, `title` TEXT NOT NULL, `displayTitle` TEXT, `language` TEXT NOT NULL, `type` TEXT NOT NULL, `codec` TEXT NOT NULL, `isExternal` INTEGER NOT NULL, `path` TEXT NOT NULL, `channelLayout` TEXT, `videoRangeType` TEXT, `height` INTEGER, `width` INTEGER, `videoDoViTitle` TEXT, `downloadId` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "sourceId",
"columnName": "sourceId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayTitle",
"columnName": "displayTitle",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "codec",
"columnName": "codec",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isExternal",
"columnName": "isExternal",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "channelLayout",
"columnName": "channelLayout",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "videoRangeType",
"columnName": "videoRangeType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "height",
"columnName": "height",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "width",
"columnName": "width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "videoDoViTitle",
"columnName": "videoDoViTitle",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "downloadId",
"columnName": "downloadId",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "trickPlayManifests",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`itemId` TEXT NOT NULL, `version` TEXT NOT NULL, `resolution` INTEGER NOT NULL, PRIMARY KEY(`itemId`))",
"fields": [
{
"fieldPath": "itemId",
"columnName": "itemId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "resolution",
"columnName": "resolution",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"itemId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "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": []
}
],
"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, '98335303560b91843defc6f7631873ab')"
]
}
}

View file

@ -2,6 +2,7 @@ package dev.jdtech.jellyfin.database
import androidx.room.TypeConverter
import dev.jdtech.jellyfin.models.FindroidChapter
import dev.jdtech.jellyfin.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

@ -2,31 +2,40 @@ package dev.jdtech.jellyfin.database
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.DeleteTable
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import dev.jdtech.jellyfin.models.CreditDto
import androidx.room.migration.AutoMigrationSpec
import dev.jdtech.jellyfin.models.FindroidEpisodeDto
import dev.jdtech.jellyfin.models.FindroidMediaStreamDto
import dev.jdtech.jellyfin.models.FindroidMovieDto
import dev.jdtech.jellyfin.models.FindroidSeasonDto
import dev.jdtech.jellyfin.models.FindroidSegmentsDto
import dev.jdtech.jellyfin.models.FindroidShowDto
import dev.jdtech.jellyfin.models.FindroidSourceDto
import dev.jdtech.jellyfin.models.FindroidUserDataDto
import dev.jdtech.jellyfin.models.IntroDto
import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.models.TrickPlayManifestDto
import dev.jdtech.jellyfin.models.User
@Database(
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, IntroDto::class, CreditDto::class, FindroidUserDataDto::class],
version = 4,
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, FindroidSegmentsDto::class, FindroidUserDataDto::class],
version = 5,
autoMigrations = [
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4),
AutoMigration(
from = 4,
to = 5,
spec = ServerDatabase.IntrosAutoMigration::class,
),
],
)
@TypeConverters(Converters::class)
abstract class ServerDatabase : RoomDatabase() {
abstract fun getServerDatabaseDao(): ServerDatabaseDao
@DeleteTable(tableName = "intros")
class IntrosAutoMigration : AutoMigrationSpec
}

View file

@ -6,15 +6,14 @@ import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import dev.jdtech.jellyfin.models.CreditDto
import dev.jdtech.jellyfin.models.FindroidEpisodeDto
import dev.jdtech.jellyfin.models.FindroidMediaStreamDto
import dev.jdtech.jellyfin.models.FindroidMovieDto
import dev.jdtech.jellyfin.models.FindroidSeasonDto
import dev.jdtech.jellyfin.models.FindroidSegmentsDto
import dev.jdtech.jellyfin.models.FindroidShowDto
import dev.jdtech.jellyfin.models.FindroidSourceDto
import dev.jdtech.jellyfin.models.FindroidUserDataDto
import dev.jdtech.jellyfin.models.IntroDto
import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.models.ServerWithAddressAndUser
@ -215,22 +214,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)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertCredit(credit: CreditDto)
@Query("SELECT * FROM credits WHERE itemId = :itemId")
fun getCredit(itemId: UUID): CreditDto?
@Query("DELETE FROM credits WHERE itemId = :itemId")
fun deleteCredit(itemId: UUID)
@Query("DELETE FROM segments WHERE itemId = :itemId")
fun deleteSegments(itemId: UUID)
@Query("SELECT * FROM seasons")
fun getSeasons(): List<FindroidSeasonDto>

View file

@ -1,33 +0,0 @@
package dev.jdtech.jellyfin.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Credit(
@SerialName("Credits")
val credit: Credits,
)
@Serializable
data class Credits(
@SerialName("IntroStart")
val introStart: Double,
@SerialName("IntroEnd")
val introEnd: Double,
@SerialName("ShowSkipPromptAt")
val showSkipPromptAt: Double,
@SerialName("HideSkipPromptAt")
val hideSkipPromptAt: Double,
)
fun CreditDto.toCredit(): Credit {
return Credit(
credit = Credits(
introStart = start,
introEnd = end,
showSkipPromptAt = showAt,
hideSkipPromptAt = hideAt,
),
)
}

View file

@ -1,25 +0,0 @@
package dev.jdtech.jellyfin.models
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID
@Entity(tableName = "credits")
data class CreditDto(
@PrimaryKey
val itemId: UUID,
val start: Double,
val end: Double,
val showAt: Double,
val hideAt: Double,
)
fun Credit.toCreditDto(itemId: UUID): CreditDto {
return CreditDto(
itemId = itemId,
start = credit.introStart,
end = credit.introEnd,
showAt = credit.showSkipPromptAt,
hideAt = credit.hideSkipPromptAt,
)
}

View file

@ -0,0 +1,39 @@
package dev.jdtech.jellyfin.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 dev.jdtech.jellyfin.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 dev.jdtech.jellyfin.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 dev.jdtech.jellyfin.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

@ -1,15 +1,14 @@
package dev.jdtech.jellyfin.repository
import androidx.paging.PagingData
import dev.jdtech.jellyfin.models.Credit
import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSeason
import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.TrickPlayManifest
import kotlinx.coroutines.flow.Flow
@ -85,9 +84,7 @@ interface JellyfinRepository {
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String
suspend fun getIntroTimestamps(itemId: UUID): Intro?
suspend fun getCreditTimestamps(itemId: UUID): Credit?
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest?

View file

@ -7,26 +7,25 @@ import androidx.paging.PagingData
import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.Credit
import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSeason
import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.FindroidSegments
import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.TrickPlayManifest
import dev.jdtech.jellyfin.models.toCredit
import dev.jdtech.jellyfin.models.toFindroidCollection
import dev.jdtech.jellyfin.models.toFindroidEpisode
import dev.jdtech.jellyfin.models.toFindroidItem
import dev.jdtech.jellyfin.models.toFindroidMovie
import dev.jdtech.jellyfin.models.toFindroidSeason
import dev.jdtech.jellyfin.models.toFindroidSegments
import dev.jdtech.jellyfin.models.toFindroidShow
import dev.jdtech.jellyfin.models.toFindroidSource
import dev.jdtech.jellyfin.models.toIntro
import dev.jdtech.jellyfin.models.toTrickPlayManifest
import io.ktor.util.cio.toByteArray
import io.ktor.utils.io.ByteReadChannel
@ -341,12 +340,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
@ -354,32 +353,37 @@ class JellyfinRepositoryImpl(
pathParameters["itemId"] = itemId
try {
return@withContext jellyfinApi.api.get<Intro>(
"/Episode/{itemId}/IntroTimestamps/v1",
pathParameters,
).content
} catch (e: Exception) {
return@withContext null
}
}
override suspend fun getCreditTimestamps(itemId: UUID): Credit? =
withContext(Dispatchers.IO) {
val credit = database.getCredit(itemId)?.toCredit()
if (credit != null) {
return@withContext credit
}
// https://github.com/ConfusedPolarBear/intro-skipper/blob/master/docs/api.md
val pathParameters = mutableMapOf<String, UUID>()
pathParameters["itemId"] = itemId
try {
return@withContext jellyfinApi.api.get<Credit>(
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

@ -5,24 +5,22 @@ import androidx.paging.PagingData
import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.Credit
import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSeason
import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.TrickPlayManifest
import dev.jdtech.jellyfin.models.toCredit
import dev.jdtech.jellyfin.models.toFindroidEpisode
import dev.jdtech.jellyfin.models.toFindroidMovie
import dev.jdtech.jellyfin.models.toFindroidSeason
import dev.jdtech.jellyfin.models.toFindroidSegments
import dev.jdtech.jellyfin.models.toFindroidShow
import dev.jdtech.jellyfin.models.toFindroidSource
import dev.jdtech.jellyfin.models.toIntro
import dev.jdtech.jellyfin.models.toTrickPlayManifest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -181,14 +179,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()
}
override suspend fun getCreditTimestamps(itemId: UUID): Credit? =
withContext(Dispatchers.IO) {
database.getCredit(itemId)?.toCredit()
database.getSegments(itemId)?.toFindroidSegments()
}
override suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? =

View file

@ -18,8 +18,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.models.Credits
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.PlayerChapter
import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.mpv.MPVPlayer
@ -54,8 +53,8 @@ constructor(
private val _uiState = MutableStateFlow(
UiState(
currentItemTitle = "",
currentIntro = null,
currentCredit = null,
currentSegment = null,
showSkip = false,
currentTrickPlay = null,
currentChapters = null,
fileLoaded = false,
@ -66,15 +65,14 @@ constructor(
private val eventsChannel = Channel<PlayerEvents>()
val eventsChannelFlow = eventsChannel.receiveAsFlow()
private val intros: MutableMap<UUID, Intro> = mutableMapOf()
private val credits: MutableMap<UUID, Credits> = mutableMapOf()
private val segments: MutableMap<UUID, List<FindroidSegment>> = mutableMapOf()
private val trickPlays: MutableMap<UUID, BifData> = mutableMapOf()
data class UiState(
val currentItemTitle: String,
val currentIntro: Intro?,
val currentCredit: Credits?,
val currentSegment: FindroidSegment?,
val showSkip: Boolean?,
val currentTrickPlay: BifData?,
val currentChapters: List<PlayerChapter>?,
val fileLoaded: Boolean,
@ -154,12 +152,10 @@ constructor(
}
if (appPreferences.playerIntroSkipper) {
jellyfinRepository.getIntroTimestamps(item.itemId)?.let { intro ->
intros[item.itemId] = intro
}
jellyfinRepository.getCreditTimestamps(item.itemId)?.let { credit ->
credits[item.itemId] = credit.credit
jellyfinRepository.getSegmentsTimestamps(item.itemId)?.let { segment ->
segments[item.itemId] = segment
}
Timber.tag("SegmentInfo").d("Segments: %s", segments)
}
Timber.d("Stream url: $streamUrl")
@ -244,35 +240,25 @@ constructor(
handler.postDelayed(this, 5000L)
}
}
val skipCheckRunnable = object : Runnable {
val segmentCheckRunnable = object : Runnable {
override fun run() {
if (player.currentMediaItem != null && player.currentMediaItem!!.mediaId.isNotEmpty()) {
val itemId = UUID.fromString(player.currentMediaItem!!.mediaId)
val currentMediaItem = player.currentMediaItem
if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) {
val itemId = UUID.fromString(currentMediaItem.mediaId)
val seconds = player.currentPosition / 1000.0
if (intros.isNotEmpty()) {
intros[itemId]?.let { intro ->
if (seconds > intro.showSkipPromptAt && seconds < intro.hideSkipPromptAt) {
_uiState.update { it.copy(currentIntro = intro) }
return@let
}
_uiState.update { it.copy(currentIntro = null) }
}
}
if (credits.isNotEmpty()) {
credits[itemId]?.let { credit ->
if (seconds > credit.showSkipPromptAt && seconds < credit.hideSkipPromptAt) {
_uiState.update { it.copy(currentCredit = credit) }
return@let
}
_uiState.update { it.copy(currentCredit = null) }
}
}
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)
val showSkip = currentSegment?.let { it.skip && seconds in it.showAt..it.hideAt } ?: false
_uiState.update { it.copy(showSkip = showSkip) }
}
handler.postDelayed(this, 1000L)
}
}
handler.post(playbackProgressRunnable)
if (intros.isNotEmpty() || credits.isNotEmpty()) handler.post(skipCheckRunnable)
if (segments.isNotEmpty()) handler.post(segmentCheckRunnable)
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
@ -291,9 +277,14 @@ constructor(
} else {
item.name
}
_uiState.update { it.copy(currentItemTitle = itemTitle, currentChapters = item.chapters, fileLoaded = false) }
_uiState.update { it.copy(currentCredit = null) }
_uiState.update {
it.copy(
currentItemTitle = itemTitle,
currentSegment = null,
currentChapters = item.chapters,
fileLoaded = false,
)
}
jellyfinRepository.postPlaybackStart(item.itemId)