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,30 +135,39 @@ class PlayerActivity : BasePlayerActivity() {
videoNameTextView.text = currentItemTitle videoNameTextView.text = currentItemTitle
// Skip Intro button // Skip Intro button
skipIntroButton.isVisible = !isInPictureInPictureMode && (currentIntro != null || currentCredit != null) // Visibility
skipIntroButton.text = if (currentCredit != null) { skipIntroButton.isVisible = !isInPictureInPictureMode && showSkip == true
if (binding.playerView.player?.hasNextMediaItem() == true) { // Text
getString(CoreR.string.skip_credit_button) when (currentSegment?.type) {
} else { "intro" -> {
getString(CoreR.string.skip_credit_button_last) skipIntroButton.text = getString(CoreR.string.skip_intro_button)
} }
} else { "credit" -> {
getString(CoreR.string.skip_intro_button) skipIntroButton.text = if (binding.playerView.player?.hasNextMediaItem() == true) {
} getString(CoreR.string.skip_credit_button)
skipIntroButton.setOnClickListener {
if (currentIntro != null) {
currentIntro?.let {
binding.playerView.player?.seekTo((it.introEnd * 1000).toLong())
}
skipIntroButton.isVisible = false
} else if (currentCredit != null) {
if (binding.playerView.player?.hasNextMediaItem() == true) {
binding.playerView.player?.seekToNext()
} else { } else {
finish() getString(CoreR.string.skip_credit_button_last)
} }
} }
} }
// onClick
skipIntroButton.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()
}
}
}
skipIntroButton.isVisible = false
}
// Trick Play // Trick Play
previewScrubListener?.let { 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.FindroidSource
import dev.jdtech.jellyfin.models.TrickPlayManifest import dev.jdtech.jellyfin.models.TrickPlayManifest
import dev.jdtech.jellyfin.models.UiText import dev.jdtech.jellyfin.models.UiText
import dev.jdtech.jellyfin.models.toCreditDto
import dev.jdtech.jellyfin.models.toFindroidEpisodeDto import dev.jdtech.jellyfin.models.toFindroidEpisodeDto
import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto
import dev.jdtech.jellyfin.models.toFindroidMovieDto import dev.jdtech.jellyfin.models.toFindroidMovieDto
import dev.jdtech.jellyfin.models.toFindroidSeasonDto import dev.jdtech.jellyfin.models.toFindroidSeasonDto
import dev.jdtech.jellyfin.models.toFindroidSegmentsDto
import dev.jdtech.jellyfin.models.toFindroidShowDto import dev.jdtech.jellyfin.models.toFindroidShowDto
import dev.jdtech.jellyfin.models.toFindroidSourceDto import dev.jdtech.jellyfin.models.toFindroidSourceDto
import dev.jdtech.jellyfin.models.toFindroidUserDataDto import dev.jdtech.jellyfin.models.toFindroidUserDataDto
import dev.jdtech.jellyfin.models.toIntroDto
import dev.jdtech.jellyfin.models.toTrickPlayManifestDto import dev.jdtech.jellyfin.models.toTrickPlayManifestDto
import dev.jdtech.jellyfin.repository.JellyfinRepository import dev.jdtech.jellyfin.repository.JellyfinRepository
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
import kotlin.Exception
import dev.jdtech.jellyfin.core.R as CoreR import dev.jdtech.jellyfin.core.R as CoreR
class DownloaderImpl( class DownloaderImpl(
@ -46,8 +44,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 credit = jellyfinRepository.getCreditTimestamps(item.id)
val trickPlayManifest = jellyfinRepository.getTrickPlayManifest(item.id) val trickPlayManifest = jellyfinRepository.getTrickPlayManifest(item.id)
val trickPlayData = if (trickPlayManifest != null) { val trickPlayData = if (trickPlayManifest != null) {
jellyfinRepository.getTrickPlayData( jellyfinRepository.getTrickPlayData(
@ -80,11 +77,8 @@ class DownloaderImpl(
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex) downloadExternalMediaStreams(item, source, storageIndex)
if (intro != null) { if (segments != null) {
database.insertIntro(intro.toIntroDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
if (credit != null) {
database.insertCredit(credit.toCreditDto(item.id))
} }
if (trickPlayManifest != null && trickPlayData != null) { if (trickPlayManifest != null && trickPlayData != null) {
downloadTrickPlay(item, trickPlayManifest, trickPlayData) downloadTrickPlay(item, trickPlayManifest, trickPlayData)
@ -112,11 +106,8 @@ class DownloaderImpl(
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty())) database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId())) database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex) downloadExternalMediaStreams(item, source, storageIndex)
if (intro != null) { if (segments != null) {
database.insertIntro(intro.toIntroDto(item.id)) database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
if (credit != null) {
database.insertCredit(credit.toCreditDto(item.id))
} }
if (trickPlayManifest != null && trickPlayData != null) { if (trickPlayManifest != null && trickPlayData != null) {
downloadTrickPlay(item, trickPlayManifest, trickPlayData) downloadTrickPlay(item, trickPlayManifest, trickPlayData)
@ -181,7 +172,7 @@ class DownloaderImpl(
database.deleteUserData(item.id) database.deleteUserData(item.id)
database.deleteIntro(item.id) database.deleteSegments(item.id)
database.deleteTrickPlayManifest(item.id) database.deleteTrickPlayManifest(item.id)
File(context.filesDir, "trickplay/${item.id}.bif").delete() File(context.filesDir, "trickplay/${item.id}.bif").delete()

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 <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_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>

View file

@ -145,7 +145,7 @@
<string name="pref_player_mpv_vo">Video output</string> <string name="pref_player_mpv_vo">Video output</string>
<string name="pref_player_mpv_ao">Audio 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">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">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_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> <string name="pref_player_chapter_markers">Chapter markers</string>

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 3, "version": 3,
"identityHash": "2611f255654b3d481be40f080a8b5401", "identityHash": "3cb9aaa3295b9e461cb94dfc708258ed",
"entities": [ "entities": [
{ {
"tableName": "servers", "tableName": "servers",
@ -758,50 +758,6 @@
"indices": [], "indices": [],
"foreignKeys": [] "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", "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`))", "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": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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 androidx.room.TypeConverter
import dev.jdtech.jellyfin.models.FindroidChapter import dev.jdtech.jellyfin.models.FindroidChapter
import dev.jdtech.jellyfin.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

@ -2,31 +2,40 @@ package dev.jdtech.jellyfin.database
import androidx.room.AutoMigration import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.DeleteTable
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters 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.FindroidEpisodeDto
import dev.jdtech.jellyfin.models.FindroidMediaStreamDto import dev.jdtech.jellyfin.models.FindroidMediaStreamDto
import dev.jdtech.jellyfin.models.FindroidMovieDto import dev.jdtech.jellyfin.models.FindroidMovieDto
import dev.jdtech.jellyfin.models.FindroidSeasonDto import dev.jdtech.jellyfin.models.FindroidSeasonDto
import dev.jdtech.jellyfin.models.FindroidSegmentsDto
import dev.jdtech.jellyfin.models.FindroidShowDto import dev.jdtech.jellyfin.models.FindroidShowDto
import dev.jdtech.jellyfin.models.FindroidSourceDto import dev.jdtech.jellyfin.models.FindroidSourceDto
import dev.jdtech.jellyfin.models.FindroidUserDataDto import dev.jdtech.jellyfin.models.FindroidUserDataDto
import dev.jdtech.jellyfin.models.IntroDto
import dev.jdtech.jellyfin.models.Server import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.models.TrickPlayManifestDto import dev.jdtech.jellyfin.models.TrickPlayManifestDto
import dev.jdtech.jellyfin.models.User import dev.jdtech.jellyfin.models.User
@Database( @Database(
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, TrickPlayManifestDto::class, IntroDto::class, CreditDto::class, FindroidUserDataDto::class], 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 = 4, version = 5,
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.IntrosAutoMigration::class,
),
], ],
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class ServerDatabase : RoomDatabase() { abstract class ServerDatabase : RoomDatabase() {
abstract fun getServerDatabaseDao(): ServerDatabaseDao 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.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import dev.jdtech.jellyfin.models.CreditDto
import dev.jdtech.jellyfin.models.FindroidEpisodeDto import dev.jdtech.jellyfin.models.FindroidEpisodeDto
import dev.jdtech.jellyfin.models.FindroidMediaStreamDto import dev.jdtech.jellyfin.models.FindroidMediaStreamDto
import dev.jdtech.jellyfin.models.FindroidMovieDto import dev.jdtech.jellyfin.models.FindroidMovieDto
import dev.jdtech.jellyfin.models.FindroidSeasonDto import dev.jdtech.jellyfin.models.FindroidSeasonDto
import dev.jdtech.jellyfin.models.FindroidSegmentsDto
import dev.jdtech.jellyfin.models.FindroidShowDto import dev.jdtech.jellyfin.models.FindroidShowDto
import dev.jdtech.jellyfin.models.FindroidSourceDto import dev.jdtech.jellyfin.models.FindroidSourceDto
import dev.jdtech.jellyfin.models.FindroidUserDataDto import dev.jdtech.jellyfin.models.FindroidUserDataDto
import dev.jdtech.jellyfin.models.IntroDto
import dev.jdtech.jellyfin.models.Server import dev.jdtech.jellyfin.models.Server
import dev.jdtech.jellyfin.models.ServerAddress import dev.jdtech.jellyfin.models.ServerAddress
import dev.jdtech.jellyfin.models.ServerWithAddressAndUser import dev.jdtech.jellyfin.models.ServerWithAddressAndUser
@ -215,22 +214,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)
@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("SELECT * FROM seasons") @Query("SELECT * FROM seasons")
fun getSeasons(): List<FindroidSeasonDto> 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 package dev.jdtech.jellyfin.repository
import androidx.paging.PagingData import androidx.paging.PagingData
import dev.jdtech.jellyfin.models.Credit
import dev.jdtech.jellyfin.models.FindroidCollection import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.models.FindroidEpisode import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSeason import dev.jdtech.jellyfin.models.FindroidSeason
import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.SortBy import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.TrickPlayManifest import dev.jdtech.jellyfin.models.TrickPlayManifest
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -85,9 +84,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 getCreditTimestamps(itemId: UUID): Credit?
suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? 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.AppPreferences
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.Credit
import dev.jdtech.jellyfin.models.FindroidCollection import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.models.FindroidEpisode import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSeason import dev.jdtech.jellyfin.models.FindroidSeason
import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.FindroidSegments
import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.SortBy import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.TrickPlayManifest import dev.jdtech.jellyfin.models.TrickPlayManifest
import dev.jdtech.jellyfin.models.toCredit
import dev.jdtech.jellyfin.models.toFindroidCollection import dev.jdtech.jellyfin.models.toFindroidCollection
import dev.jdtech.jellyfin.models.toFindroidEpisode import dev.jdtech.jellyfin.models.toFindroidEpisode
import dev.jdtech.jellyfin.models.toFindroidItem import dev.jdtech.jellyfin.models.toFindroidItem
import dev.jdtech.jellyfin.models.toFindroidMovie import dev.jdtech.jellyfin.models.toFindroidMovie
import dev.jdtech.jellyfin.models.toFindroidSeason import dev.jdtech.jellyfin.models.toFindroidSeason
import dev.jdtech.jellyfin.models.toFindroidSegments
import dev.jdtech.jellyfin.models.toFindroidShow import dev.jdtech.jellyfin.models.toFindroidShow
import dev.jdtech.jellyfin.models.toFindroidSource import dev.jdtech.jellyfin.models.toFindroidSource
import dev.jdtech.jellyfin.models.toIntro
import dev.jdtech.jellyfin.models.toTrickPlayManifest import dev.jdtech.jellyfin.models.toTrickPlayManifest
import io.ktor.util.cio.toByteArray import io.ktor.util.cio.toByteArray
import io.ktor.utils.io.ByteReadChannel 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) { 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
@ -354,32 +353,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",
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>(
"/Episode/{itemId}/IntroSkipperSegments", "/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

@ -5,24 +5,22 @@ import androidx.paging.PagingData
import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.api.JellyfinApi
import dev.jdtech.jellyfin.database.ServerDatabaseDao import dev.jdtech.jellyfin.database.ServerDatabaseDao
import dev.jdtech.jellyfin.models.Credit
import dev.jdtech.jellyfin.models.FindroidCollection import dev.jdtech.jellyfin.models.FindroidCollection
import dev.jdtech.jellyfin.models.FindroidEpisode import dev.jdtech.jellyfin.models.FindroidEpisode
import dev.jdtech.jellyfin.models.FindroidItem import dev.jdtech.jellyfin.models.FindroidItem
import dev.jdtech.jellyfin.models.FindroidMovie import dev.jdtech.jellyfin.models.FindroidMovie
import dev.jdtech.jellyfin.models.FindroidSeason import dev.jdtech.jellyfin.models.FindroidSeason
import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidShow
import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.FindroidSource
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.SortBy import dev.jdtech.jellyfin.models.SortBy
import dev.jdtech.jellyfin.models.TrickPlayManifest import dev.jdtech.jellyfin.models.TrickPlayManifest
import dev.jdtech.jellyfin.models.toCredit
import dev.jdtech.jellyfin.models.toFindroidEpisode import dev.jdtech.jellyfin.models.toFindroidEpisode
import dev.jdtech.jellyfin.models.toFindroidMovie import dev.jdtech.jellyfin.models.toFindroidMovie
import dev.jdtech.jellyfin.models.toFindroidSeason import dev.jdtech.jellyfin.models.toFindroidSeason
import dev.jdtech.jellyfin.models.toFindroidSegments
import dev.jdtech.jellyfin.models.toFindroidShow import dev.jdtech.jellyfin.models.toFindroidShow
import dev.jdtech.jellyfin.models.toFindroidSource import dev.jdtech.jellyfin.models.toFindroidSource
import dev.jdtech.jellyfin.models.toIntro
import dev.jdtech.jellyfin.models.toTrickPlayManifest import dev.jdtech.jellyfin.models.toTrickPlayManifest
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -181,14 +179,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 getCreditTimestamps(itemId: UUID): Credit? =
withContext(Dispatchers.IO) {
database.getCredit(itemId)?.toCredit()
} }
override suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? = 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 androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dev.jdtech.jellyfin.AppPreferences import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.models.Credits import dev.jdtech.jellyfin.models.FindroidSegment
import dev.jdtech.jellyfin.models.Intro
import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerChapter
import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.PlayerItem
import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.mpv.MPVPlayer
@ -54,8 +53,8 @@ constructor(
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
UiState( UiState(
currentItemTitle = "", currentItemTitle = "",
currentIntro = null, currentSegment = null,
currentCredit = null, showSkip = false,
currentTrickPlay = null, currentTrickPlay = null,
currentChapters = null, currentChapters = null,
fileLoaded = false, fileLoaded = false,
@ -66,15 +65,14 @@ 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()
private val credits: MutableMap<UUID, Credits> = mutableMapOf()
private val trickPlays: MutableMap<UUID, BifData> = mutableMapOf() private val trickPlays: MutableMap<UUID, BifData> = mutableMapOf()
data class UiState( data class UiState(
val currentItemTitle: String, val currentItemTitle: String,
val currentIntro: Intro?, val currentSegment: FindroidSegment?,
val currentCredit: Credits?, val showSkip: Boolean?,
val currentTrickPlay: BifData?, val currentTrickPlay: BifData?,
val currentChapters: List<PlayerChapter>?, val currentChapters: List<PlayerChapter>?,
val fileLoaded: Boolean, val fileLoaded: Boolean,
@ -154,12 +152,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
}
jellyfinRepository.getCreditTimestamps(item.itemId)?.let { credit ->
credits[item.itemId] = credit.credit
} }
Timber.tag("SegmentInfo").d("Segments: %s", segments)
} }
Timber.d("Stream url: $streamUrl") Timber.d("Stream url: $streamUrl")
@ -244,35 +240,25 @@ constructor(
handler.postDelayed(this, 5000L) handler.postDelayed(this, 5000L)
} }
} }
val skipCheckRunnable = 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()) {
val itemId = UUID.fromString(currentMediaItem.mediaId)
val seconds = player.currentPosition / 1000.0 val seconds = player.currentPosition / 1000.0
if (intros.isNotEmpty()) {
intros[itemId]?.let { intro -> val currentSegment = segments[itemId]?.find { segment -> seconds in segment.startTime..segment.endTime }
if (seconds > intro.showSkipPromptAt && seconds < intro.hideSkipPromptAt) { _uiState.update { it.copy(currentSegment = currentSegment) }
_uiState.update { it.copy(currentIntro = intro) } Timber.tag("SegmentInfo").d("currentSegment: %s", currentSegment)
return@let
} val showSkip = currentSegment?.let { it.skip && seconds in it.showAt..it.hideAt } ?: false
_uiState.update { it.copy(currentIntro = null) } _uiState.update { it.copy(showSkip = showSkip) }
}
}
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) }
}
}
} }
handler.postDelayed(this, 1000L) handler.postDelayed(this, 1000L)
} }
} }
handler.post(playbackProgressRunnable) handler.post(playbackProgressRunnable)
if (intros.isNotEmpty() || credits.isNotEmpty()) handler.post(skipCheckRunnable) if (segments.isNotEmpty()) handler.post(segmentCheckRunnable)
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
@ -291,9 +277,14 @@ constructor(
} else { } else {
item.name item.name
} }
_uiState.update { it.copy(currentItemTitle = itemTitle, currentChapters = item.chapters, fileLoaded = false) } _uiState.update {
it.copy(
_uiState.update { it.copy(currentCredit = null) } currentItemTitle = itemTitle,
currentSegment = null,
currentChapters = item.chapters,
fileLoaded = false,
)
}
jellyfinRepository.postPlaybackStart(item.itemId) jellyfinRepository.postPlaybackStart(item.itemId)

View file

@ -16,4 +16,4 @@
<string name="none">Ingen</string> <string name="none">Ingen</string>
<string name="player_controls_progress">Process indikator</string> <string name="player_controls_progress">Process indikator</string>
<string name="player_controls_rewind">Spol tilbage</string> <string name="player_controls_rewind">Spol tilbage</string>
</resources> </resources>