From df984fb24b3a2026e26ef2f3195282f3f13cbe6e Mon Sep 17 00:00:00 2001 From: cd16b Date: Thu, 20 Jun 2024 23:59:24 +0200 Subject: [PATCH] FindroidSegment --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 47 +- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 23 +- core/src/main/res/values-it/strings.xml | 2 +- core/src/main/res/values/strings.xml | 2 +- .../3.json | 48 +- .../5.json | 813 ++++++++++++++++++ .../jdtech/jellyfin/database/Converters.kt | 11 + .../jellyfin/database/ServerDatabase.kt | 17 +- .../jellyfin/database/ServerDatabaseDao.kt | 22 +- .../java/dev/jdtech/jellyfin/models/Credit.kt | 33 - .../dev/jdtech/jellyfin/models/CreditDto.kt | 25 - .../jdtech/jellyfin/models/FindroidSegment.kt | 39 + .../jellyfin/models/FindroidSegmentDto.kt | 19 + .../java/dev/jdtech/jellyfin/models/Intro.kt | 25 - .../dev/jdtech/jellyfin/models/IntroDto.kt | 25 - .../jellyfin/repository/JellyfinRepository.kt | 7 +- .../repository/JellyfinRepositoryImpl.kt | 66 +- .../JellyfinRepositoryOfflineImpl.kt | 15 +- .../viewmodels/PlayerActivityViewModel.kt | 67 +- .../video/src/main/res/values-da/strings.xml | 2 +- 20 files changed, 1011 insertions(+), 297 deletions(-) create mode 100644 data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json delete mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/Credit.kt delete mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/CreditDto.kt create mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegment.kt create mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegmentDto.kt delete mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/Intro.kt delete mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/IntroDto.kt diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 0e6d8463..3dd2c133 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -135,30 +135,39 @@ 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) { - getString(CoreR.string.skip_credit_button) - } else { - getString(CoreR.string.skip_credit_button_last) + // Visibility + skipIntroButton.isVisible = !isInPictureInPictureMode && showSkip == true + // Text + when (currentSegment?.type) { + "intro" -> { + skipIntroButton.text = getString(CoreR.string.skip_intro_button) } - } else { - getString(CoreR.string.skip_intro_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() + "credit" -> { + skipIntroButton.text = if (binding.playerView.player?.hasNextMediaItem() == true) { + getString(CoreR.string.skip_credit_button) } 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 previewScrubListener?.let { diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 66630ed8..f4f8527f 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -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 { 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() diff --git a/core/src/main/res/values-it/strings.xml b/core/src/main/res/values-it/strings.xml index dd31be9a..51f4537d 100644 --- a/core/src/main/res/values-it/strings.xml +++ b/core/src/main/res/values-it/strings.xml @@ -135,7 +135,7 @@ Aggiungi Connessione Rapida Salta intro - Richiede il plugin Intro Skipper di ConfusedPolarBear installato sul server.\nInstalla Intro Skipper v0.1.8.0 o maggiore di jumoog per saltare anche i titoli di coda + Richiede il plugin Intro Skipper di jumoog installato sul server. Scorri orizzontalmente per posizionarti avanti o indietro Gesto posizionamento Audio diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 056aa8e6..b1628fb1 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -145,7 +145,7 @@ Video output Audio output Intro Skipper - Requires ConfusedPolarBear\'s Intro Skipper plugin to be installed on the server.\nInstall jumoog\'s Intro Skipper v0.1.8.0 or higher to skip end credits. + Requires jumoog\'s Intro Skipper plugin to be installed on the server. Trick Play Requires nicknsy\'s Jellyscrub plugin to be installed on the server Chapter markers diff --git a/data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/3.json b/data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/3.json index 6742f17d..22b5646d 100644 --- a/data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/3.json +++ b/data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/3.json @@ -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')" ] } } \ No newline at end of file diff --git a/data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json b/data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json new file mode 100644 index 00000000..d6a144bb --- /dev/null +++ b/data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/data/src/main/java/dev/jdtech/jellyfin/database/Converters.kt b/data/src/main/java/dev/jdtech/jellyfin/database/Converters.kt index 0d6c112e..8eed546f 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/database/Converters.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/database/Converters.kt @@ -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? { return value?.let { Json.decodeFromString(value) } } + + @TypeConverter + fun fromFindroidSegmentsToString(value: List?): String? { + return value?.let { Json.encodeToString(value) } + } + + @TypeConverter + fun fromStringToFindroidSegments(value: String?): List? { + return value?.let { Json.decodeFromString(value) } + } } diff --git a/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt b/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt index fdfa8fc4..90e3a3e2 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt @@ -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 } diff --git a/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt b/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt index cb12df6a..5b5ce974 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabaseDao.kt @@ -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 diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/Credit.kt b/data/src/main/java/dev/jdtech/jellyfin/models/Credit.kt deleted file mode 100644 index f19f607f..00000000 --- a/data/src/main/java/dev/jdtech/jellyfin/models/Credit.kt +++ /dev/null @@ -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, - ), - ) -} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/CreditDto.kt b/data/src/main/java/dev/jdtech/jellyfin/models/CreditDto.kt deleted file mode 100644 index 4f24bf68..00000000 --- a/data/src/main/java/dev/jdtech/jellyfin/models/CreditDto.kt +++ /dev/null @@ -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, - ) -} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegment.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegment.kt new file mode 100644 index 00000000..ca3a182c --- /dev/null +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegment.kt @@ -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 { + return segments.map { segment -> + FindroidSegment( + type = segment.type, + skip = segment.skip, + startTime = segment.startTime, + endTime = segment.endTime, + showAt = segment.showAt, + hideAt = segment.hideAt, + ) + } +} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegmentDto.kt b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegmentDto.kt new file mode 100644 index 00000000..a07650ce --- /dev/null +++ b/data/src/main/java/dev/jdtech/jellyfin/models/FindroidSegmentDto.kt @@ -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, +) + +fun List.toFindroidSegmentsDto(itemId: UUID): FindroidSegmentsDto { + return FindroidSegmentsDto( + itemId = itemId, + segments = this, + ) +} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/Intro.kt b/data/src/main/java/dev/jdtech/jellyfin/models/Intro.kt deleted file mode 100644 index 31193e73..00000000 --- a/data/src/main/java/dev/jdtech/jellyfin/models/Intro.kt +++ /dev/null @@ -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, - ) -} diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/IntroDto.kt b/data/src/main/java/dev/jdtech/jellyfin/models/IntroDto.kt deleted file mode 100644 index 735fabf4..00000000 --- a/data/src/main/java/dev/jdtech/jellyfin/models/IntroDto.kt +++ /dev/null @@ -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, - ) -} diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index 7f31e281..8bd82e94 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -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? suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 6a79b74c..c1fee495 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -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? = 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( - "/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() - pathParameters["itemId"] = itemId - - try { - return@withContext jellyfinApi.api.get( + val segmentToConvert = jellyfinApi.api.get( "/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 } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 389749a1..9a10c3c4 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -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? = 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? = diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 5fc1ce1b..9fc9b233 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -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() val eventsChannelFlow = eventsChannel.receiveAsFlow() - private val intros: MutableMap = mutableMapOf() - private val credits: MutableMap = mutableMapOf() + private val segments: MutableMap> = mutableMapOf() private val trickPlays: MutableMap = 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?, 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) diff --git a/player/video/src/main/res/values-da/strings.xml b/player/video/src/main/res/values-da/strings.xml index 6dfacea9..4cb768c0 100644 --- a/player/video/src/main/res/values-da/strings.xml +++ b/player/video/src/main/res/values-da/strings.xml @@ -16,4 +16,4 @@ Ingen Process indikator Spol tilbage - + \ No newline at end of file