Merge remote-tracking branch 'refs/remotes/origin/main' into Skip-credit
# Conflicts: # core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt # core/src/main/res/values-it/strings.xml # core/src/main/res/values/strings.xml # data/schemas/dev.jdtech.jellyfin.database.ServerDatabase/5.json # data/src/main/java/dev/jdtech/jellyfin/database/ServerDatabase.kt # data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt # data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt # player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt
This commit is contained in:
commit
5ab65062e6
56 changed files with 376 additions and 400 deletions
|
@ -207,9 +207,9 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
}
|
||||
oldSegment = currentSegment
|
||||
|
||||
// Trick Play
|
||||
// Trickplay
|
||||
previewScrubListener?.let {
|
||||
it.currentTrickPlay = currentTrickPlay
|
||||
it.currentTrickplay = currentTrickplay
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
@ -325,7 +325,7 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
val timeBar = binding.playerView.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||
timeBar.setAdMarkerColor(Color.WHITE)
|
||||
|
||||
if (appPreferences.playerTrickPlay) {
|
||||
if (appPreferences.playerTrickplay) {
|
||||
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||
previewScrubListener = PreviewScrubListener(
|
||||
imagePreview,
|
||||
|
|
|
@ -8,8 +8,8 @@ import androidx.media3.common.Player
|
|||
import androidx.media3.ui.TimeBar
|
||||
import coil.load
|
||||
import coil.transform.RoundedCornersTransformation
|
||||
import dev.jdtech.jellyfin.utils.bif.BifData
|
||||
import dev.jdtech.jellyfin.utils.bif.BifUtil
|
||||
import dev.jdtech.jellyfin.models.Trickplay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import timber.log.Timber
|
||||
|
||||
class PreviewScrubListener(
|
||||
|
@ -17,14 +17,14 @@ class PreviewScrubListener(
|
|||
private val timeBarView: View,
|
||||
private val player: Player,
|
||||
) : TimeBar.OnScrubListener {
|
||||
var currentTrickPlay: BifData? = null
|
||||
var currentTrickplay: Trickplay? = null
|
||||
private val roundedCorners = RoundedCornersTransformation(10f)
|
||||
private var currentBitMap: Bitmap? = null
|
||||
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) {
|
||||
Timber.d("Scrubbing started at $position")
|
||||
|
||||
if (currentTrickPlay == null) {
|
||||
if (currentTrickplay == null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -35,8 +35,8 @@ class PreviewScrubListener(
|
|||
override fun onScrubMove(timeBar: TimeBar, position: Long) {
|
||||
Timber.d("Scrubbing to $position")
|
||||
|
||||
val currentBifData = currentTrickPlay ?: return
|
||||
val image = BifUtil.getTrickPlayFrame(position.toInt(), currentBifData) ?: return
|
||||
val trickplay = currentTrickplay ?: return
|
||||
val image = trickplay.images[position.div(trickplay.interval).toInt()]
|
||||
|
||||
val parent = scrubbingPreview.parent as ViewGroup
|
||||
|
||||
|
@ -57,6 +57,7 @@ class PreviewScrubListener(
|
|||
|
||||
if (currentBitMap != image) {
|
||||
scrubbingPreview.load(image) {
|
||||
dispatcher(Dispatchers.Main.immediate)
|
||||
transformations(roundedCorners)
|
||||
}
|
||||
currentBitMap = image
|
||||
|
|
|
@ -56,6 +56,7 @@ val dummyEpisode = FindroidEpisode(
|
|||
communityRating = 9.2f,
|
||||
images = FindroidImages(),
|
||||
chapters = null,
|
||||
trickplayInfo = null,
|
||||
)
|
||||
|
||||
val dummyEpisodes = listOf(
|
||||
|
|
|
@ -56,6 +56,7 @@ val dummyMovie = FindroidMovie(
|
|||
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
|
||||
images = FindroidImages(),
|
||||
chapters = null,
|
||||
trickplayInfo = null,
|
||||
)
|
||||
|
||||
val dummyMovies = listOf(
|
||||
|
|
|
@ -13,7 +13,8 @@ import dev.jdtech.jellyfin.models.FindroidEpisode
|
|||
import dev.jdtech.jellyfin.models.FindroidItem
|
||||
import dev.jdtech.jellyfin.models.FindroidMovie
|
||||
import dev.jdtech.jellyfin.models.FindroidSource
|
||||
import dev.jdtech.jellyfin.models.TrickPlayManifest
|
||||
import dev.jdtech.jellyfin.models.FindroidSources
|
||||
import dev.jdtech.jellyfin.models.FindroidTrickplayInfo
|
||||
import dev.jdtech.jellyfin.models.UiText
|
||||
import dev.jdtech.jellyfin.models.toFindroidEpisodeDto
|
||||
import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto
|
||||
|
@ -22,11 +23,13 @@ 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.toFindroidTrickplayInfoDto
|
||||
import dev.jdtech.jellyfin.models.toFindroidUserDataDto
|
||||
import dev.jdtech.jellyfin.models.toTrickPlayManifestDto
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
import kotlin.Exception
|
||||
import kotlin.math.ceil
|
||||
import dev.jdtech.jellyfin.core.R as CoreR
|
||||
|
||||
class DownloaderImpl(
|
||||
|
@ -45,12 +48,8 @@ class DownloaderImpl(
|
|||
try {
|
||||
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
|
||||
val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
|
||||
val trickPlayManifest = jellyfinRepository.getTrickPlayManifest(item.id)
|
||||
val trickPlayData = if (trickPlayManifest != null) {
|
||||
jellyfinRepository.getTrickPlayData(
|
||||
item.id,
|
||||
trickPlayManifest.widthResolutions.max(),
|
||||
)
|
||||
val trickplayInfo = if (item is FindroidSources) {
|
||||
item.trickplayInfo?.get(sourceId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
@ -77,12 +76,12 @@ class DownloaderImpl(
|
|||
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||
downloadExternalMediaStreams(item, source, storageIndex)
|
||||
if (trickplayInfo != null) {
|
||||
downloadTrickplayData(item.id, sourceId, trickplayInfo)
|
||||
}
|
||||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
if (trickPlayManifest != null && trickPlayData != null) {
|
||||
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
|
||||
}
|
||||
val request = DownloadManager.Request(source.path.toUri())
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
|
@ -106,12 +105,12 @@ class DownloaderImpl(
|
|||
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||
downloadExternalMediaStreams(item, source, storageIndex)
|
||||
if (trickplayInfo != null) {
|
||||
downloadTrickplayData(item.id, sourceId, trickplayInfo)
|
||||
}
|
||||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
if (trickPlayManifest != null && trickPlayData != null) {
|
||||
downloadTrickPlay(item, trickPlayManifest, trickPlayData)
|
||||
}
|
||||
val request = DownloadManager.Request(source.path.toUri())
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
|
@ -174,8 +173,7 @@ class DownloaderImpl(
|
|||
|
||||
database.deleteSegments(item.id)
|
||||
|
||||
database.deleteTrickPlayManifest(item.id)
|
||||
File(context.filesDir, "trickplay/${item.id}.bif").delete()
|
||||
File(context.filesDir, "trickplay/${item.id}").deleteRecursively()
|
||||
}
|
||||
|
||||
override suspend fun getProgress(downloadId: Long?): Pair<Int, Int> {
|
||||
|
@ -232,14 +230,37 @@ class DownloaderImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private fun downloadTrickPlay(
|
||||
item: FindroidItem,
|
||||
trickPlayManifest: TrickPlayManifest,
|
||||
byteArray: ByteArray,
|
||||
private suspend fun downloadTrickplayData(
|
||||
itemId: UUID,
|
||||
sourceId: String,
|
||||
trickplayInfo: FindroidTrickplayInfo,
|
||||
) {
|
||||
database.insertTrickPlayManifest(trickPlayManifest.toTrickPlayManifestDto(item.id))
|
||||
File(context.filesDir, "trickplay").mkdirs()
|
||||
val file = File(context.filesDir, "trickplay/${item.id}.bif")
|
||||
val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt()
|
||||
val byteArrays = mutableListOf<ByteArray>()
|
||||
for (i in 0..maxIndex) {
|
||||
jellyfinRepository.getTrickplayData(
|
||||
itemId,
|
||||
trickplayInfo.width,
|
||||
i,
|
||||
)?.let { byteArray ->
|
||||
byteArrays.add(byteArray)
|
||||
}
|
||||
}
|
||||
saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays)
|
||||
}
|
||||
|
||||
private fun saveTrickplayData(
|
||||
itemId: UUID,
|
||||
sourceId: String,
|
||||
trickplayInfo: FindroidTrickplayInfo,
|
||||
byteArrays: List<ByteArray>,
|
||||
) {
|
||||
val basePath = "trickplay/$itemId/$sourceId"
|
||||
database.insertTrickplayInfo(trickplayInfo.toFindroidTrickplayInfoDto(sourceId))
|
||||
File(context.filesDir, basePath).mkdirs()
|
||||
for ((i, byteArray) in byteArrays.withIndex()) {
|
||||
val file = File(context.filesDir, "$basePath/$i")
|
||||
file.writeBytes(byteArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -138,7 +138,6 @@
|
|||
<string name="pref_player_intro_skipper_summary">Requiere que el complemento Intro Skipper de ConfusedPolarBear esté instalado en el servidor</string>
|
||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||
<string name="episode_name_extended_with_end">T%1$d:E%2$d-%3$d - %4$s</string>
|
||||
<string name="pref_player_trick_play_summary">Requiere que el complemento Jellyscrub de nicknsy esté instalado en el servidor</string>
|
||||
<string name="extra_info_summary">Mostrar información detallada de audio, video y subtítulos</string>
|
||||
<string name="offline_mode">Modo desconectado</string>
|
||||
<string name="offline_mode_icon">Icono de modo desconectado</string>
|
||||
|
@ -161,7 +160,6 @@
|
|||
<string name="player_gestures_seek">Gesto de búsqueda</string>
|
||||
<string name="player_gestures_seek_summary">Deslizar horizontalmente para buscar adelante o atrás</string>
|
||||
<string name="downloaded_indicator">Indicador de descargado</string>
|
||||
<string name="pref_player_trick_play">Miniaturas (Trick Play)</string>
|
||||
<string name="storage_name">%1$s (%2$d MB libres)</string>
|
||||
<string name="preparing_download">Preparando descarga</string>
|
||||
<string name="cancel_download">Cancelar descarga</string>
|
||||
|
|
|
@ -142,7 +142,6 @@
|
|||
<string name="settings_category_network">Мрежа</string>
|
||||
<string name="settings_socket_timeout">Лимит на сокета (ms)</string>
|
||||
<string name="pref_player_mpv_hwdec">Хардуерно декодиране</string>
|
||||
<string name="pref_player_trick_play_summary">Изисква Jellyscrub на nicknsy да бъде инсталиран на сървъра</string>
|
||||
<string name="add_address">Добави адрес</string>
|
||||
<string name="audio">Аудио</string>
|
||||
<string name="video">Видео</string>
|
||||
|
@ -161,7 +160,6 @@
|
|||
<string name="select_video_version_title">Изберете версия</string>
|
||||
<string name="theme_dark">Тъмна</string>
|
||||
<string name="pref_player_intro_skipper">Пропускане на интрота</string>
|
||||
<string name="pref_player_trick_play">Trick Play</string>
|
||||
<string name="addresses">Адреси</string>
|
||||
<string name="add_server_address">Добави адрес на сървър</string>
|
||||
<string name="player_gestures_zoom">Жест за приближаване (zoom)</string>
|
||||
|
|
|
@ -136,8 +136,6 @@
|
|||
<string name="pref_player_mpv_hwdec">Hardware-Dekodierung</string>
|
||||
<string name="pref_player_mpv_vo">Videoausgang</string>
|
||||
<string name="pref_player_mpv_ao">Audioausgang</string>
|
||||
<string name="pref_player_trick_play">Trickspiel</string>
|
||||
<string name="pref_player_trick_play_summary">Erfordert die Installation des Jellyscrub-Plugins von nicknsy auf dem Server</string>
|
||||
<string name="subtitle_chip_text">CC</string>
|
||||
<string name="extra_info_summary">Anzeigen von detaillierten Infos zu Ton/Audio, Video und Untertiteln</string>
|
||||
<string name="video">Video</string>
|
||||
|
|
|
@ -149,14 +149,12 @@
|
|||
<string name="player_gestures_seek">Gesto de búsqueda</string>
|
||||
<string name="player_gestures_seek_summary">Deslice horizontalmente para buscar hacia adelante o hacia atrás</string>
|
||||
<string name="app_language">Idioma de la aplicación</string>
|
||||
<string name="pref_player_trick_play_summary">Requiere que el complemento Jellyscrub de nicknsy esté instalado en el servidor</string>
|
||||
<string name="cancel_download">Cancelar descarga</string>
|
||||
<string name="cancel_download_message">¿Quiere cancelar la descarga\?</string>
|
||||
<string name="subtitle">Subtítulos</string>
|
||||
<string name="subtitle_chip_text">CC</string>
|
||||
<string name="temp">temporáneo</string>
|
||||
<string name="downloaded_indicator">Indicador de descargado</string>
|
||||
<string name="pref_player_trick_play">Miniaturas (Trick Play)</string>
|
||||
<string name="preparing_download">Preparando la descarga</string>
|
||||
<string name="storage_name">%1$s (%2$d MB libres)</string>
|
||||
<string name="not_enough_storage">Éste elemento requiere %1$s de almacenamiento pero solo hay disponible %2$s</string>
|
||||
|
|
|
@ -147,8 +147,6 @@
|
|||
<string name="temp">temporáneo</string>
|
||||
<string name="extra_info_summary">Muestra información detallada de audio, video y subtítulos</string>
|
||||
<string name="extra_info">Mostrar info extra</string>
|
||||
<string name="pref_player_trick_play">Miniaturas (Trick Play)</string>
|
||||
<string name="pref_player_trick_play_summary">Requiere que el plugin Jellyscrub de nicknsy esté instalado en el servidor</string>
|
||||
<string name="offline_mode">Modo sin conexión</string>
|
||||
<string name="offline_mode_icon">Icono de Modo sin conexión</string>
|
||||
<string name="offline_mode_go_online">Conectarse</string>
|
||||
|
|
|
@ -142,7 +142,6 @@
|
|||
<string name="add_address">Ajouter une adresse</string>
|
||||
<string name="add_server_address">Ajouter l\'adresse d\'un serveur</string>
|
||||
<string name="add">Ajouter</string>
|
||||
<string name="pref_player_trick_play_summary">Nécessite que le plugin Jellyscrub de Nicknsy soit installé sur le serveur</string>
|
||||
<string name="temp">temporaire</string>
|
||||
<string name="subtitle_chip_text">CC</string>
|
||||
<string name="player_gestures_seek">Déplacement du curseur de lecture</string>
|
||||
|
@ -166,7 +165,6 @@
|
|||
<string name="no_server_connection">Aucune connection au serveur Jellyfin, pour regarder hors-ligne activer ce mode</string>
|
||||
<string name="cancel_download_message">Êtes-vous sûr de vouloir arrêter le téléchargement \?</string>
|
||||
<string name="app_language">Langue de l\'application</string>
|
||||
<string name="pref_player_trick_play">Lecture Spéciale</string>
|
||||
<string name="offline_mode">Mode hors-ligne</string>
|
||||
<string name="preparing_download">Préparation du téléchargement</string>
|
||||
<string name="downloaded_indicator">Indicateur de téléchargements</string>
|
||||
|
|
|
@ -137,7 +137,6 @@
|
|||
<string name="libraries">Könyvtárak</string>
|
||||
<string name="remove_user_dialog_text">Biztosan el akarod távolítani a következő felhasználót: %1$s</string>
|
||||
<string name="downloaded_indicator">Letöltött indikátor</string>
|
||||
<string name="pref_player_trick_play_summary">nicknsy Jellyscrub bővítményének telepítve kell legyen a szerveren</string>
|
||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
|
||||
<string name="offline_mode_go_online">Váltás online módra</string>
|
||||
|
@ -170,7 +169,6 @@
|
|||
<string name="subtitle_chip_text">Felirat</string>
|
||||
<string name="remove_server_address">Szerver címének eltávolítása</string>
|
||||
<string name="remove_server_address_dialog_text">Biztosan el akarod távolítani a %1$s szervert</string>
|
||||
<string name="pref_player_trick_play">Trükkös játék</string>
|
||||
<string name="player_gestures_seek">Gesztus keresése</string>
|
||||
<string name="picture_in_picture">Kép a képben</string>
|
||||
<string name="picture_in_picture_gesture">Kép a képben otthoni gesztus</string>
|
||||
|
|
|
@ -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>jumoog</i> installato sul server.</string>
|
||||
<string name="pref_player_intro_skipper_summary">Richiede il plugin <b>Intro Skipper</b> di <i>jumoog</i> installato sul server</string>
|
||||
<string name="player_gestures_seek_summary">Scorri orizzontalmente per posizionarti avanti o indietro</string>
|
||||
<string name="player_gestures_seek">Gesto posizionamento</string>
|
||||
<string name="audio">Audio</string>
|
||||
|
@ -147,8 +147,8 @@
|
|||
<string name="extra_info">Mostra più informazioni</string>
|
||||
<string name="amoled_theme">Tema scuro AMOLED</string>
|
||||
<string name="amoled_theme_summary">Usa il tema AMOLED con lo sfondo nero</string>
|
||||
<string name="pref_player_trick_play">Anteprima</string>
|
||||
<string name="pref_player_trick_play_summary">Richiede il plugin <b>Jellyscrub</b> di <i>nicknsy</i> installato sul server</string>
|
||||
<string name="pref_player_trickplay">Anteprima</string>
|
||||
<string name="pref_player_trickplay_summary">Richiede il plugin Jellyscrub di nicknsy installato sul server</string>
|
||||
<string name="size">Dimensione</string>
|
||||
<string name="privacy_policy_notice">Utilizzando Findroid accetti l\'<a href="https://raw.githubusercontent.com/jarnedemeulemeester/findroid/main/PRIVACY">informativa sulla privacy</a> che afferma che non raccogliamo alcun dato</string>
|
||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||
|
|
|
@ -147,7 +147,6 @@
|
|||
<string name="player_gestures_zoom_summary">צבוט כדי להציג את הוידאו במסך מלא</string>
|
||||
<string name="seek_back_increment">גודל קפיצה אחורה (מילי שניות)</string>
|
||||
<string name="dynamic_colors_summary">השתמש בצבעים דינמיים של Material You (זמין רק בגרסת אנדרואיד 12 ומעלה)</string>
|
||||
<string name="pref_player_trick_play_summary">דורש תוסף Jellyscrub של nicknsy מותקן על השרת</string>
|
||||
<string name="size">גודל</string>
|
||||
<string name="offline_mode_icon">סמל מצב לא מקוון</string>
|
||||
<string name="not_enough_storage">פריט זה דורש %1$s של מקום פנוי אבל רק %2$s זמינים</string>
|
||||
|
@ -171,7 +170,6 @@
|
|||
<string name="downloaded_indicator">מחוון הורדה</string>
|
||||
<string name="remove_server_address">הסר כתובת שרת</string>
|
||||
<string name="remove_server_address_dialog_text">האם אתה בטוח שברצונך להסיר את כתובת השרת %1$s</string>
|
||||
<string name="pref_player_trick_play">Trick Play</string>
|
||||
<string name="picture_in_picture">תמונה-בתוך-תמונה</string>
|
||||
<string name="picture_in_picture_gesture_summary">השתמש בכפתור הבית או במחווה כדי להכנס למצב תמונה-בתוך-תמונה כאשר הוידאו פועל</string>
|
||||
</resources>
|
|
@ -136,8 +136,6 @@
|
|||
<string name="quick_connect">Quick Connect</string>
|
||||
<string name="pref_player_intro_skipper">Intro Skipper 기능</string>
|
||||
<string name="pref_player_intro_skipper_summary">서버에 ConfusedPolarBear의 Intro Skipper 플러그인이 설치되어 있어야 합니다.</string>
|
||||
<string name="pref_player_trick_play_summary">nicknsy\'s Jellyscrub 플러그인이 서버에 설치되어 있어야 합니다</string>
|
||||
<string name="pref_player_trick_play">Trick Play</string>
|
||||
<string name="internal">내부</string>
|
||||
<string name="storage_name">%1$s (%2$d MB 사용 가능)</string>
|
||||
<string name="external">외부</string>
|
||||
|
|
|
@ -161,14 +161,12 @@
|
|||
<string name="amoled_theme">AMOLED donker thema</string>
|
||||
<string name="amoled_theme_summary">Gebruik AMOLED thema met een compleet zwarte achtergrond</string>
|
||||
<string name="downloading_error">Fout tijdens het downloaden</string>
|
||||
<string name="pref_player_trick_play_summary">Verzoek voor nicknsy\'s Jellyscrub te installeren op de server</string>
|
||||
<string name="temp">tijdelijk</string>
|
||||
<string name="pref_player_intro_skipper">Intro Overslaan</string>
|
||||
<string name="subtitle_chip_text">CC</string>
|
||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
|
||||
<string name="downloaded_indicator">Gedownloade indicator</string>
|
||||
<string name="pref_player_trick_play">Trick Play</string>
|
||||
<string name="player_gestures_seek">Zoek gebaar</string>
|
||||
<string name="no_users_found">Geen gebruikers gevonden</string>
|
||||
<string name="picture_in_picture">Scherm-in-scherm</string>
|
||||
|
|
|
@ -147,8 +147,6 @@
|
|||
<string name="extra_info_summary">Wyświetla szczegółowe informacje o audio, wideo i napisach</string>
|
||||
<string name="amoled_theme">Ciemny motyw AMOLED</string>
|
||||
<string name="amoled_theme_summary">Użyj motywu AMOLED z czystym czarnym tłem</string>
|
||||
<string name="pref_player_trick_play">Trick Play</string>
|
||||
<string name="pref_player_trick_play_summary">Wymaga zainstalowania na serwerze wtyczki Nicknsy\'s Jellyscrub</string>
|
||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
|
||||
<string name="offline_mode">Tryb offline</string>
|
||||
|
|
|
@ -147,8 +147,6 @@
|
|||
<string name="amoled_theme_summary">Use o tema AMOLED com um fundo preto</string>
|
||||
<string name="player_gestures_seek">Gesto de buscar</string>
|
||||
<string name="player_gestures_seek_summary">Deslize horizontalmente para buscar para frente ou para trás</string>
|
||||
<string name="pref_player_trick_play_summary">Requer que o plug-in Jellyscrub do nicknsy seja instalado no servidor</string>
|
||||
<string name="pref_player_trick_play">Miniatura de pré-visualização</string>
|
||||
<string name="offline_mode_icon">Ícone do modo offline</string>
|
||||
<string name="not_enough_storage">Este item requer %1$s de armazenamento livre, mas apenas %2$s está disponível</string>
|
||||
<string name="cancel_download_message">Tem certeza de que deseja cancelar o download\?</string>
|
||||
|
|
|
@ -125,14 +125,12 @@
|
|||
<string name="pref_player_mpv_vo">Saida de video</string>
|
||||
<string name="pref_player_mpv_ao">Saída de áudio</string>
|
||||
<string name="add_address">Adicionar endereço</string>
|
||||
<string name="pref_player_trick_play">Jogo de truque</string>
|
||||
<string name="episode_name">%1$d. %2$s</string>
|
||||
<string name="episode_name_extended">S%1$d:E%2$d - %3$s</string>
|
||||
<string name="remove_server_address_dialog_text">Tem certeza de que deseja remover o endereço do servidor %1$s</string>
|
||||
<string name="runtime_minutes">%1$d minutos</string>
|
||||
<string name="pref_player_intro_skipper">Capitão de introdução</string>
|
||||
<string name="pref_player_intro_skipper_summary">Requer que o plugin Confused Polar Bears Intro Skipper esteja instalado no servidor</string>
|
||||
<string name="pref_player_trick_play_summary">Requer que o plugin Jellyscrub do Nicknsy esteja instalado no servidor</string>
|
||||
<string name="add_server_address">Adicionar endereço do servidor</string>
|
||||
<string name="player_gestures_seek">Procure gesto</string>
|
||||
<string name="player_gestures_seek_summary">Deslize horizontalmente para avançar ou retroceder</string>
|
||||
|
|
|
@ -80,7 +80,6 @@
|
|||
<string name="pref_player_mpv_vo">Ieșire video</string>
|
||||
<string name="pref_player_mpv_ao">Ieșire audio</string>
|
||||
<string name="pref_player_intro_skipper_summary">Necesită ca pluginul IntroSkipper de ConfusedPolarBear să fie instalat pe server</string>
|
||||
<string name="pref_player_trick_play_summary">Necesită ca pluginul Jellyscrub de nicknsy să fie instalat pe server</string>
|
||||
<string name="addresses">Adrese</string>
|
||||
<string name="add_address">Adaugă o adresă</string>
|
||||
<string name="add_server_address">Adaugă o adresă de server</string>
|
||||
|
|
|
@ -136,8 +136,6 @@
|
|||
<string name="settings_request_timeout">Время ожидания запроса (мс)</string>
|
||||
<string name="settings_connect_timeout">Время ожидания соединения (мс)</string>
|
||||
<string name="settings_socket_timeout">Время ожидания сокета (мс)</string>
|
||||
<string name="pref_player_trick_play_summary">Требуется, чтобы на сервере был установлен плагин Jellyscrub от nicknsy</string>
|
||||
<string name="pref_player_trick_play">Миниатюры предпросмотра (Trick Play)</string>
|
||||
<string name="subtitle">Субтитры</string>
|
||||
<string name="extra_info_summary">Показывает дополнительную информацию о Аудио, Видео и Субтитрах</string>
|
||||
<string name="extra_info">Показывать дополнительную информацию</string>
|
||||
|
|
|
@ -147,8 +147,6 @@
|
|||
<string name="temp">dočasné</string>
|
||||
<string name="amoled_theme">AMOLED temný motív</string>
|
||||
<string name="amoled_theme_summary">Použiť AMOLED motív s úplne čiernym pozadím</string>
|
||||
<string name="pref_player_trick_play">Miniatúry (Trick Play)</string>
|
||||
<string name="pref_player_trick_play_summary">Potrebuje aby bol na serveri nainštalovaný Jellyscrub plugin od nicknsy</string>
|
||||
<string name="episode_name_with_end">%1$d:%2$d. %3$s</string>
|
||||
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
|
||||
<string name="offline_mode">Offline Režim</string>
|
||||
|
|
|
@ -139,7 +139,6 @@
|
|||
<string name="track_selection">[%1$s] %2$s (%3$s)</string>
|
||||
<string name="settings_connect_timeout">Časovna omejitev povezave (ms)</string>
|
||||
<string name="cancel_download_message">Ali ste prepričani, da želite preklicati prenos\?</string>
|
||||
<string name="pref_player_trick_play_summary">Zahteva namestitev vtičnika nicknsy Jellyscrub na strežniku</string>
|
||||
<string name="size">Velikost</string>
|
||||
<string name="offline_mode">Način brez povezave</string>
|
||||
<string name="offline_mode_go_online">izhod iz načina brez povezave</string>
|
||||
|
|
|
@ -93,7 +93,6 @@
|
|||
<string name="add_user">Lägg till användare</string>
|
||||
<string name="pref_player_mpv_vo">Videooutput</string>
|
||||
<string name="pref_player_mpv_ao">Ljudoutput</string>
|
||||
<string name="pref_player_trick_play">Trickspel</string>
|
||||
<string name="addresses">Adresser</string>
|
||||
<string name="add_address">Lägg till adress</string>
|
||||
<string name="add_server_address">Lägg till serveradress</string>
|
||||
|
@ -146,7 +145,6 @@
|
|||
<string name="pref_player_mpv_hwdec">Hårdvaruavkodning</string>
|
||||
<string name="pref_player_intro_skipper">Introskippare</string>
|
||||
<string name="pref_player_intro_skipper_summary">Kräver ConfusedPolarBears Intro Skipper-plugin installerat på servern</string>
|
||||
<string name="pref_player_trick_play_summary">Kräver nicknsys Jellyscrub-plugin installerat på servern</string>
|
||||
<string name="add_server_error_outdated">Serverversionen är ej aktuell: %1$s. Vänligen uppdatera din server</string>
|
||||
<string name="add_server_error_version">Serverversion stöds ej: %1$s Vänligen uppdatera din server</string>
|
||||
</resources>
|
|
@ -100,8 +100,6 @@
|
|||
<string name="pref_player_mpv_ao">Виведення аудіо</string>
|
||||
<string name="pref_player_intro_skipper">Intro Skipper (Пропуск інтро)</string>
|
||||
<string name="pref_player_intro_skipper_summary">Потребує встановлення на сервері плагіна Intro Skipper від ConfusedPolarBear</string>
|
||||
<string name="pref_player_trick_play">Мініатюри попереднього перегляду (Trick Play)</string>
|
||||
<string name="pref_player_trick_play_summary">Потрібно встановити на сервері плагін Jellyscrub від nicknsy</string>
|
||||
<string name="addresses">Адреси</string>
|
||||
<string name="add_address">Додати адресу</string>
|
||||
<string name="add_server_address">Додати адресу сервера</string>
|
||||
|
|
|
@ -156,9 +156,7 @@
|
|||
<string name="temp">tạm</string>
|
||||
<string name="extra_info">Hiện thị thêm thông tin</string>
|
||||
<string name="video">Video</string>
|
||||
<string name="pref_player_trick_play">Tua mượt mà</string>
|
||||
<string name="extra_info_summary">Hiển thị thêm các thông tin về âm thanh, video và phụ đề</string>
|
||||
<string name="pref_player_trick_play_summary">Yêu cầu phần mở rộng Jellyscrub của nicknsy đã được cài đặt trên máy chủ</string>
|
||||
<string name="storage_name">%1$s (%2$d MB trống)</string>
|
||||
<string name="amoled_theme">Chủ đề màu tối (AMOLED)</string>
|
||||
<string name="amoled_theme_summary">Sử dụng chủ đề cho màn hình AMOLED với nền màu đen tuyệt đối</string>
|
||||
|
|
|
@ -145,8 +145,6 @@
|
|||
<string name="extra_info">显示额外信息</string>
|
||||
<string name="extra_info_summary">显示关于音频、视频和字幕的详细信息</string>
|
||||
<string name="temp">temp</string>
|
||||
<string name="pref_player_trick_play">跳转预览</string>
|
||||
<string name="pref_player_trick_play_summary">需要在服务器上安装 nicknsy 的 Jellyscrub 插件</string>
|
||||
<string name="amoled_theme">AMOLED 深色模式</string>
|
||||
<string name="amoled_theme_summary">使用带有纯黑背景的 AMOLED 主题</string>
|
||||
<string name="offline_mode_go_online">上线</string>
|
||||
|
|
|
@ -138,7 +138,6 @@
|
|||
<string name="add">添加</string>
|
||||
<string name="episode_name_with_end">%1$d-%2$d. %3$s</string>
|
||||
<string name="episode_name_extended_with_end">S%1$d:E%2$d-%3$d - %4$s</string>
|
||||
<string name="pref_player_trick_play_summary">需要在服務器上安裝 nicknsy 的 Jellyscrub 插件</string>
|
||||
<string name="downloading_error">下載時出錯</string>
|
||||
<string name="size">大小</string>
|
||||
<string name="no_server_connection">未連接到 Jellyfin 服務器,要離線觀看請啟用離線模式</string>
|
||||
|
@ -173,7 +172,6 @@
|
|||
<string name="picture_in_picture_gesture_summary">影片播放時使用主頁按鈕或手勢進入畫中畫</string>
|
||||
<string name="remove_server_address">刪除伺服器位址</string>
|
||||
<string name="remove_server_address_dialog_text">您確定要刪除伺服器位址嗎%1$s</string>
|
||||
<string name="pref_player_trick_play">跳轉預覽</string>
|
||||
<string name="temp">臨時文件</string>
|
||||
<string name="no_servers_found">未找到伺服器</string>
|
||||
<string name="no_users_found">未找到相應的用戶</string>
|
||||
|
|
|
@ -145,9 +145,9 @@
|
|||
<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>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_intro_skipper_summary">Requires <i>jumoog\'s</i> <b>Intro Skipper</b> plugin to be installed on the server</string>
|
||||
<string name="pref_player_trickplay">Trickplay</string>
|
||||
<string name="pref_player_trickplay_summary">Display preview images while scrubbing</string>
|
||||
<string name="pref_player_chapter_markers">Chapter markers</string>
|
||||
<string name="pref_player_chapter_markers_summary">Display chapter markers on the timebar</string>
|
||||
<string name="addresses">Addresses</string>
|
||||
|
|
|
@ -98,9 +98,9 @@
|
|||
|
||||
<SwitchPreferenceCompat
|
||||
app:defaultValue="true"
|
||||
app:key="pref_player_trick_play"
|
||||
app:summary="@string/pref_player_trick_play_summary"
|
||||
app:title="@string/pref_player_trick_play"
|
||||
app:key="pref_player_trickplay"
|
||||
app:summary="@string/pref_player_trickplay_summary"
|
||||
app:title="@string/pref_player_trickplay"
|
||||
app:widgetLayout="@layout/preference_material3_switch" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 5,
|
||||
"identityHash": "98335303560b91843defc6f7631873ab",
|
||||
"identityHash": "d675d95afd81500a1121fe8b696cd070",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "servers",
|
||||
|
@ -695,8 +695,8 @@
|
|||
"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`))",
|
||||
"tableName": "intros",
|
||||
"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",
|
||||
|
@ -705,41 +705,27 @@
|
|||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "TEXT",
|
||||
"fieldPath": "start",
|
||||
"columnName": "start",
|
||||
"affinity": "REAL",
|
||||
"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",
|
||||
"fieldPath": "end",
|
||||
"columnName": "end",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "segments",
|
||||
"columnName": "segments",
|
||||
"affinity": "TEXT",
|
||||
"fieldPath": "showAt",
|
||||
"columnName": "showAt",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "hideAt",
|
||||
"columnName": "hideAt",
|
||||
"affinity": "REAL",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
|
@ -802,12 +788,86 @@
|
|||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "trickplayInfos",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceId` TEXT NOT NULL, `width` INTEGER NOT NULL, `height` INTEGER NOT NULL, `tileWidth` INTEGER NOT NULL, `tileHeight` INTEGER NOT NULL, `thumbnailCount` INTEGER NOT NULL, `interval` INTEGER NOT NULL, `bandwidth` INTEGER NOT NULL, PRIMARY KEY(`sourceId`), FOREIGN KEY(`sourceId`) REFERENCES `sources`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "sourceId",
|
||||
"columnName": "sourceId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "width",
|
||||
"columnName": "width",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "height",
|
||||
"columnName": "height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tileWidth",
|
||||
"columnName": "tileWidth",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tileHeight",
|
||||
"columnName": "tileHeight",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "thumbnailCount",
|
||||
"columnName": "thumbnailCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "interval",
|
||||
"columnName": "interval",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bandwidth",
|
||||
"columnName": "bandwidth",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"sourceId"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"table": "sources",
|
||||
"onDelete": "CASCADE",
|
||||
"onUpdate": "NO ACTION",
|
||||
"columns": [
|
||||
"sourceId"
|
||||
],
|
||||
"referencedColumns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '98335303560b91843defc6f7631873ab')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd675d95afd81500a1121fe8b696cd070')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import org.jellyfin.sdk.api.client.extensions.playStateApi
|
|||
import org.jellyfin.sdk.api.client.extensions.quickConnectApi
|
||||
import org.jellyfin.sdk.api.client.extensions.sessionApi
|
||||
import org.jellyfin.sdk.api.client.extensions.systemApi
|
||||
import org.jellyfin.sdk.api.client.extensions.trickplayApi
|
||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
|
@ -59,6 +60,7 @@ class JellyfinApi(
|
|||
val sessionApi = api.sessionApi
|
||||
val showsApi = api.tvShowsApi
|
||||
val systemApi = api.systemApi
|
||||
val trickplayApi = api.trickplayApi
|
||||
val userApi = api.userApi
|
||||
val userLibraryApi = api.userLibraryApi
|
||||
val videosApi = api.videosApi
|
||||
|
|
|
@ -13,29 +13,29 @@ 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.FindroidTrickplayInfoDto
|
||||
import dev.jdtech.jellyfin.models.FindroidUserDataDto
|
||||
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, FindroidSegmentsDto::class, FindroidUserDataDto::class],
|
||||
version = 5,
|
||||
entities = [Server::class, ServerAddress::class, User::class, FindroidMovieDto::class, FindroidShowDto::class, FindroidSeasonDto::class, FindroidEpisodeDto::class, FindroidSourceDto::class, FindroidMediaStreamDto::class, FindroidSegmentsDto::class, FindroidUserDataDto::class, FindroidTrickplayInfoDto::class],
|
||||
version = 6,
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(from = 3, to = 4),
|
||||
AutoMigration(
|
||||
from = 4,
|
||||
to = 5,
|
||||
spec = ServerDatabase.IntrosAutoMigration::class,
|
||||
),
|
||||
AutoMigration(from = 4, to = 5, spec = ServerDatabase.TrickplayMigration::class),
|
||||
AutoMigration(from = 5, to = 6, spec = ServerDatabase.IntrosMigration::class,),
|
||||
],
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class ServerDatabase : RoomDatabase() {
|
||||
abstract fun getServerDatabaseDao(): ServerDatabaseDao
|
||||
|
||||
@DeleteTable(tableName = "trickPlayManifests")
|
||||
class TrickplayMigration : AutoMigrationSpec
|
||||
|
||||
@DeleteTable(tableName = "intros")
|
||||
class IntrosAutoMigration : AutoMigrationSpec
|
||||
class IntrosMigration : AutoMigrationSpec
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ 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.FindroidTrickplayInfoDto
|
||||
import dev.jdtech.jellyfin.models.FindroidUserDataDto
|
||||
import dev.jdtech.jellyfin.models.Server
|
||||
import dev.jdtech.jellyfin.models.ServerAddress
|
||||
|
@ -20,7 +21,6 @@ import dev.jdtech.jellyfin.models.ServerWithAddressAndUser
|
|||
import dev.jdtech.jellyfin.models.ServerWithAddresses
|
||||
import dev.jdtech.jellyfin.models.ServerWithAddressesAndUsers
|
||||
import dev.jdtech.jellyfin.models.ServerWithUsers
|
||||
import dev.jdtech.jellyfin.models.TrickPlayManifestDto
|
||||
import dev.jdtech.jellyfin.models.User
|
||||
import java.util.UUID
|
||||
|
||||
|
@ -147,15 +147,6 @@ interface ServerDatabaseDao {
|
|||
@Query("UPDATE userdata SET favorite = :favorite WHERE userId = :userId AND itemId = :itemId")
|
||||
fun setFavorite(userId: UUID, itemId: UUID, favorite: Boolean)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertTrickPlayManifest(trickPlayManifestDto: TrickPlayManifestDto)
|
||||
|
||||
@Query("SELECT * FROM trickPlayManifests WHERE itemId = :itemId")
|
||||
fun getTrickPlayManifest(itemId: UUID): TrickPlayManifestDto?
|
||||
|
||||
@Query("DELETE FROM trickPlayManifests WHERE itemId = :itemId")
|
||||
fun deleteTrickPlayManifest(itemId: UUID)
|
||||
|
||||
@Query("SELECT * FROM movies ORDER BY name ASC")
|
||||
fun getMovies(): List<FindroidMovieDto>
|
||||
|
||||
|
@ -270,4 +261,10 @@ interface ServerDatabaseDao {
|
|||
|
||||
@Query("SELECT * FROM episodes WHERE serverId = :serverId AND name LIKE '%' || :name || '%'")
|
||||
fun searchEpisodes(serverId: String, name: String): List<FindroidEpisodeDto>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertTrickplayInfo(trickplayInfoDto: FindroidTrickplayInfoDto)
|
||||
|
||||
@Query("SELECT * FROM trickplayInfos WHERE sourceId = :sourceId")
|
||||
fun getTrickplayInfo(sourceId: String): FindroidTrickplayInfoDto?
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ data class FindroidEpisode(
|
|||
val missing: Boolean = false,
|
||||
override val images: FindroidImages,
|
||||
override val chapters: List<FindroidChapter>?,
|
||||
override val trickplayInfo: Map<String, FindroidTrickplayInfo>?,
|
||||
) : FindroidItem, FindroidSources
|
||||
|
||||
suspend fun BaseItemDto.toFindroidEpisode(
|
||||
|
@ -67,6 +68,7 @@ suspend fun BaseItemDto.toFindroidEpisode(
|
|||
missing = locationType == LocationType.VIRTUAL,
|
||||
images = toFindroidImages(jellyfinRepository),
|
||||
chapters = toFindroidChapters(),
|
||||
trickplayInfo = trickplay?.mapValues { it.value[it.value.keys.max()]!!.toFindroidTrickplayInfo() },
|
||||
)
|
||||
} catch (_: NullPointerException) {
|
||||
null
|
||||
|
@ -75,6 +77,13 @@ suspend fun BaseItemDto.toFindroidEpisode(
|
|||
|
||||
fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UUID): FindroidEpisode {
|
||||
val userData = database.getUserDataOrCreateNew(id, userId)
|
||||
val sources = database.getSources(id).map { it.toFindroidSource(database) }
|
||||
val trickplayInfos = mutableMapOf<String, FindroidTrickplayInfo>()
|
||||
for (source in sources) {
|
||||
database.getTrickplayInfo(source.id)?.toFindroidTrickplayInfo()?.let {
|
||||
trickplayInfos[source.id] = it
|
||||
}
|
||||
}
|
||||
return FindroidEpisode(
|
||||
id = id,
|
||||
name = name,
|
||||
|
@ -83,7 +92,7 @@ fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UU
|
|||
indexNumber = indexNumber,
|
||||
indexNumberEnd = indexNumberEnd,
|
||||
parentIndexNumber = parentIndexNumber,
|
||||
sources = database.getSources(id).map { it.toFindroidSource(database) },
|
||||
sources = sources,
|
||||
played = userData.played,
|
||||
favorite = userData.favorite,
|
||||
canPlay = true,
|
||||
|
@ -97,5 +106,6 @@ fun FindroidEpisodeDto.toFindroidEpisode(database: ServerDatabaseDao, userId: UU
|
|||
communityRating = communityRating,
|
||||
images = FindroidImages(),
|
||||
chapters = chapters,
|
||||
trickplayInfo = trickplayInfos,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ data class FindroidMovie(
|
|||
override val unplayedItemCount: Int? = null,
|
||||
override val images: FindroidImages,
|
||||
override val chapters: List<FindroidChapter>?,
|
||||
override val trickplayInfo: Map<String, FindroidTrickplayInfo>?,
|
||||
) : FindroidItem, FindroidSources
|
||||
|
||||
suspend fun BaseItemDto.toFindroidMovie(
|
||||
|
@ -66,11 +67,19 @@ suspend fun BaseItemDto.toFindroidMovie(
|
|||
trailer = remoteTrailers?.getOrNull(0)?.url,
|
||||
images = toFindroidImages(jellyfinRepository),
|
||||
chapters = toFindroidChapters(),
|
||||
trickplayInfo = trickplay?.mapValues { it.value[it.value.keys.max()]!!.toFindroidTrickplayInfo() },
|
||||
)
|
||||
}
|
||||
|
||||
fun FindroidMovieDto.toFindroidMovie(database: ServerDatabaseDao, userId: UUID): FindroidMovie {
|
||||
val userData = database.getUserDataOrCreateNew(id, userId)
|
||||
val sources = database.getSources(id).map { it.toFindroidSource(database) }
|
||||
val trickplayInfos = mutableMapOf<String, FindroidTrickplayInfo>()
|
||||
for (source in sources) {
|
||||
database.getTrickplayInfo(source.id)?.toFindroidTrickplayInfo()?.let {
|
||||
trickplayInfos[source.id] = it
|
||||
}
|
||||
}
|
||||
return FindroidMovie(
|
||||
id = id,
|
||||
name = name,
|
||||
|
@ -94,5 +103,6 @@ fun FindroidMovieDto.toFindroidMovie(database: ServerDatabaseDao, userId: UUID):
|
|||
trailer = null,
|
||||
images = FindroidImages(),
|
||||
chapters = chapters,
|
||||
trickplayInfo = trickplayInfos,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,4 +3,5 @@ package dev.jdtech.jellyfin.models
|
|||
interface FindroidSources {
|
||||
val sources: List<FindroidSource>
|
||||
val runtimeTicks: Long
|
||||
val trickplayInfo: Map<String, FindroidTrickplayInfo>?
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import org.jellyfin.sdk.model.api.TrickplayInfo
|
||||
|
||||
data class FindroidTrickplayInfo(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val tileWidth: Int,
|
||||
val tileHeight: Int,
|
||||
val thumbnailCount: Int,
|
||||
val interval: Int,
|
||||
val bandwidth: Int,
|
||||
)
|
||||
|
||||
fun TrickplayInfo.toFindroidTrickplayInfo(): FindroidTrickplayInfo {
|
||||
return FindroidTrickplayInfo(
|
||||
width = width,
|
||||
height = height,
|
||||
tileWidth = tileWidth,
|
||||
tileHeight = tileHeight,
|
||||
thumbnailCount = thumbnailCount,
|
||||
interval = interval,
|
||||
bandwidth = bandwidth,
|
||||
)
|
||||
}
|
||||
|
||||
fun FindroidTrickplayInfoDto.toFindroidTrickplayInfo(): FindroidTrickplayInfo {
|
||||
return FindroidTrickplayInfo(
|
||||
width = width,
|
||||
height = height,
|
||||
tileWidth = tileWidth,
|
||||
tileHeight = tileHeight,
|
||||
thumbnailCount = thumbnailCount,
|
||||
interval = interval,
|
||||
bandwidth = bandwidth,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(
|
||||
tableName = "trickplayInfos",
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FindroidSourceDto::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("sourceId"),
|
||||
onDelete = ForeignKey.CASCADE,
|
||||
),
|
||||
],
|
||||
)
|
||||
data class FindroidTrickplayInfoDto(
|
||||
@PrimaryKey
|
||||
val sourceId: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val tileWidth: Int,
|
||||
val tileHeight: Int,
|
||||
val thumbnailCount: Int,
|
||||
val interval: Int,
|
||||
val bandwidth: Int,
|
||||
)
|
||||
|
||||
fun FindroidTrickplayInfo.toFindroidTrickplayInfoDto(sourceId: String): FindroidTrickplayInfoDto {
|
||||
return FindroidTrickplayInfoDto(
|
||||
sourceId = sourceId,
|
||||
width = width,
|
||||
height = height,
|
||||
tileWidth = tileWidth,
|
||||
tileHeight = tileHeight,
|
||||
thumbnailCount = thumbnailCount,
|
||||
interval = interval,
|
||||
bandwidth = bandwidth,
|
||||
)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TrickPlayManifest(
|
||||
@SerialName("Version")
|
||||
val version: String,
|
||||
@SerialName("WidthResolutions")
|
||||
val widthResolutions: List<Int>,
|
||||
)
|
||||
|
||||
fun TrickPlayManifestDto.toTrickPlayManifest(): TrickPlayManifest {
|
||||
return TrickPlayManifest(
|
||||
version = version,
|
||||
widthResolutions = listOf(resolution),
|
||||
)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(tableName = "trickPlayManifests")
|
||||
data class TrickPlayManifestDto(
|
||||
@PrimaryKey
|
||||
val itemId: UUID,
|
||||
val version: String,
|
||||
val resolution: Int,
|
||||
)
|
||||
|
||||
fun TrickPlayManifest.toTrickPlayManifestDto(itemId: UUID): TrickPlayManifestDto {
|
||||
return TrickPlayManifestDto(
|
||||
itemId = itemId,
|
||||
version = version,
|
||||
resolution = widthResolutions.max(),
|
||||
)
|
||||
}
|
|
@ -10,7 +10,6 @@ import dev.jdtech.jellyfin.models.FindroidSegment
|
|||
import dev.jdtech.jellyfin.models.FindroidShow
|
||||
import dev.jdtech.jellyfin.models.FindroidSource
|
||||
import dev.jdtech.jellyfin.models.SortBy
|
||||
import dev.jdtech.jellyfin.models.TrickPlayManifest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
@ -86,9 +85,7 @@ interface JellyfinRepository {
|
|||
|
||||
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
|
||||
|
||||
suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest?
|
||||
|
||||
suspend fun getTrickPlayData(itemId: UUID, width: Int): ByteArray?
|
||||
suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?
|
||||
|
||||
suspend fun postCapabilities()
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import dev.jdtech.jellyfin.models.FindroidSegments
|
|||
import dev.jdtech.jellyfin.models.FindroidShow
|
||||
import dev.jdtech.jellyfin.models.FindroidSource
|
||||
import dev.jdtech.jellyfin.models.SortBy
|
||||
import dev.jdtech.jellyfin.models.TrickPlayManifest
|
||||
import dev.jdtech.jellyfin.models.toFindroidCollection
|
||||
import dev.jdtech.jellyfin.models.toFindroidEpisode
|
||||
import dev.jdtech.jellyfin.models.toFindroidItem
|
||||
|
@ -26,9 +25,7 @@ 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.toTrickPlayManifest
|
||||
import io.ktor.util.cio.toByteArray
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.util.toByteArray
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -389,46 +386,17 @@ class JellyfinRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? =
|
||||
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val trickPlayManifest = database.getTrickPlayManifest(itemId)
|
||||
if (trickPlayManifest != null) {
|
||||
return@withContext trickPlayManifest.toTrickPlayManifest()
|
||||
}
|
||||
// https://github.com/nicknsy/jellyscrub/blob/main/Nick.Plugin.Jellyscrub/Api/TrickplayController.cs
|
||||
val pathParameters = mutableMapOf<String, UUID>()
|
||||
pathParameters["itemId"] = itemId
|
||||
|
||||
try {
|
||||
return@withContext jellyfinApi.api.get<TrickPlayManifest>(
|
||||
"/Trickplay/{itemId}/GetManifest",
|
||||
pathParameters,
|
||||
).content
|
||||
} catch (e: Exception) {
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTrickPlayData(itemId: UUID, width: Int): ByteArray? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val trickPlayManifest = database.getTrickPlayManifest(itemId)
|
||||
if (trickPlayManifest != null) {
|
||||
return@withContext File(
|
||||
context.filesDir,
|
||||
"trickplay/$itemId.bif",
|
||||
).readBytes()
|
||||
}
|
||||
|
||||
// https://github.com/nicknsy/jellyscrub/blob/main/Nick.Plugin.Jellyscrub/Api/TrickplayController.cs
|
||||
val pathParameters = mutableMapOf<String, Any>()
|
||||
pathParameters["itemId"] = itemId
|
||||
pathParameters["width"] = width
|
||||
|
||||
try {
|
||||
return@withContext jellyfinApi.api.get<ByteReadChannel>(
|
||||
"/Trickplay/{itemId}/{width}/GetBIF",
|
||||
pathParameters,
|
||||
).content.toByteArray()
|
||||
val sources = File(context.filesDir, "trickplay/$itemId").listFiles()
|
||||
if (sources != null) {
|
||||
return@withContext File(sources.first(), index.toString()).readBytes()
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
|
||||
return@withContext jellyfinApi.trickplayApi.getTrickplayTileImage(itemId, width, index).content.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
return@withContext null
|
||||
}
|
||||
|
@ -522,7 +490,7 @@ class JellyfinRepositoryImpl(
|
|||
withContext(Dispatchers.IO) {
|
||||
database.setFavorite(jellyfinApi.userId!!, itemId, true)
|
||||
try {
|
||||
jellyfinApi.userLibraryApi.markFavoriteItem(jellyfinApi.userId!!, itemId)
|
||||
jellyfinApi.userLibraryApi.markFavoriteItem(itemId)
|
||||
} catch (e: Exception) {
|
||||
database.setUserDataToBeSynced(jellyfinApi.userId!!, itemId, true)
|
||||
}
|
||||
|
@ -533,7 +501,7 @@ class JellyfinRepositoryImpl(
|
|||
withContext(Dispatchers.IO) {
|
||||
database.setFavorite(jellyfinApi.userId!!, itemId, false)
|
||||
try {
|
||||
jellyfinApi.userLibraryApi.unmarkFavoriteItem(jellyfinApi.userId!!, itemId)
|
||||
jellyfinApi.userLibraryApi.unmarkFavoriteItem(itemId)
|
||||
} catch (e: Exception) {
|
||||
database.setUserDataToBeSynced(jellyfinApi.userId!!, itemId, true)
|
||||
}
|
||||
|
@ -544,7 +512,7 @@ class JellyfinRepositoryImpl(
|
|||
withContext(Dispatchers.IO) {
|
||||
database.setPlayed(jellyfinApi.userId!!, itemId, true)
|
||||
try {
|
||||
jellyfinApi.playStateApi.markPlayedItem(jellyfinApi.userId!!, itemId)
|
||||
jellyfinApi.playStateApi.markPlayedItem(itemId)
|
||||
} catch (e: Exception) {
|
||||
database.setUserDataToBeSynced(jellyfinApi.userId!!, itemId, true)
|
||||
}
|
||||
|
@ -555,7 +523,7 @@ class JellyfinRepositoryImpl(
|
|||
withContext(Dispatchers.IO) {
|
||||
database.setPlayed(jellyfinApi.userId!!, itemId, false)
|
||||
try {
|
||||
jellyfinApi.playStateApi.markUnplayedItem(jellyfinApi.userId!!, itemId)
|
||||
jellyfinApi.playStateApi.markUnplayedItem(itemId)
|
||||
} catch (e: Exception) {
|
||||
database.setUserDataToBeSynced(jellyfinApi.userId!!, itemId, true)
|
||||
}
|
||||
|
|
|
@ -14,14 +14,12 @@ import dev.jdtech.jellyfin.models.FindroidSegment
|
|||
import dev.jdtech.jellyfin.models.FindroidShow
|
||||
import dev.jdtech.jellyfin.models.FindroidSource
|
||||
import dev.jdtech.jellyfin.models.SortBy
|
||||
import dev.jdtech.jellyfin.models.TrickPlayManifest
|
||||
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.toTrickPlayManifest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -184,22 +182,15 @@ class JellyfinRepositoryOfflineImpl(
|
|||
database.getSegments(itemId)?.toFindroidSegments()
|
||||
}
|
||||
|
||||
override suspend fun getTrickPlayManifest(itemId: UUID): TrickPlayManifest? =
|
||||
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? =
|
||||
withContext(Dispatchers.IO) {
|
||||
database.getTrickPlayManifest(itemId)?.toTrickPlayManifest()
|
||||
}
|
||||
|
||||
override suspend fun getTrickPlayData(itemId: UUID, width: Int): ByteArray? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val trickPlayManifest = database.getTrickPlayManifest(itemId)
|
||||
if (trickPlayManifest != null) {
|
||||
return@withContext File(
|
||||
context.filesDir,
|
||||
"trickplay/$itemId.bif",
|
||||
).readBytes()
|
||||
}
|
||||
try {
|
||||
val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null
|
||||
File(sources.first(), index.toString()).readBytes()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun postCapabilities() {}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[versions]
|
||||
aboutlibraries = "11.2.1"
|
||||
android-desugar-jdk-libs = "2.0.4"
|
||||
android-plugin = "8.4.2"
|
||||
android-plugin = "8.5.0"
|
||||
androidx-activity = "1.9.0"
|
||||
androidx-appcompat = "1.7.0"
|
||||
androidx-compose-bom = "2024.06.00"
|
||||
|
@ -28,7 +28,7 @@ androidx-work = "2.9.0"
|
|||
coil = "2.6.0"
|
||||
hilt = "2.51.1"
|
||||
compose-destinations = "1.10.2"
|
||||
jellyfin = "1.5.0-beta.3"
|
||||
jellyfin = "1.5.0-beta.4"
|
||||
junit = "4.13.2"
|
||||
kotlin = "2.0.0"
|
||||
kotlinx-serialization = "1.7.0"
|
||||
|
|
|
@ -16,4 +16,5 @@ data class PlayerItem(
|
|||
val indexNumberEnd: Int? = null,
|
||||
val externalSubtitles: List<ExternalSubtitle> = emptyList(),
|
||||
val chapters: List<PlayerChapter>? = null,
|
||||
val trickplayInfo: TrickplayInfo? = null,
|
||||
) : Parcelable
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
data class Trickplay(
|
||||
val interval: Int,
|
||||
val images: List<Bitmap>,
|
||||
)
|
|
@ -0,0 +1,15 @@
|
|||
package dev.jdtech.jellyfin.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class TrickplayInfo(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val tileWidth: Int,
|
||||
val tileHeight: Int,
|
||||
val thumbnailCount: Int,
|
||||
val interval: Int,
|
||||
val bandwidth: Int,
|
||||
) : Parcelable
|
|
@ -1,11 +0,0 @@
|
|||
package dev.jdtech.jellyfin.utils.bif
|
||||
|
||||
import android.graphics.Bitmap
|
||||
|
||||
data class BifData(
|
||||
val version: Int,
|
||||
val timestampMultiplier: Int,
|
||||
val imageCount: Int,
|
||||
val images: Map<Int, Bitmap>,
|
||||
val imageWidth: Int,
|
||||
)
|
|
@ -1,3 +0,0 @@
|
|||
package dev.jdtech.jellyfin.utils.bif
|
||||
|
||||
data class BifIndexEntry(val timestamp: Int, val offset: Int)
|
|
@ -1,77 +0,0 @@
|
|||
package dev.jdtech.jellyfin.utils.bif
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import timber.log.Timber
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
object BifUtil {
|
||||
|
||||
/* https://github.com/nicknsy/jellyscrub/blob/main/Nick.Plugin.Jellyscrub/Api/trickplay.js */
|
||||
private val BIF_MAGIC_NUMBERS = byteArrayOf(0x89.toByte(), 0x42.toByte(), 0x49.toByte(), 0x46.toByte(), 0x0D.toByte(), 0x0A.toByte(), 0x1A.toByte(), 0x0A.toByte())
|
||||
private const val SUPPORTED_BIF_VERSION = 0
|
||||
|
||||
fun trickPlayDecode(array: ByteArray, width: Int): BifData? {
|
||||
val data = Indexed8Array(array)
|
||||
|
||||
Timber.d("BIF file size: ${data.limit()}")
|
||||
|
||||
for (b in BIF_MAGIC_NUMBERS) {
|
||||
if (data.read() != b) {
|
||||
Timber.d("Attempted to read invalid bif file.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val bifVersion = data.readInt32()
|
||||
if (bifVersion != SUPPORTED_BIF_VERSION) {
|
||||
Timber.d("Client only supports BIF v$SUPPORTED_BIF_VERSION but file is v$bifVersion")
|
||||
return null
|
||||
}
|
||||
|
||||
Timber.d("BIF version: $bifVersion")
|
||||
|
||||
val bifImgCount = data.readInt32()
|
||||
if (bifImgCount <= 0) {
|
||||
Timber.d("BIF file contains no images.")
|
||||
return null
|
||||
}
|
||||
|
||||
Timber.d("BIF image count: $bifImgCount")
|
||||
|
||||
var timestampMultiplier = data.readInt32()
|
||||
if (timestampMultiplier == 0) timestampMultiplier = 1000
|
||||
|
||||
data.addPosition(44) // Reserved
|
||||
|
||||
val bifIndex = mutableListOf<BifIndexEntry>()
|
||||
for (i in 0 until bifImgCount) {
|
||||
bifIndex.add(BifIndexEntry(data.readInt32(), data.readInt32()))
|
||||
}
|
||||
|
||||
val bifImages = mutableMapOf<Int, Bitmap>()
|
||||
for (i in bifIndex.indices) {
|
||||
val indexEntry = bifIndex[i]
|
||||
val timestamp = indexEntry.timestamp
|
||||
val offset = indexEntry.offset
|
||||
val nextOffset = bifIndex.getOrNull(i + 1)?.offset ?: data.limit()
|
||||
|
||||
val imageBuffer = ByteBuffer.wrap(data.array(), offset, nextOffset - offset).order(data.order())
|
||||
val imageBytes = ByteArray(imageBuffer.remaining())
|
||||
imageBuffer.get(imageBytes)
|
||||
|
||||
val bmp = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
||||
bifImages[timestamp] = bmp
|
||||
}
|
||||
|
||||
return BifData(bifVersion, timestampMultiplier, bifImgCount, bifImages, width)
|
||||
}
|
||||
|
||||
fun getTrickPlayFrame(playerTimestamp: Int, data: BifData): Bitmap? {
|
||||
val multiplier = data.timestampMultiplier
|
||||
val images = data.images
|
||||
|
||||
val frame = playerTimestamp / multiplier
|
||||
return images[frame]
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package dev.jdtech.jellyfin.utils.bif
|
||||
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class Indexed8Array(private val array: ByteArray) {
|
||||
|
||||
private var readIndex = 0
|
||||
|
||||
fun read(): Byte {
|
||||
return array[readIndex++]
|
||||
}
|
||||
|
||||
fun addPosition(amount: Int) {
|
||||
readIndex += amount
|
||||
}
|
||||
|
||||
fun readInt32(): Int {
|
||||
val b1 = read().toInt().and(0xFF)
|
||||
val b2 = read().toInt().and(0xFF)
|
||||
val b3 = read().toInt().and(0xFF)
|
||||
val b4 = read().toInt().and(0xFF)
|
||||
|
||||
return b1 or (b2 shl 8) or (b3 shl 16) or (b4 shl 24)
|
||||
}
|
||||
|
||||
fun array(): ByteArray {
|
||||
return array
|
||||
}
|
||||
|
||||
fun limit(): Int {
|
||||
return array.size
|
||||
}
|
||||
|
||||
fun order(): ByteOrder {
|
||||
return ByteOrder.BIG_ENDIAN
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
|
@ -21,12 +23,12 @@ import dev.jdtech.jellyfin.AppPreferences
|
|||
import dev.jdtech.jellyfin.models.FindroidSegment
|
||||
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
import dev.jdtech.jellyfin.models.Trickplay
|
||||
import dev.jdtech.jellyfin.mpv.MPVPlayer
|
||||
import dev.jdtech.jellyfin.player.video.R
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import dev.jdtech.jellyfin.utils.bif.BifData
|
||||
import dev.jdtech.jellyfin.utils.bif.BifUtil
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -35,9 +37,11 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.ceil
|
||||
|
||||
@HiltViewModel
|
||||
class PlayerActivityViewModel
|
||||
|
@ -55,7 +59,7 @@ constructor(
|
|||
currentItemTitle = "",
|
||||
currentSegment = null,
|
||||
showSkip = false,
|
||||
currentTrickPlay = null,
|
||||
currentTrickplay = null,
|
||||
currentChapters = null,
|
||||
fileLoaded = false,
|
||||
),
|
||||
|
@ -67,13 +71,11 @@ constructor(
|
|||
|
||||
private val segments: MutableMap<UUID, List<FindroidSegment>> = mutableMapOf()
|
||||
|
||||
private val trickPlays: MutableMap<UUID, BifData> = mutableMapOf()
|
||||
|
||||
data class UiState(
|
||||
val currentItemTitle: String,
|
||||
val currentSegment: FindroidSegment?,
|
||||
val showSkip: Boolean?,
|
||||
val currentTrickPlay: BifData?,
|
||||
val currentTrickplay: Trickplay?,
|
||||
val currentChapters: List<PlayerChapter>?,
|
||||
val fileLoaded: Boolean,
|
||||
)
|
||||
|
@ -211,7 +213,7 @@ constructor(
|
|||
}
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(currentTrickPlay = null) }
|
||||
_uiState.update { it.copy(currentTrickplay = null) }
|
||||
playWhenReady = false
|
||||
playbackPosition = 0L
|
||||
currentMediaItemIndex = 0
|
||||
|
@ -291,8 +293,8 @@ constructor(
|
|||
|
||||
jellyfinRepository.postPlaybackStart(item.itemId)
|
||||
|
||||
if (appPreferences.playerTrickPlay) {
|
||||
getTrickPlay(item.itemId)
|
||||
if (appPreferences.playerTrickplay) {
|
||||
getTrickplay(item)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -353,29 +355,32 @@ constructor(
|
|||
playbackSpeed = speed
|
||||
}
|
||||
|
||||
private suspend fun getTrickPlay(itemId: UUID) {
|
||||
if (trickPlays[itemId] != null) return
|
||||
jellyfinRepository.getTrickPlayManifest(itemId)
|
||||
?.let { trickPlayManifest ->
|
||||
val widthResolution =
|
||||
trickPlayManifest.widthResolutions.max()
|
||||
Timber.d("Trickplay Resolution: $widthResolution")
|
||||
private suspend fun getTrickplay(item: PlayerItem) {
|
||||
val trickplayInfo = item.trickplayInfo ?: return
|
||||
Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
|
||||
|
||||
jellyfinRepository.getTrickPlayData(
|
||||
itemId,
|
||||
widthResolution,
|
||||
withContext(Dispatchers.Default) {
|
||||
val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt()
|
||||
val bitmaps = mutableListOf<Bitmap>()
|
||||
|
||||
for (i in 0..maxIndex) {
|
||||
jellyfinRepository.getTrickplayData(
|
||||
item.itemId,
|
||||
trickplayInfo.width,
|
||||
i,
|
||||
)?.let { byteArray ->
|
||||
val trickPlayData =
|
||||
BifUtil.trickPlayDecode(byteArray, widthResolution)
|
||||
|
||||
trickPlayData?.let { bifData ->
|
||||
Timber.d("Trickplay Images: ${bifData.imageCount}")
|
||||
trickPlays[itemId] = bifData
|
||||
_uiState.update { it.copy(currentTrickPlay = trickPlays[itemId]) }
|
||||
val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
||||
for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) {
|
||||
for (offsetX in 0..<trickplayInfo.width * trickplayInfo.tileWidth step trickplayInfo.width) {
|
||||
val bitmap = Bitmap.createBitmap(fullBitmap, offsetX, offsetY, trickplayInfo.width, trickplayInfo.height)
|
||||
bitmaps.add(bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_uiState.update { it.copy(currentTrickplay = Trickplay(trickplayInfo.interval, bitmaps)) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get chapters of current item
|
||||
|
|
|
@ -13,8 +13,10 @@ import dev.jdtech.jellyfin.models.FindroidMovie
|
|||
import dev.jdtech.jellyfin.models.FindroidSeason
|
||||
import dev.jdtech.jellyfin.models.FindroidShow
|
||||
import dev.jdtech.jellyfin.models.FindroidSourceType
|
||||
import dev.jdtech.jellyfin.models.FindroidSources
|
||||
import dev.jdtech.jellyfin.models.PlayerChapter
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
import dev.jdtech.jellyfin.models.TrickplayInfo
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
|
@ -115,7 +117,7 @@ class PlayerViewModel @Inject internal constructor(
|
|||
.getEpisodes(
|
||||
seriesId = item.seriesId,
|
||||
seasonId = item.seasonId,
|
||||
fields = listOf(ItemFields.MEDIA_SOURCES, ItemFields.CHAPTERS),
|
||||
fields = listOf(ItemFields.MEDIA_SOURCES, ItemFields.CHAPTERS, ItemFields.TRICKPLAY),
|
||||
startItemId = item.id,
|
||||
limit = if (userConfig?.enableNextEpisodeAutoPlay != false) null else 1,
|
||||
)
|
||||
|
@ -151,6 +153,22 @@ class PlayerViewModel @Inject internal constructor(
|
|||
},
|
||||
)
|
||||
}
|
||||
val trickplayInfo = when (this) {
|
||||
is FindroidSources -> {
|
||||
this.trickplayInfo?.get(mediaSource.id)?.let {
|
||||
TrickplayInfo(
|
||||
width = it.width,
|
||||
height = it.height,
|
||||
tileWidth = it.tileWidth,
|
||||
tileHeight = it.tileHeight,
|
||||
thumbnailCount = it.thumbnailCount,
|
||||
interval = it.interval,
|
||||
bandwidth = it.bandwidth,
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
return PlayerItem(
|
||||
name = name,
|
||||
itemId = id,
|
||||
|
@ -162,6 +180,7 @@ class PlayerViewModel @Inject internal constructor(
|
|||
indexNumberEnd = if (this is FindroidEpisode) indexNumberEnd else null,
|
||||
externalSubtitles = externalSubtitles,
|
||||
chapters = chapters.toPlayerChapters(),
|
||||
trickplayInfo = trickplayInfo,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ constructor(
|
|||
val playerMpvVo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_VO, "gpu-next")!!
|
||||
val playerMpvAo get() = sharedPreferences.getString(Constants.PREF_PLAYER_MPV_AO, "audiotrack")!!
|
||||
val playerIntroSkipper get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_INTRO_SKIPPER, true)
|
||||
val playerTrickPlay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICK_PLAY, true)
|
||||
val playerTrickplay get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_TRICKPLAY, true)
|
||||
val showChapterMarkers get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_CHAPTER_MARKERS, true)
|
||||
|
||||
val playerPipGesture get() = sharedPreferences.getBoolean(Constants.PREF_PLAYER_PIP_GESTURE, false)
|
||||
|
|
|
@ -27,7 +27,7 @@ object Constants {
|
|||
const val PREF_PLAYER_MPV_VO = "pref_player_mpv_vo"
|
||||
const val PREF_PLAYER_MPV_AO = "pref_player_mpv_ao"
|
||||
const val PREF_PLAYER_INTRO_SKIPPER = "pref_player_intro_skipper"
|
||||
const val PREF_PLAYER_TRICK_PLAY = "pref_player_trick_play"
|
||||
const val PREF_PLAYER_TRICKPLAY = "pref_player_trickplay"
|
||||
const val PREF_PLAYER_CHAPTER_MARKERS = "pref_player_chapter_markers"
|
||||
const val PREF_PLAYER_PIP_GESTURE = "pref_player_picture_in_picture_gesture"
|
||||
const val PREF_AUDIO_LANGUAGE = "pref_audio_language"
|
||||
|
|
Loading…
Reference in a new issue