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:
cd16b 2024-06-24 12:53:47 +02:00
commit 5ab65062e6
56 changed files with 376 additions and 400 deletions

View file

@ -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,

View file

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

View file

@ -56,6 +56,7 @@ val dummyEpisode = FindroidEpisode(
communityRating = 9.2f,
images = FindroidImages(),
chapters = null,
trickplayInfo = null,
)
val dummyEpisodes = listOf(

View file

@ -56,6 +56,7 @@ val dummyMovie = FindroidMovie(
trailer = "https://www.youtube.com/watch?v=puKWa8hrvA8",
images = FindroidImages(),
chapters = null,
trickplayInfo = null,
)
val dummyMovies = listOf(

View file

@ -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")
file.writeBytes(byteArray)
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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -135,7 +135,7 @@
<string name="add">Aggiungi</string>
<string name="quick_connect">Connessione Rapida</string>
<string name="pref_player_intro_skipper">Salta intro</string>
<string name="pref_player_intro_skipper_summary">Richiede il plugin <b>Intro Skipper</b> di <i>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>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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')"
]
}
}

View file

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

View file

@ -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
}

View file

@ -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?
}

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -3,4 +3,5 @@ package dev.jdtech.jellyfin.models
interface FindroidSources {
val sources: List<FindroidSource>
val runtimeTicks: Long
val trickplayInfo: Map<String, FindroidTrickplayInfo>?
}

View file

@ -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,
)
}

View file

@ -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,
)
}

View file

@ -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),
)
}

View file

@ -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(),
)
}

View file

@ -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()

View file

@ -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
}
}
try {
val sources = File(context.filesDir, "trickplay/$itemId").listFiles()
if (sources != null) {
return@withContext File(sources.first(), index.toString()).readBytes()
}
} catch (_: Exception) { }
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()
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)
}

View file

@ -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,21 +182,14 @@ 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
}
null
}
override suspend fun postCapabilities() {}

View file

@ -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"

View file

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

View file

@ -0,0 +1,8 @@
package dev.jdtech.jellyfin.models
import android.graphics.Bitmap
data class Trickplay(
val interval: Int,
val images: List<Bitmap>,
)

View file

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

View file

@ -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,
)

View file

@ -1,3 +0,0 @@
package dev.jdtech.jellyfin.utils.bif
data class BifIndexEntry(val timestamp: Int, val offset: Int)

View file

@ -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]
}
}

View file

@ -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
}
}

View file

@ -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,28 +355,31 @@ 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)) }
}
}
/**

View file

@ -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,
)
}

View file

@ -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)

View file

@ -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"