FindroidSegment
This commit is contained in:
parent
9f3be43eac
commit
df984fb24b
20 changed files with 1011 additions and 297 deletions
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
813
data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json
Normal file
813
data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json
Normal 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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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?
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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? =
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue