bugfixes: getDeviceId() / code: New Enum VideoQuality
This commit is contained in:
parent
36dd8480e1
commit
4e8ee15d0a
8 changed files with 1450 additions and 1172 deletions
|
@ -24,19 +24,16 @@ import android.widget.ImageView
|
||||||
import android.widget.Space
|
import android.widget.Space
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.media3.common.C
|
import androidx.media3.common.C
|
||||||
import androidx.media3.common.Player
|
|
||||||
import androidx.media3.ui.DefaultTimeBar
|
import androidx.media3.ui.DefaultTimeBar
|
||||||
import androidx.media3.ui.PlayerControlView
|
import androidx.media3.ui.PlayerControlView
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import androidx.navigation.navArgs
|
import androidx.navigation.navArgs
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import com.nomadics9.ananas.databinding.ActivityPlayerBinding
|
import com.nomadics9.ananas.databinding.ActivityPlayerBinding
|
||||||
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
|
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
|
||||||
import com.nomadics9.ananas.dialogs.TrackSelectionDialogFragment
|
import com.nomadics9.ananas.dialogs.TrackSelectionDialogFragment
|
||||||
|
@ -45,6 +42,7 @@ import com.nomadics9.ananas.utils.PlayerGestureHelper
|
||||||
import com.nomadics9.ananas.utils.PreviewScrubListener
|
import com.nomadics9.ananas.utils.PreviewScrubListener
|
||||||
import com.nomadics9.ananas.viewmodels.PlayerActivityViewModel
|
import com.nomadics9.ananas.viewmodels.PlayerActivityViewModel
|
||||||
import com.nomadics9.ananas.viewmodels.PlayerEvents
|
import com.nomadics9.ananas.viewmodels.PlayerEvents
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -54,7 +52,6 @@ var isControlsLocked: Boolean = false
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PlayerActivity : BasePlayerActivity() {
|
class PlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var appPreferences: AppPreferences
|
lateinit var appPreferences: AppPreferences
|
||||||
|
|
||||||
|
@ -115,7 +112,8 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
configureInsets(lockedControls)
|
configureInsets(lockedControls)
|
||||||
|
|
||||||
if (appPreferences.playerGestures) {
|
if (appPreferences.playerGestures) {
|
||||||
playerGestureHelper = PlayerGestureHelper(
|
playerGestureHelper =
|
||||||
|
PlayerGestureHelper(
|
||||||
appPreferences,
|
appPreferences,
|
||||||
this,
|
this,
|
||||||
binding.playerView,
|
binding.playerView,
|
||||||
|
@ -155,7 +153,12 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
skipButton.text =
|
skipButton.text =
|
||||||
getString(CoreR.string.skip_intro_button)
|
getString(CoreR.string.skip_intro_button)
|
||||||
skipButton.isVisible =
|
skipButton.isVisible =
|
||||||
!isInPictureInPictureMode && !buttonPressed && (showSkip == true || (binding.playerView.isControllerFullyVisible && currentSegment?.skip == true))
|
!isInPictureInPictureMode &&
|
||||||
|
!buttonPressed &&
|
||||||
|
(
|
||||||
|
showSkip == true ||
|
||||||
|
(binding.playerView.isControllerFullyVisible && currentSegment?.skip == true)
|
||||||
|
)
|
||||||
watchCreditsButton.isVisible = false
|
watchCreditsButton.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +170,10 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
getString(CoreR.string.skip_credit_button_last)
|
getString(CoreR.string.skip_credit_button_last)
|
||||||
}
|
}
|
||||||
skipButton.isVisible =
|
skipButton.isVisible =
|
||||||
!isInPictureInPictureMode && !buttonPressed && currentSegment?.skip == true && !binding.playerView.isControllerFullyVisible
|
!isInPictureInPictureMode &&
|
||||||
|
!buttonPressed &&
|
||||||
|
currentSegment?.skip == true &&
|
||||||
|
!binding.playerView.isControllerFullyVisible
|
||||||
watchCreditsButton.isVisible = skipButton.isVisible
|
watchCreditsButton.isVisible = skipButton.isVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,12 +187,15 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
when (currentSegment?.type) {
|
when (currentSegment?.type) {
|
||||||
"intro" -> {
|
"intro" -> {
|
||||||
skipButton.isVisible =
|
skipButton.isVisible =
|
||||||
!buttonPressed && (showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
|
!buttonPressed &&
|
||||||
|
(showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
|
||||||
}
|
}
|
||||||
|
|
||||||
"credit" -> {
|
"credit" -> {
|
||||||
skipButton.isVisible =
|
skipButton.isVisible =
|
||||||
!buttonPressed && currentSegment?.skip == true && visibility == View.GONE
|
!buttonPressed &&
|
||||||
|
currentSegment?.skip == true &&
|
||||||
|
visibility == View.GONE
|
||||||
watchCreditsButton.isVisible = skipButton.isVisible
|
watchCreditsButton.isVisible = skipButton.isVisible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -268,7 +277,8 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
if (appPreferences.playerPipGesture) {
|
if (appPreferences.playerPipGesture) {
|
||||||
try {
|
try {
|
||||||
setPictureInPictureParams(pipParams(event.isPlaying))
|
setPictureInPictureParams(pipParams(event.isPlaying))
|
||||||
} catch (_: IllegalArgumentException) { }
|
} catch (_: IllegalArgumentException) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -346,7 +356,8 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
if (appPreferences.playerTrickplay) {
|
if (appPreferences.playerTrickplay) {
|
||||||
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||||
previewScrubListener = PreviewScrubListener(
|
previewScrubListener =
|
||||||
|
PreviewScrubListener(
|
||||||
imagePreview,
|
imagePreview,
|
||||||
timeBar,
|
timeBar,
|
||||||
viewModel.player,
|
viewModel.player,
|
||||||
|
@ -381,14 +392,16 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
|
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
|
||||||
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
|
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
|
||||||
|
|
||||||
val aspectRatio = binding.playerView.player?.videoSize?.let {
|
val aspectRatio =
|
||||||
|
binding.playerView.player?.videoSize?.let {
|
||||||
Rational(
|
Rational(
|
||||||
it.width.coerceAtMost((it.height * 2.39f).toInt()),
|
it.width.coerceAtMost((it.height * 2.39f).toInt()),
|
||||||
it.height.coerceAtMost((it.width * 2.39f).toInt()),
|
it.height.coerceAtMost((it.width * 2.39f).toInt()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceRectHint = if (displayAspectRatio < aspectRatio!!) {
|
val sourceRectHint =
|
||||||
|
if (displayAspectRatio < aspectRatio!!) {
|
||||||
val space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
|
val space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
|
||||||
Rect(
|
Rect(
|
||||||
0,
|
0,
|
||||||
|
@ -406,7 +419,9 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = PictureInPictureParams.Builder()
|
val builder =
|
||||||
|
PictureInPictureParams
|
||||||
|
.Builder()
|
||||||
.setAspectRatio(aspectRatio)
|
.setAspectRatio(aspectRatio)
|
||||||
.setSourceRectHint(sourceRectHint)
|
.setSourceRectHint(sourceRectHint)
|
||||||
|
|
||||||
|
@ -424,30 +439,51 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
enterPictureInPictureMode(pipParams())
|
enterPictureInPictureMode(pipParams())
|
||||||
} catch (_: IllegalArgumentException) { }
|
} catch (_: IllegalArgumentException) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showQualitySelectionDialog() {
|
private fun showQualitySelectionDialog() {
|
||||||
val height = viewModel.getOriginalHeight()
|
val height = viewModel.getOriginalHeight()
|
||||||
|
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList()
|
||||||
|
val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList()
|
||||||
|
|
||||||
val qualities = when (height) {
|
// Map entries to values
|
||||||
0 -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
|
val qualityMap = qualityEntries.zip(qualityValues).toMap()
|
||||||
in 1001..1999 -> arrayOf("Auto", "Original (1080p) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
|
|
||||||
in 2000..3000 -> arrayOf("Auto", "Original (4K) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
|
val qualities: List<String> =
|
||||||
else -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
|
when (height) {
|
||||||
|
0 -> qualityEntries
|
||||||
|
in 1001..1999 ->
|
||||||
|
listOf(
|
||||||
|
qualityEntries[0],
|
||||||
|
"${qualityEntries[1]} (1080p)",
|
||||||
|
qualityEntries[2],
|
||||||
|
qualityEntries[3],
|
||||||
|
qualityEntries[4],
|
||||||
|
qualityEntries[5],
|
||||||
|
)
|
||||||
|
in 2000..3000 ->
|
||||||
|
listOf(
|
||||||
|
qualityEntries[0],
|
||||||
|
"${qualityEntries[1]} (4K)",
|
||||||
|
qualityEntries[2],
|
||||||
|
qualityEntries[3],
|
||||||
|
qualityEntries[4],
|
||||||
|
qualityEntries[5],
|
||||||
|
)
|
||||||
|
else -> qualityEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle("Select Video Quality")
|
.setTitle("Select Video Quality")
|
||||||
.setItems(qualities) { _, which ->
|
.setItems(qualities.toTypedArray()) { _, which ->
|
||||||
val selectedQuality = qualities[which]
|
val selectedQualityEntry = qualities[which]
|
||||||
viewModel.changeVideoQuality(selectedQuality)
|
val selectedQualityValue =
|
||||||
|
qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry
|
||||||
|
viewModel.changeVideoQuality(selectedQualityValue)
|
||||||
|
}.show()
|
||||||
}
|
}
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override fun onPictureInPictureModeChanged(
|
override fun onPictureInPictureModeChanged(
|
||||||
isInPictureInPictureMode: Boolean,
|
isInPictureInPictureMode: Boolean,
|
||||||
|
@ -463,7 +499,8 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
playerGestureHelper?.updateZoomMode(false)
|
playerGestureHelper?.updateZoomMode(false)
|
||||||
|
|
||||||
// Brightness mode Auto
|
// Brightness mode Auto
|
||||||
window.attributes = window.attributes.apply {
|
window.attributes =
|
||||||
|
window.attributes.apply {
|
||||||
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -472,11 +509,14 @@ class PlayerActivity : BasePlayerActivity() {
|
||||||
playerGestureHelper?.updateZoomMode(wasZoom)
|
playerGestureHelper?.updateZoomMode(wasZoom)
|
||||||
|
|
||||||
// Override auto brightness
|
// Override auto brightness
|
||||||
window.attributes = window.attributes.apply {
|
window.attributes =
|
||||||
screenBrightness = if (appPreferences.playerBrightnessRemember) {
|
window.attributes.apply {
|
||||||
|
screenBrightness =
|
||||||
|
if (appPreferences.playerBrightnessRemember) {
|
||||||
appPreferences.playerBrightness
|
appPreferences.playerBrightness
|
||||||
} else {
|
} else {
|
||||||
Settings.System.getInt(
|
Settings.System
|
||||||
|
.getInt(
|
||||||
contentResolver,
|
contentResolver,
|
||||||
Settings.System.SCREEN_BRIGHTNESS,
|
Settings.System.SCREEN_BRIGHTNESS,
|
||||||
).toFloat() / 255
|
).toFloat() / 255
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.nomadics9.ananas.models.FindroidSource
|
||||||
import com.nomadics9.ananas.models.FindroidSources
|
import com.nomadics9.ananas.models.FindroidSources
|
||||||
import com.nomadics9.ananas.models.FindroidTrickplayInfo
|
import com.nomadics9.ananas.models.FindroidTrickplayInfo
|
||||||
import com.nomadics9.ananas.models.UiText
|
import com.nomadics9.ananas.models.UiText
|
||||||
|
import com.nomadics9.ananas.models.VideoQuality
|
||||||
import com.nomadics9.ananas.models.toFindroidEpisodeDto
|
import com.nomadics9.ananas.models.toFindroidEpisodeDto
|
||||||
import com.nomadics9.ananas.models.toFindroidMediaStreamDto
|
import com.nomadics9.ananas.models.toFindroidMediaStreamDto
|
||||||
import com.nomadics9.ananas.models.toFindroidMovieDto
|
import com.nomadics9.ananas.models.toFindroidMovieDto
|
||||||
|
@ -50,13 +51,13 @@ class DownloaderImpl(
|
||||||
storageIndex: Int,
|
storageIndex: Int,
|
||||||
): Pair<Long, UiText?> {
|
): Pair<Long, UiText?> {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
Timber.d("Downloading item: ${item.id} with sourceId: $sourceId")
|
Timber.d("Downloading item: ${item.id} with sourceId: $sourceId")
|
||||||
|
|
||||||
val source =
|
val source =
|
||||||
jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
|
jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
|
||||||
val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
|
val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
|
||||||
val trickplayInfo = if (item is FindroidSources) {
|
val trickplayInfo =
|
||||||
|
if (item is FindroidSources) {
|
||||||
item.trickplayInfo?.get(sourceId)
|
item.trickplayInfo?.get(sourceId)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -96,9 +97,13 @@ class DownloaderImpl(
|
||||||
|
|
||||||
return Pair(
|
return Pair(
|
||||||
-1,
|
-1,
|
||||||
if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(
|
if (e.message != null) {
|
||||||
CoreR.string.unknown_error
|
UiText.DynamicString(e.message!!)
|
||||||
|
} else {
|
||||||
|
UiText.StringResource(
|
||||||
|
CoreR.string.unknown_error,
|
||||||
)
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +115,7 @@ class DownloaderImpl(
|
||||||
trickplayInfo: FindroidTrickplayInfo?,
|
trickplayInfo: FindroidTrickplayInfo?,
|
||||||
segments: List<FindroidSegment>?,
|
segments: List<FindroidSegment>?,
|
||||||
path: Uri,
|
path: Uri,
|
||||||
quality: String
|
quality: String,
|
||||||
): Pair<Long, UiText?> {
|
): Pair<Long, UiText?> {
|
||||||
val transcodingUrl = getTranscodedUrl(item.id, quality)
|
val transcodingUrl = getTranscodedUrl(item.id, quality)
|
||||||
when (item) {
|
when (item) {
|
||||||
|
@ -126,7 +131,9 @@ class DownloaderImpl(
|
||||||
if (segments != null) {
|
if (segments != null) {
|
||||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||||
}
|
}
|
||||||
val request = DownloadManager.Request(transcodingUrl)
|
val request =
|
||||||
|
DownloadManager
|
||||||
|
.Request(transcodingUrl)
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||||
|
@ -139,7 +146,8 @@ class DownloaderImpl(
|
||||||
|
|
||||||
is FindroidEpisode -> {
|
is FindroidEpisode -> {
|
||||||
database.insertShow(
|
database.insertShow(
|
||||||
jellyfinRepository.getShow(item.seriesId)
|
jellyfinRepository
|
||||||
|
.getShow(item.seriesId)
|
||||||
.toFindroidShowDto(appPreferences.currentServer!!),
|
.toFindroidShowDto(appPreferences.currentServer!!),
|
||||||
)
|
)
|
||||||
database.insertSeason(
|
database.insertSeason(
|
||||||
|
@ -156,7 +164,9 @@ class DownloaderImpl(
|
||||||
if (segments != null) {
|
if (segments != null) {
|
||||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||||
}
|
}
|
||||||
val request = DownloadManager.Request(transcodingUrl)
|
val request =
|
||||||
|
DownloadManager
|
||||||
|
.Request(transcodingUrl)
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||||
|
@ -190,7 +200,9 @@ class DownloaderImpl(
|
||||||
if (segments != null) {
|
if (segments != null) {
|
||||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||||
}
|
}
|
||||||
val request = DownloadManager.Request(source.path.toUri())
|
val request =
|
||||||
|
DownloadManager
|
||||||
|
.Request(source.path.toUri())
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||||
|
@ -203,7 +215,8 @@ class DownloaderImpl(
|
||||||
|
|
||||||
is FindroidEpisode -> {
|
is FindroidEpisode -> {
|
||||||
database.insertShow(
|
database.insertShow(
|
||||||
jellyfinRepository.getShow(item.seriesId)
|
jellyfinRepository
|
||||||
|
.getShow(item.seriesId)
|
||||||
.toFindroidShowDto(appPreferences.currentServer!!),
|
.toFindroidShowDto(appPreferences.currentServer!!),
|
||||||
)
|
)
|
||||||
database.insertSeason(
|
database.insertSeason(
|
||||||
|
@ -219,7 +232,9 @@ class DownloaderImpl(
|
||||||
if (segments != null) {
|
if (segments != null) {
|
||||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||||
}
|
}
|
||||||
val request = DownloadManager.Request(source.path.toUri())
|
val request =
|
||||||
|
DownloadManager
|
||||||
|
.Request(source.path.toUri())
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||||
|
@ -233,15 +248,20 @@ class DownloaderImpl(
|
||||||
return Pair(-1, null)
|
return Pair(-1, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun cancelDownload(
|
||||||
override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) {
|
item: FindroidItem,
|
||||||
|
source: FindroidSource,
|
||||||
|
) {
|
||||||
if (source.downloadId != null) {
|
if (source.downloadId != null) {
|
||||||
downloadManager.remove(source.downloadId!!)
|
downloadManager.remove(source.downloadId!!)
|
||||||
}
|
}
|
||||||
deleteItem(item, source)
|
deleteItem(item, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteItem(item: FindroidItem, source: FindroidSource) {
|
override suspend fun deleteItem(
|
||||||
|
item: FindroidItem,
|
||||||
|
source: FindroidSource,
|
||||||
|
) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is FindroidMovie -> {
|
is FindroidMovie -> {
|
||||||
database.deleteMovie(item.id)
|
database.deleteMovie(item.id)
|
||||||
|
@ -283,11 +303,14 @@ class DownloaderImpl(
|
||||||
if (downloadId == null) {
|
if (downloadId == null) {
|
||||||
return Pair(downloadStatus, progress)
|
return Pair(downloadStatus, progress)
|
||||||
}
|
}
|
||||||
val query = DownloadManager.Query()
|
val query =
|
||||||
|
DownloadManager
|
||||||
|
.Query()
|
||||||
.setFilterById(downloadId)
|
.setFilterById(downloadId)
|
||||||
val cursor = downloadManager.query(query)
|
val cursor = downloadManager.query(query)
|
||||||
if (cursor.moveToFirst()) {
|
if (cursor.moveToFirst()) {
|
||||||
downloadStatus = cursor.getInt(
|
downloadStatus =
|
||||||
|
cursor.getInt(
|
||||||
cursor.getColumnIndexOrThrow(
|
cursor.getColumnIndexOrThrow(
|
||||||
DownloadManager.COLUMN_STATUS,
|
DownloadManager.COLUMN_STATUS,
|
||||||
),
|
),
|
||||||
|
@ -320,20 +343,23 @@ class DownloaderImpl(
|
||||||
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
|
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
|
||||||
for (mediaStream in source.mediaStreams.filter { it.isExternal }) {
|
for (mediaStream in source.mediaStreams.filter { it.isExternal }) {
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
val streamPath = Uri.fromFile(
|
val streamPath =
|
||||||
|
Uri.fromFile(
|
||||||
File(
|
File(
|
||||||
storageLocation,
|
storageLocation,
|
||||||
"downloads/${item.id}.${source.id}.$id.download"
|
"downloads/${item.id}.${source.id}.$id.download",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
database.insertMediaStream(
|
database.insertMediaStream(
|
||||||
mediaStream.toFindroidMediaStreamDto(
|
mediaStream.toFindroidMediaStreamDto(
|
||||||
id,
|
id,
|
||||||
source.id,
|
source.id,
|
||||||
streamPath.path.orEmpty()
|
streamPath.path.orEmpty(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
val request =
|
||||||
val request = DownloadManager.Request(Uri.parse(mediaStream.path))
|
DownloadManager
|
||||||
|
.Request(Uri.parse(mediaStream.path))
|
||||||
.setTitle(mediaStream.title)
|
.setTitle(mediaStream.title)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||||
|
@ -347,7 +373,7 @@ class DownloaderImpl(
|
||||||
private fun downloadEmbeddedMediaStreams(
|
private fun downloadEmbeddedMediaStreams(
|
||||||
item: FindroidItem,
|
item: FindroidItem,
|
||||||
source: FindroidSource,
|
source: FindroidSource,
|
||||||
storageIndex: Int = 0
|
storageIndex: Int = 0,
|
||||||
) {
|
) {
|
||||||
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
|
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
|
||||||
val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null }
|
val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null }
|
||||||
|
@ -357,20 +383,23 @@ class DownloaderImpl(
|
||||||
deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt")
|
deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt")
|
||||||
}
|
}
|
||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
val streamPath = Uri.fromFile(
|
val streamPath =
|
||||||
|
Uri.fromFile(
|
||||||
File(
|
File(
|
||||||
storageLocation,
|
storageLocation,
|
||||||
"downloads/${item.id}.${source.id}.$id.download"
|
"downloads/${item.id}.${source.id}.$id.download",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
database.insertMediaStream(
|
database.insertMediaStream(
|
||||||
mediaStream.toFindroidMediaStreamDto(
|
mediaStream.toFindroidMediaStreamDto(
|
||||||
id,
|
id,
|
||||||
source.id,
|
source.id,
|
||||||
streamPath.path.orEmpty()
|
streamPath.path.orEmpty(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
val request =
|
||||||
val request = DownloadManager.Request(Uri.parse(deliveryUrl))
|
DownloadManager
|
||||||
|
.Request(Uri.parse(deliveryUrl))
|
||||||
.setTitle(mediaStream.title)
|
.setTitle(mediaStream.title)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||||
|
@ -382,19 +411,21 @@ class DownloaderImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun downloadTrickplayData(
|
private suspend fun downloadTrickplayData(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
sourceId: String,
|
sourceId: String,
|
||||||
trickplayInfo: FindroidTrickplayInfo,
|
trickplayInfo: FindroidTrickplayInfo,
|
||||||
) {
|
) {
|
||||||
val maxIndex = ceil(
|
val maxIndex =
|
||||||
trickplayInfo.thumbnailCount.toDouble()
|
ceil(
|
||||||
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)
|
trickplayInfo.thumbnailCount
|
||||||
|
.toDouble()
|
||||||
|
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
|
||||||
).toInt()
|
).toInt()
|
||||||
val byteArrays = mutableListOf<ByteArray>()
|
val byteArrays = mutableListOf<ByteArray>()
|
||||||
for (i in 0..maxIndex) {
|
for (i in 0..maxIndex) {
|
||||||
jellyfinRepository.getTrickplayData(
|
jellyfinRepository
|
||||||
|
.getTrickplayData(
|
||||||
itemId,
|
itemId,
|
||||||
trickplayInfo.width,
|
trickplayInfo.width,
|
||||||
i,
|
i,
|
||||||
|
@ -420,52 +451,46 @@ class DownloaderImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getTranscodedUrl(itemId: UUID, quality: String): Uri? {
|
private suspend fun getTranscodedUrl(
|
||||||
val maxBitrate = when (quality) {
|
itemId: UUID,
|
||||||
"720p" -> 2000000 // 2 Mbps
|
quality: String,
|
||||||
"480p" -> 1000000 // 1 Mbps
|
): Uri? {
|
||||||
"360p" -> 800000 // 800Kbps
|
val videoQuality = VideoQuality.fromString(quality)!!
|
||||||
else -> 2000000 // Default to 2 Mbps if not specified
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
val deviceProfile =
|
||||||
val deviceProfile = jellyfinRepository.buildDeviceProfile(maxBitrate,"mkv", EncodingContext.STATIC)
|
jellyfinRepository.buildDeviceProfile(
|
||||||
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,false,deviceProfile,maxBitrate)
|
VideoQuality.getBitrate(videoQuality),
|
||||||
val mediaSourceId = playbackInfo.content.mediaSources.firstOrNull()?.id!!
|
"mkv",
|
||||||
|
EncodingContext.STATIC,
|
||||||
|
)
|
||||||
|
val playbackInfo =
|
||||||
|
jellyfinRepository.getPostedPlaybackInfo(
|
||||||
|
itemId,
|
||||||
|
false,
|
||||||
|
deviceProfile,
|
||||||
|
VideoQuality.getBitrate(videoQuality),
|
||||||
|
)
|
||||||
|
val mediaSourceId =
|
||||||
|
playbackInfo.content.mediaSources
|
||||||
|
.firstOrNull()
|
||||||
|
?.id!!
|
||||||
val playSessionId = playbackInfo.content.playSessionId!!
|
val playSessionId = playbackInfo.content.playSessionId!!
|
||||||
val deviceId = jellyfinRepository.getDeviceId()
|
val deviceId = jellyfinRepository.getDeviceId()
|
||||||
val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, deviceId, mediaSourceId, playSessionId, maxBitrate, "ts")
|
val downloadUrl =
|
||||||
|
jellyfinRepository.getVideoStreambyContainerUrl(
|
||||||
|
itemId,
|
||||||
|
deviceId,
|
||||||
|
mediaSourceId,
|
||||||
|
playSessionId,
|
||||||
|
VideoQuality.getBitrate(videoQuality),
|
||||||
|
VideoQuality.getQualityInt(videoQuality),
|
||||||
|
"mkv",
|
||||||
|
)
|
||||||
|
|
||||||
val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality)
|
return downloadUrl.toUri()
|
||||||
Timber.d("Constructed Transcode URL: $transcodeUri")
|
|
||||||
transcodeUri
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildTranscodeUri(
|
|
||||||
transcodingUrl: String,
|
|
||||||
maxBitrate: Int,
|
|
||||||
quality: String
|
|
||||||
): Uri {
|
|
||||||
val resolution = when (quality) {
|
|
||||||
"720p" -> "720"
|
|
||||||
"480p" -> "480"
|
|
||||||
"360p" -> "360"
|
|
||||||
else -> "720"
|
|
||||||
}
|
}
|
||||||
return Uri.parse(transcodingUrl).buildUpon()
|
|
||||||
.appendQueryParameter("MaxVideoHeight", resolution)
|
|
||||||
.appendQueryParameter("MaxVideoBitRate", maxBitrate.toString())
|
|
||||||
.appendQueryParameter("subtitleMethod", "External")
|
|
||||||
//.appendQueryParameter("api_key", apiKey)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,13 +26,17 @@
|
||||||
<item>opensles</item>
|
<item>opensles</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="quality_entries">
|
<string-array name="quality_entries">
|
||||||
|
<item>Auto</item>
|
||||||
<item>Original</item>
|
<item>Original</item>
|
||||||
|
<item>1080p - 8Mbps</item>
|
||||||
<item>720p - 2Mbps</item>
|
<item>720p - 2Mbps</item>
|
||||||
<item>480p - 1Mbps</item>
|
<item>480p - 1Mbps</item>
|
||||||
<item>360p - 800Kbps</item>
|
<item>360p - 800Kbps</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="quality_values">
|
<string-array name="quality_values">
|
||||||
|
<item>Auto</item>
|
||||||
<item>Original</item>
|
<item>Original</item>
|
||||||
|
<item>1080p</item>
|
||||||
<item>720p</item>
|
<item>720p</item>
|
||||||
<item>480p</item>
|
<item>480p</item>
|
||||||
<item>360p</item>
|
<item>360p</item>
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.nomadics9.ananas.models
|
||||||
|
|
||||||
|
enum class VideoQuality(
|
||||||
|
val bitrate: Int,
|
||||||
|
val qualityString: String,
|
||||||
|
val qualityInt: Int,
|
||||||
|
) {
|
||||||
|
PAuto(1, "Auto", 1080),
|
||||||
|
POriginal(1000000000, "Original", 1080),
|
||||||
|
P1080(8000000, "1080p", 1080),
|
||||||
|
P720(2000000, "720p", 720),
|
||||||
|
P480(1000000, "480p", 480),
|
||||||
|
P360(700000, "360p", 360),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(quality: String): VideoQuality? = entries.find { it.qualityString == quality }
|
||||||
|
|
||||||
|
fun getBitrate(quality: VideoQuality): Int = quality.bitrate
|
||||||
|
|
||||||
|
fun getQualityString(quality: VideoQuality): String = quality.qualityString
|
||||||
|
|
||||||
|
fun getQualityInt(quality: VideoQuality): Int = quality.qualityInt
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,8 +14,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import org.jellyfin.sdk.api.client.Response
|
import org.jellyfin.sdk.api.client.Response
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.DeviceInfo
|
|
||||||
import org.jellyfin.sdk.model.api.DeviceInfoQueryResult
|
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import org.jellyfin.sdk.model.api.EncodingContext
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
@ -31,7 +29,9 @@ interface JellyfinRepository {
|
||||||
suspend fun getUserViews(): List<BaseItemDto>
|
suspend fun getUserViews(): List<BaseItemDto>
|
||||||
|
|
||||||
suspend fun getItem(itemId: UUID): BaseItemDto
|
suspend fun getItem(itemId: UUID): BaseItemDto
|
||||||
|
|
||||||
suspend fun getEpisode(itemId: UUID): FindroidEpisode
|
suspend fun getEpisode(itemId: UUID): FindroidEpisode
|
||||||
|
|
||||||
suspend fun getMovie(itemId: UUID): FindroidMovie
|
suspend fun getMovie(itemId: UUID): FindroidMovie
|
||||||
|
|
||||||
suspend fun getShow(itemId: UUID): FindroidShow
|
suspend fun getShow(itemId: UUID): FindroidShow
|
||||||
|
@ -72,7 +72,10 @@ interface JellyfinRepository {
|
||||||
|
|
||||||
suspend fun getLatestMedia(parentId: UUID): List<FindroidItem>
|
suspend fun getLatestMedia(parentId: UUID): List<FindroidItem>
|
||||||
|
|
||||||
suspend fun getSeasons(seriesId: UUID, offline: Boolean = false): List<FindroidSeason>
|
suspend fun getSeasons(
|
||||||
|
seriesId: UUID,
|
||||||
|
offline: Boolean = false,
|
||||||
|
): List<FindroidSeason>
|
||||||
|
|
||||||
suspend fun getNextUp(seriesId: UUID? = null): List<FindroidEpisode>
|
suspend fun getNextUp(seriesId: UUID? = null): List<FindroidEpisode>
|
||||||
|
|
||||||
|
@ -85,21 +88,40 @@ interface JellyfinRepository {
|
||||||
offline: Boolean = false,
|
offline: Boolean = false,
|
||||||
): List<FindroidEpisode>
|
): List<FindroidEpisode>
|
||||||
|
|
||||||
suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List<FindroidSource>
|
suspend fun getMediaSources(
|
||||||
|
itemId: UUID,
|
||||||
|
includePath: Boolean = false,
|
||||||
|
): List<FindroidSource>
|
||||||
|
|
||||||
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String
|
suspend fun getStreamUrl(
|
||||||
|
itemId: UUID,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String? = null,
|
||||||
|
): String
|
||||||
|
|
||||||
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
|
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
|
||||||
|
|
||||||
suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?
|
suspend fun getTrickplayData(
|
||||||
|
itemId: UUID,
|
||||||
|
width: Int,
|
||||||
|
index: Int,
|
||||||
|
): ByteArray?
|
||||||
|
|
||||||
suspend fun postCapabilities()
|
suspend fun postCapabilities()
|
||||||
|
|
||||||
suspend fun postPlaybackStart(itemId: UUID)
|
suspend fun postPlaybackStart(itemId: UUID)
|
||||||
|
|
||||||
suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int)
|
suspend fun postPlaybackStop(
|
||||||
|
itemId: UUID,
|
||||||
|
positionTicks: Long,
|
||||||
|
playedPercentage: Int,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun postPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean)
|
suspend fun postPlaybackProgress(
|
||||||
|
itemId: UUID,
|
||||||
|
positionTicks: Long,
|
||||||
|
isPaused: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun markAsFavorite(itemId: UUID)
|
suspend fun markAsFavorite(itemId: UUID)
|
||||||
|
|
||||||
|
@ -121,15 +143,36 @@ interface JellyfinRepository {
|
||||||
|
|
||||||
suspend fun getDeviceId(): String
|
suspend fun getDeviceId(): String
|
||||||
|
|
||||||
suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int>
|
suspend fun buildDeviceProfile(
|
||||||
|
maxBitrate: Int,
|
||||||
|
container: String,
|
||||||
|
context: EncodingContext,
|
||||||
|
): DeviceProfile
|
||||||
|
|
||||||
suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile
|
suspend fun getVideoStreambyContainerUrl(
|
||||||
|
itemId: UUID,
|
||||||
|
deviceId: String,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String,
|
||||||
|
videoBitrate: Int,
|
||||||
|
maxHeight: Int,
|
||||||
|
container: String,
|
||||||
|
): String
|
||||||
|
|
||||||
suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String
|
suspend fun getTranscodedVideoStream(
|
||||||
|
itemId: UUID,
|
||||||
|
deviceId: String,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String,
|
||||||
|
videoBitrate: Int,
|
||||||
|
): String
|
||||||
|
|
||||||
suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String
|
suspend fun getPostedPlaybackInfo(
|
||||||
|
itemId: UUID,
|
||||||
suspend fun getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse>
|
enableDirectStream: Boolean,
|
||||||
|
deviceProfile: DeviceProfile,
|
||||||
|
maxBitrate: Int,
|
||||||
|
): Response<PlaybackInfoResponse>
|
||||||
|
|
||||||
suspend fun stopEncodingProcess(playSessionId: String)
|
suspend fun stopEncodingProcess(playSessionId: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.nomadics9.ananas.models.FindroidSegments
|
||||||
import com.nomadics9.ananas.models.FindroidShow
|
import com.nomadics9.ananas.models.FindroidShow
|
||||||
import com.nomadics9.ananas.models.FindroidSource
|
import com.nomadics9.ananas.models.FindroidSource
|
||||||
import com.nomadics9.ananas.models.SortBy
|
import com.nomadics9.ananas.models.SortBy
|
||||||
|
import com.nomadics9.ananas.models.VideoQuality
|
||||||
import com.nomadics9.ananas.models.toFindroidCollection
|
import com.nomadics9.ananas.models.toFindroidCollection
|
||||||
import com.nomadics9.ananas.models.toFindroidEpisode
|
import com.nomadics9.ananas.models.toFindroidEpisode
|
||||||
import com.nomadics9.ananas.models.toFindroidItem
|
import com.nomadics9.ananas.models.toFindroidItem
|
||||||
|
@ -36,7 +37,6 @@ import org.jellyfin.sdk.api.client.extensions.hlsSegmentApi
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto
|
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto
|
||||||
import org.jellyfin.sdk.model.api.DeviceInfoQueryResult
|
|
||||||
import org.jellyfin.sdk.model.api.DeviceOptionsDto
|
import org.jellyfin.sdk.model.api.DeviceOptionsDto
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import org.jellyfin.sdk.model.api.DirectPlayProfile
|
import org.jellyfin.sdk.model.api.DirectPlayProfile
|
||||||
|
@ -57,7 +57,6 @@ import org.jellyfin.sdk.model.api.PublicSystemInfo
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||||
import org.jellyfin.sdk.model.api.SubtitleProfile
|
import org.jellyfin.sdk.model.api.SubtitleProfile
|
||||||
import org.jellyfin.sdk.model.api.TranscodeReason
|
|
||||||
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
|
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
|
||||||
import org.jellyfin.sdk.model.api.TranscodingProfile
|
import org.jellyfin.sdk.model.api.TranscodingProfile
|
||||||
import org.jellyfin.sdk.model.api.UserConfiguration
|
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||||
|
@ -71,53 +70,68 @@ class JellyfinRepositoryImpl(
|
||||||
private val database: ServerDatabaseDao,
|
private val database: ServerDatabaseDao,
|
||||||
private val appPreferences: AppPreferences,
|
private val appPreferences: AppPreferences,
|
||||||
) : JellyfinRepository {
|
) : JellyfinRepository {
|
||||||
override suspend fun getPublicSystemInfo(): PublicSystemInfo = withContext(Dispatchers.IO) {
|
override suspend fun getPublicSystemInfo(): PublicSystemInfo =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.systemApi.getPublicSystemInfo().content
|
jellyfinApi.systemApi.getPublicSystemInfo().content
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
override suspend fun getUserViews(): List<BaseItemDto> =
|
||||||
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty()
|
withContext(Dispatchers.IO) {
|
||||||
|
jellyfinApi.viewsApi
|
||||||
|
.getUserViews(jellyfinApi.userId!!)
|
||||||
|
.content.items
|
||||||
|
.orEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getItem(itemId: UUID): BaseItemDto = withContext(Dispatchers.IO) {
|
override suspend fun getItem(itemId: UUID): BaseItemDto =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
|
jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getEpisode(itemId: UUID): FindroidEpisode =
|
override suspend fun getEpisode(itemId: UUID): FindroidEpisode =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.userLibraryApi.getItem(
|
jellyfinApi.userLibraryApi
|
||||||
|
.getItem(
|
||||||
itemId,
|
itemId,
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
|
).content
|
||||||
|
.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMovie(itemId: UUID): FindroidMovie =
|
override suspend fun getMovie(itemId: UUID): FindroidMovie =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.userLibraryApi.getItem(
|
jellyfinApi.userLibraryApi
|
||||||
|
.getItem(
|
||||||
itemId,
|
itemId,
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
).content.toFindroidMovie(this@JellyfinRepositoryImpl, database)
|
).content
|
||||||
|
.toFindroidMovie(this@JellyfinRepositoryImpl, database)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getShow(itemId: UUID): FindroidShow =
|
override suspend fun getShow(itemId: UUID): FindroidShow =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.userLibraryApi.getItem(
|
jellyfinApi.userLibraryApi
|
||||||
|
.getItem(
|
||||||
itemId,
|
itemId,
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
).content.toFindroidShow(this@JellyfinRepositoryImpl)
|
).content
|
||||||
|
.toFindroidShow(this@JellyfinRepositoryImpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSeason(itemId: UUID): FindroidSeason =
|
override suspend fun getSeason(itemId: UUID): FindroidSeason =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.userLibraryApi.getItem(
|
jellyfinApi.userLibraryApi
|
||||||
|
.getItem(
|
||||||
itemId,
|
itemId,
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
).content.toFindroidSeason(this@JellyfinRepositoryImpl)
|
).content
|
||||||
|
.toFindroidSeason(this@JellyfinRepositoryImpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLibraries(): List<FindroidCollection> =
|
override suspend fun getLibraries(): List<FindroidCollection> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.itemsApi.getItems(
|
jellyfinApi.itemsApi
|
||||||
|
.getItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
).content.items
|
).content.items
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
|
@ -134,7 +148,8 @@ class JellyfinRepositoryImpl(
|
||||||
limit: Int?,
|
limit: Int?,
|
||||||
): List<FindroidItem> =
|
): List<FindroidItem> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.itemsApi.getItems(
|
jellyfinApi.itemsApi
|
||||||
|
.getItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
includeItemTypes = includeTypes,
|
includeItemTypes = includeTypes,
|
||||||
|
@ -154,9 +169,10 @@ class JellyfinRepositoryImpl(
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
sortBy: SortBy,
|
sortBy: SortBy,
|
||||||
sortOrder: SortOrder,
|
sortOrder: SortOrder,
|
||||||
): Flow<PagingData<FindroidItem>> {
|
): Flow<PagingData<FindroidItem>> =
|
||||||
return Pager(
|
Pager(
|
||||||
config = PagingConfig(
|
config =
|
||||||
|
PagingConfig(
|
||||||
pageSize = 10,
|
pageSize = 10,
|
||||||
maxSize = 100,
|
maxSize = 100,
|
||||||
enablePlaceholders = false,
|
enablePlaceholders = false,
|
||||||
|
@ -172,14 +188,15 @@ class JellyfinRepositoryImpl(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
).flow
|
).flow
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPersonItems(
|
override suspend fun getPersonItems(
|
||||||
personIds: List<UUID>,
|
personIds: List<UUID>,
|
||||||
includeTypes: List<BaseItemKind>?,
|
includeTypes: List<BaseItemKind>?,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
): List<FindroidItem> = withContext(Dispatchers.IO) {
|
): List<FindroidItem> =
|
||||||
jellyfinApi.itemsApi.getItems(
|
withContext(Dispatchers.IO) {
|
||||||
|
jellyfinApi.itemsApi
|
||||||
|
.getItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
personIds = personIds,
|
personIds = personIds,
|
||||||
includeItemTypes = includeTypes,
|
includeItemTypes = includeTypes,
|
||||||
|
@ -193,10 +210,12 @@ class JellyfinRepositoryImpl(
|
||||||
|
|
||||||
override suspend fun getFavoriteItems(): List<FindroidItem> =
|
override suspend fun getFavoriteItems(): List<FindroidItem> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.itemsApi.getItems(
|
jellyfinApi.itemsApi
|
||||||
|
.getItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
filters = listOf(ItemFilter.IS_FAVORITE),
|
filters = listOf(ItemFilter.IS_FAVORITE),
|
||||||
includeItemTypes = listOf(
|
includeItemTypes =
|
||||||
|
listOf(
|
||||||
BaseItemKind.MOVIE,
|
BaseItemKind.MOVIE,
|
||||||
BaseItemKind.SERIES,
|
BaseItemKind.SERIES,
|
||||||
BaseItemKind.EPISODE,
|
BaseItemKind.EPISODE,
|
||||||
|
@ -209,10 +228,12 @@ class JellyfinRepositoryImpl(
|
||||||
|
|
||||||
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
|
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.itemsApi.getItems(
|
jellyfinApi.itemsApi
|
||||||
|
.getItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
searchTerm = searchQuery,
|
searchTerm = searchQuery,
|
||||||
includeItemTypes = listOf(
|
includeItemTypes =
|
||||||
|
listOf(
|
||||||
BaseItemKind.MOVIE,
|
BaseItemKind.MOVIE,
|
||||||
BaseItemKind.SERIES,
|
BaseItemKind.SERIES,
|
||||||
BaseItemKind.EPISODE,
|
BaseItemKind.EPISODE,
|
||||||
|
@ -224,12 +245,15 @@ class JellyfinRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getResumeItems(): List<FindroidItem> {
|
override suspend fun getResumeItems(): List<FindroidItem> {
|
||||||
val items = withContext(Dispatchers.IO) {
|
val items =
|
||||||
jellyfinApi.itemsApi.getResumeItems(
|
withContext(Dispatchers.IO) {
|
||||||
|
jellyfinApi.itemsApi
|
||||||
|
.getResumeItems(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
limit = 12,
|
limit = 12,
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
||||||
).content.items.orEmpty()
|
).content.items
|
||||||
|
.orEmpty()
|
||||||
}
|
}
|
||||||
return items.mapNotNull {
|
return items.mapNotNull {
|
||||||
it.toFindroidItem(this, database)
|
it.toFindroidItem(this, database)
|
||||||
|
@ -237,8 +261,10 @@ class JellyfinRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
|
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
|
||||||
val items = withContext(Dispatchers.IO) {
|
val items =
|
||||||
jellyfinApi.userLibraryApi.getLatestMedia(
|
withContext(Dispatchers.IO) {
|
||||||
|
jellyfinApi.userLibraryApi
|
||||||
|
.getLatestMedia(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
parentId = parentId,
|
parentId = parentId,
|
||||||
limit = 16,
|
limit = 16,
|
||||||
|
@ -249,10 +275,15 @@ class JellyfinRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> =
|
override suspend fun getSeasons(
|
||||||
|
seriesId: UUID,
|
||||||
|
offline: Boolean,
|
||||||
|
): List<FindroidSeason> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items
|
jellyfinApi.showsApi
|
||||||
|
.getSeasons(seriesId, jellyfinApi.userId!!)
|
||||||
|
.content.items
|
||||||
.orEmpty()
|
.orEmpty()
|
||||||
.map { it.toFindroidSeason(this@JellyfinRepositoryImpl) }
|
.map { it.toFindroidSeason(this@JellyfinRepositoryImpl) }
|
||||||
} else {
|
} else {
|
||||||
|
@ -262,7 +293,8 @@ class JellyfinRepositoryImpl(
|
||||||
|
|
||||||
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
|
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.showsApi.getNextUp(
|
jellyfinApi.showsApi
|
||||||
|
.getNextUp(
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
limit = 24,
|
limit = 24,
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
@ -282,7 +314,8 @@ class JellyfinRepositoryImpl(
|
||||||
): List<FindroidEpisode> =
|
): List<FindroidEpisode> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
jellyfinApi.showsApi.getEpisodes(
|
jellyfinApi.showsApi
|
||||||
|
.getEpisodes(
|
||||||
seriesId,
|
seriesId,
|
||||||
jellyfinApi.userId!!,
|
jellyfinApi.userId!!,
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
@ -297,33 +330,41 @@ class JellyfinRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> =
|
override suspend fun getMediaSources(
|
||||||
|
itemId: UUID,
|
||||||
|
includePath: Boolean,
|
||||||
|
): List<FindroidSource> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val sources = mutableListOf<FindroidSource>()
|
val sources = mutableListOf<FindroidSource>()
|
||||||
sources.addAll(
|
sources.addAll(
|
||||||
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
jellyfinApi.mediaInfoApi
|
||||||
|
.getPostedPlaybackInfo(
|
||||||
itemId,
|
itemId,
|
||||||
PlaybackInfoDto(
|
PlaybackInfoDto(
|
||||||
userId = jellyfinApi.userId!!,
|
userId = jellyfinApi.userId!!,
|
||||||
deviceProfile = DeviceProfile(
|
deviceProfile =
|
||||||
|
DeviceProfile(
|
||||||
name = "Direct play all",
|
name = "Direct play all",
|
||||||
maxStaticBitrate = 1_000_000_000,
|
maxStaticBitrate = 1_000_000_000,
|
||||||
maxStreamingBitrate = 1_000_000_000,
|
maxStreamingBitrate = 1_000_000_000,
|
||||||
codecProfiles = emptyList(),
|
codecProfiles = emptyList(),
|
||||||
containerProfiles = emptyList(),
|
containerProfiles = emptyList(),
|
||||||
directPlayProfiles = listOf(
|
directPlayProfiles =
|
||||||
|
listOf(
|
||||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||||
),
|
),
|
||||||
transcodingProfiles = emptyList(),
|
transcodingProfiles = emptyList(),
|
||||||
subtitleProfiles = listOf(
|
subtitleProfiles =
|
||||||
|
listOf(
|
||||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
maxStreamingBitrate = 1_000_000_000,
|
maxStreamingBitrate = 1_000_000_000,
|
||||||
),
|
),
|
||||||
).content.mediaSources.map {
|
).content.mediaSources
|
||||||
|
.map {
|
||||||
it.toFindroidSource(
|
it.toFindroidSource(
|
||||||
this@JellyfinRepositoryImpl,
|
this@JellyfinRepositoryImpl,
|
||||||
itemId,
|
itemId,
|
||||||
|
@ -337,24 +378,30 @@ class JellyfinRepositoryImpl(
|
||||||
sources
|
sources
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String =
|
override suspend fun getStreamUrl(
|
||||||
|
itemId: UUID,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String?,
|
||||||
|
): String =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
// val deviceId = getDeviceId()
|
||||||
try {
|
try {
|
||||||
val url = if (playSessionId != null) {
|
val url =
|
||||||
|
if (playSessionId != null) {
|
||||||
jellyfinApi.videosApi.getVideoStreamUrl(
|
jellyfinApi.videosApi.getVideoStreamUrl(
|
||||||
itemId,
|
itemId,
|
||||||
static = true,
|
static = true,
|
||||||
mediaSourceId = mediaSourceId,
|
mediaSourceId = mediaSourceId,
|
||||||
playSessionId = playSessionId,
|
playSessionId = playSessionId,
|
||||||
deviceId = getDeviceId(),
|
// deviceId = deviceId,
|
||||||
context = EncodingContext.STATIC
|
context = EncodingContext.STREAMING,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
jellyfinApi.videosApi.getVideoStreamUrl(
|
jellyfinApi.videosApi.getVideoStreamUrl(
|
||||||
itemId,
|
itemId,
|
||||||
static = true,
|
static = true,
|
||||||
mediaSourceId = mediaSourceId,
|
mediaSourceId = mediaSourceId,
|
||||||
deviceId = getDeviceId(),
|
// deviceId = deviceId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
url
|
url
|
||||||
|
@ -377,12 +424,15 @@ class JellyfinRepositoryImpl(
|
||||||
pathParameters["itemId"] = itemId
|
pathParameters["itemId"] = itemId
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val segmentToConvert = jellyfinApi.api.get<FindroidSegments>(
|
val segmentToConvert =
|
||||||
|
jellyfinApi.api
|
||||||
|
.get<FindroidSegments>(
|
||||||
"/Episode/{itemId}/IntroSkipperSegments",
|
"/Episode/{itemId}/IntroSkipperSegments",
|
||||||
pathParameters,
|
pathParameters,
|
||||||
).content
|
).content
|
||||||
|
|
||||||
val segmentConverted = mutableListOf(
|
val segmentConverted =
|
||||||
|
mutableListOf(
|
||||||
segmentToConvert.intro!!.let {
|
segmentToConvert.intro!!.let {
|
||||||
FindroidSegment(
|
FindroidSegment(
|
||||||
type = "intro",
|
type = "intro",
|
||||||
|
@ -413,7 +463,11 @@ class JellyfinRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? =
|
override suspend fun getTrickplayData(
|
||||||
|
itemId: UUID,
|
||||||
|
width: Int,
|
||||||
|
index: Int,
|
||||||
|
): ByteArray? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
|
@ -421,9 +475,13 @@ class JellyfinRepositoryImpl(
|
||||||
if (sources != null) {
|
if (sources != null) {
|
||||||
return@withContext File(sources.first(), index.toString()).readBytes()
|
return@withContext File(sources.first(), index.toString()).readBytes()
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
return@withContext jellyfinApi.trickplayApi.getTrickplayTileImage(itemId, width, index).content.toByteArray()
|
return@withContext jellyfinApi.trickplayApi
|
||||||
|
.getTrickplayTileImage(itemId, width, index)
|
||||||
|
.content
|
||||||
|
.toByteArray()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
|
@ -434,7 +492,8 @@ class JellyfinRepositoryImpl(
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
jellyfinApi.sessionApi.postCapabilities(
|
jellyfinApi.sessionApi.postCapabilities(
|
||||||
playableMediaTypes = listOf(MediaType.VIDEO),
|
playableMediaTypes = listOf(MediaType.VIDEO),
|
||||||
supportedCommands = listOf(
|
supportedCommands =
|
||||||
|
listOf(
|
||||||
GeneralCommandType.VOLUME_UP,
|
GeneralCommandType.VOLUME_UP,
|
||||||
GeneralCommandType.VOLUME_DOWN,
|
GeneralCommandType.VOLUME_DOWN,
|
||||||
GeneralCommandType.TOGGLE_MUTE,
|
GeneralCommandType.TOGGLE_MUTE,
|
||||||
|
@ -570,57 +629,62 @@ class JellyfinRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUserConfiguration(): UserConfiguration = withContext(Dispatchers.IO) {
|
override suspend fun getUserConfiguration(): UserConfiguration =
|
||||||
jellyfinApi.userApi.getCurrentUser().content.configuration!!
|
withContext(Dispatchers.IO) {
|
||||||
|
jellyfinApi.userApi
|
||||||
|
.getCurrentUser()
|
||||||
|
.content.configuration!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDownloads(): List<FindroidItem> =
|
override suspend fun getDownloads(): List<FindroidItem> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val items = mutableListOf<FindroidItem>()
|
val items = mutableListOf<FindroidItem>()
|
||||||
items.addAll(
|
items.addAll(
|
||||||
database.getMoviesByServerId(appPreferences.currentServer!!)
|
database
|
||||||
|
.getMoviesByServerId(appPreferences.currentServer!!)
|
||||||
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
|
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
|
||||||
)
|
)
|
||||||
items.addAll(
|
items.addAll(
|
||||||
database.getShowsByServerId(appPreferences.currentServer!!)
|
database
|
||||||
|
.getShowsByServerId(appPreferences.currentServer!!)
|
||||||
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
|
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
|
||||||
)
|
)
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUserId(): UUID {
|
override fun getUserId(): UUID = jellyfinApi.userId!!
|
||||||
return jellyfinApi.userId!!
|
|
||||||
}
|
|
||||||
|
|
||||||
|
override suspend fun buildDeviceProfile(
|
||||||
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
|
maxBitrate: Int,
|
||||||
return when (transcodeResolution) {
|
container: String,
|
||||||
1080 -> 8000000 to 384000 // Adjusted for 1080p
|
context: EncodingContext,
|
||||||
720 -> 2000000 to 384000 // Adjusted for 720p
|
): DeviceProfile {
|
||||||
480 -> 1000000 to 384000 // Adjusted for 480p
|
val deviceProfile =
|
||||||
360 -> 800000 to 128000 // Adjusted for 360p
|
ClientCapabilitiesDto(
|
||||||
else -> 12000000 to 384000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile {
|
|
||||||
val deviceProfile = ClientCapabilitiesDto(
|
|
||||||
supportedCommands = emptyList(),
|
supportedCommands = emptyList(),
|
||||||
playableMediaTypes = emptyList(),
|
playableMediaTypes =
|
||||||
|
listOf(
|
||||||
|
MediaType.VIDEO,
|
||||||
|
MediaType.AUDIO,
|
||||||
|
MediaType.UNKNOWN,
|
||||||
|
),
|
||||||
supportsMediaControl = true,
|
supportsMediaControl = true,
|
||||||
supportsPersistentIdentifier = true,
|
supportsPersistentIdentifier = true,
|
||||||
deviceProfile = DeviceProfile(
|
deviceProfile =
|
||||||
|
DeviceProfile(
|
||||||
name = "AnanasUser",
|
name = "AnanasUser",
|
||||||
id = getUserId().toString(),
|
id = getUserId().toString(),
|
||||||
maxStaticBitrate = maxBitrate,
|
maxStaticBitrate = maxBitrate,
|
||||||
maxStreamingBitrate = maxBitrate,
|
maxStreamingBitrate = maxBitrate,
|
||||||
codecProfiles = emptyList(),
|
codecProfiles = emptyList(),
|
||||||
containerProfiles = listOf(),
|
containerProfiles = listOf(),
|
||||||
directPlayProfiles = listOf(
|
directPlayProfiles =
|
||||||
|
listOf(
|
||||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||||
),
|
),
|
||||||
transcodingProfiles = listOf(
|
transcodingProfiles =
|
||||||
|
listOf(
|
||||||
TranscodingProfile(
|
TranscodingProfile(
|
||||||
container = container,
|
container = container,
|
||||||
context = context,
|
context = context,
|
||||||
|
@ -628,20 +692,22 @@ class JellyfinRepositoryImpl(
|
||||||
audioCodec = "aac,ac3,eac3",
|
audioCodec = "aac,ac3,eac3",
|
||||||
videoCodec = "hevc,h264",
|
videoCodec = "hevc,h264",
|
||||||
type = DlnaProfileType.VIDEO,
|
type = DlnaProfileType.VIDEO,
|
||||||
conditions = listOf(
|
conditions =
|
||||||
|
listOf(
|
||||||
ProfileCondition(
|
ProfileCondition(
|
||||||
condition = ProfileConditionType.LESS_THAN_EQUAL,
|
condition = ProfileConditionType.LESS_THAN_EQUAL,
|
||||||
property = ProfileConditionValue.VIDEO_BITRATE,
|
property = ProfileConditionValue.VIDEO_BITRATE,
|
||||||
value = "8000000",
|
value = "8000000",
|
||||||
isRequired = true,
|
isRequired = true,
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
copyTimestamps = true,
|
copyTimestamps = true,
|
||||||
enableSubtitlesInManifest = true,
|
enableSubtitlesInManifest = true,
|
||||||
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
|
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitleProfiles = listOf(
|
subtitleProfiles =
|
||||||
|
listOf(
|
||||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
|
@ -649,16 +715,21 @@ class JellyfinRepositoryImpl(
|
||||||
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
|
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL)
|
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return deviceProfile.deviceProfile!!
|
return deviceProfile.deviceProfile!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getPostedPlaybackInfo(
|
||||||
override suspend fun getPostedPlaybackInfo(itemId: UUID ,enableDirectStream: Boolean ,deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse> {
|
itemId: UUID,
|
||||||
val playbackInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
enableDirectStream: Boolean,
|
||||||
|
deviceProfile: DeviceProfile,
|
||||||
|
maxBitrate: Int,
|
||||||
|
): Response<PlaybackInfoResponse> {
|
||||||
|
val playbackInfo =
|
||||||
|
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
||||||
itemId = itemId,
|
itemId = itemId,
|
||||||
PlaybackInfoDto(
|
PlaybackInfoDto(
|
||||||
userId = jellyfinApi.userId!!,
|
userId = jellyfinApi.userId!!,
|
||||||
|
@ -666,37 +737,56 @@ class JellyfinRepositoryImpl(
|
||||||
enableDirectPlay = false,
|
enableDirectPlay = false,
|
||||||
enableDirectStream = enableDirectStream,
|
enableDirectStream = enableDirectStream,
|
||||||
autoOpenLiveStream = true,
|
autoOpenLiveStream = true,
|
||||||
deviceProfile = deviceProfile,
|
deviceProfile = buildDeviceProfile(maxBitrate, "ts", EncodingContext.STREAMING),
|
||||||
allowAudioStreamCopy = true,
|
allowAudioStreamCopy = true,
|
||||||
allowVideoStreamCopy = true,
|
allowVideoStreamCopy = true,
|
||||||
maxStreamingBitrate = maxBitrate,
|
maxStreamingBitrate = maxBitrate,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
return playbackInfo
|
return playbackInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String {
|
override suspend fun getVideoStreambyContainerUrl(
|
||||||
val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl(
|
itemId: UUID,
|
||||||
|
deviceId: String,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String,
|
||||||
|
videoBitrate: Int,
|
||||||
|
maxHeight: Int,
|
||||||
|
container: String,
|
||||||
|
): String {
|
||||||
|
val url =
|
||||||
|
jellyfinApi.videosApi.getVideoStreamByContainerUrl(
|
||||||
itemId,
|
itemId,
|
||||||
static = false,
|
static = false,
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
mediaSourceId = mediaSourceId,
|
mediaSourceId = mediaSourceId,
|
||||||
playSessionId = playSessionId,
|
playSessionId = playSessionId,
|
||||||
videoBitRate = videoBitrate,
|
videoBitRate = videoBitrate,
|
||||||
audioBitRate = 384000,
|
maxHeight = maxHeight,
|
||||||
|
audioBitRate = 128000,
|
||||||
videoCodec = "hevc",
|
videoCodec = "hevc",
|
||||||
audioCodec = "aac,ac3,eac3",
|
audioCodec = "aac",
|
||||||
container = container,
|
container = container,
|
||||||
startTimeTicks = 0,
|
startTimeTicks = 0,
|
||||||
copyTimestamps = true,
|
copyTimestamps = true,
|
||||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL
|
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||||
)
|
)
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String {
|
override suspend fun getTranscodedVideoStream(
|
||||||
val isAuto = videoBitrate == 12000000
|
itemId: UUID,
|
||||||
val url = if (!isAuto) {
|
deviceId: String,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String,
|
||||||
|
videoBitrate: Int,
|
||||||
|
): String {
|
||||||
|
val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto)
|
||||||
|
val url: String
|
||||||
|
try {
|
||||||
|
url =
|
||||||
|
if (!isAuto) {
|
||||||
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
|
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
|
||||||
itemId,
|
itemId,
|
||||||
static = false,
|
static = false,
|
||||||
|
@ -705,9 +795,9 @@ class JellyfinRepositoryImpl(
|
||||||
playSessionId = playSessionId,
|
playSessionId = playSessionId,
|
||||||
videoBitRate = videoBitrate,
|
videoBitRate = videoBitrate,
|
||||||
enableAdaptiveBitrateStreaming = false,
|
enableAdaptiveBitrateStreaming = false,
|
||||||
audioBitRate = 384000,
|
audioBitRate = 128000,
|
||||||
videoCodec = "hevc",
|
videoCodec = "hevc",
|
||||||
audioCodec = "aac,ac3,eac3",
|
audioCodec = "aac",
|
||||||
startTimeTicks = 0,
|
startTimeTicks = 0,
|
||||||
copyTimestamps = true,
|
copyTimestamps = true,
|
||||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||||
|
@ -724,7 +814,7 @@ class JellyfinRepositoryImpl(
|
||||||
playSessionId = playSessionId,
|
playSessionId = playSessionId,
|
||||||
enableAdaptiveBitrateStreaming = true,
|
enableAdaptiveBitrateStreaming = true,
|
||||||
videoCodec = "hevc",
|
videoCodec = "hevc",
|
||||||
audioCodec = "aac,ac3,eac3",
|
audioCodec = "aac",
|
||||||
startTimeTicks = 0,
|
startTimeTicks = 0,
|
||||||
copyTimestamps = true,
|
copyTimestamps = true,
|
||||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||||
|
@ -733,23 +823,20 @@ class JellyfinRepositoryImpl(
|
||||||
transcodeReasons = "ContainerBitrateExceedsLimit",
|
transcodeReasons = "ContainerBitrateExceedsLimit",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getDeviceId(): String = jellyfinApi.api.deviceInfo.id
|
||||||
override suspend fun getDeviceId(): String {
|
|
||||||
val devices = jellyfinApi.devicesApi.getDevices(getUserId())
|
|
||||||
return devices.content.items?.firstOrNull()?.id!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun stopEncodingProcess(playSessionId: String) {
|
override suspend fun stopEncodingProcess(playSessionId: String) {
|
||||||
val deviceId = getDeviceId()
|
val deviceId = getDeviceId()
|
||||||
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
|
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
playSessionId = playSessionId
|
playSessionId = playSessionId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.nomadics9.ananas.repository
|
package com.nomadics9.ananas.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.devicelock.DeviceId
|
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import com.nomadics9.ananas.AppPreferences
|
import com.nomadics9.ananas.AppPreferences
|
||||||
import com.nomadics9.ananas.api.JellyfinApi
|
import com.nomadics9.ananas.api.JellyfinApi
|
||||||
|
@ -27,7 +26,6 @@ import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.Response
|
import org.jellyfin.sdk.api.client.Response
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.DeviceInfo
|
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import org.jellyfin.sdk.model.api.EncodingContext
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
@ -44,14 +42,9 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
private val database: ServerDatabaseDao,
|
private val database: ServerDatabaseDao,
|
||||||
private val appPreferences: AppPreferences,
|
private val appPreferences: AppPreferences,
|
||||||
) : JellyfinRepository {
|
) : JellyfinRepository {
|
||||||
|
override suspend fun getPublicSystemInfo(): PublicSystemInfo = throw Exception("System info not available in offline mode")
|
||||||
|
|
||||||
override suspend fun getPublicSystemInfo(): PublicSystemInfo {
|
override suspend fun getUserViews(): List<BaseItemDto> = emptyList()
|
||||||
throw Exception("System info not available in offline mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getUserViews(): List<BaseItemDto> {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getItem(itemId: UUID): BaseItemDto {
|
override suspend fun getItem(itemId: UUID): BaseItemDto {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
|
@ -115,36 +108,59 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> {
|
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
|
||||||
return withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val movies = database.searchMovies(appPreferences.currentServer!!, searchQuery).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }
|
val movies =
|
||||||
val shows = database.searchShows(appPreferences.currentServer!!, searchQuery).map { it.toFindroidShow(database, jellyfinApi.userId!!) }
|
database
|
||||||
val episodes = database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }
|
.searchMovies(
|
||||||
|
appPreferences.currentServer!!,
|
||||||
|
searchQuery,
|
||||||
|
).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }
|
||||||
|
val shows =
|
||||||
|
database
|
||||||
|
.searchShows(
|
||||||
|
appPreferences.currentServer!!,
|
||||||
|
searchQuery,
|
||||||
|
).map { it.toFindroidShow(database, jellyfinApi.userId!!) }
|
||||||
|
val episodes =
|
||||||
|
database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map {
|
||||||
|
it.toFindroidEpisode(database, jellyfinApi.userId!!)
|
||||||
|
}
|
||||||
movies + shows + episodes
|
movies + shows + episodes
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getResumeItems(): List<FindroidItem> {
|
override suspend fun getResumeItems(): List<FindroidItem> =
|
||||||
return withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val movies = database.getMoviesByServerId(appPreferences.currentServer!!).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 }
|
val movies =
|
||||||
val episodes = database.getEpisodesByServerId(appPreferences.currentServer!!).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 }
|
database
|
||||||
|
.getMoviesByServerId(appPreferences.currentServer!!)
|
||||||
|
.map {
|
||||||
|
it.toFindroidMovie(database, jellyfinApi.userId!!)
|
||||||
|
}.filter { it.playbackPositionTicks > 0 }
|
||||||
|
val episodes =
|
||||||
|
database
|
||||||
|
.getEpisodesByServerId(appPreferences.currentServer!!)
|
||||||
|
.map {
|
||||||
|
it.toFindroidEpisode(database, jellyfinApi.userId!!)
|
||||||
|
}.filter { it.playbackPositionTicks > 0 }
|
||||||
movies + episodes
|
movies + episodes
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
|
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> = emptyList()
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> =
|
override suspend fun getSeasons(
|
||||||
|
seriesId: UUID,
|
||||||
|
offline: Boolean,
|
||||||
|
): List<FindroidSeason> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) }
|
database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> {
|
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
|
||||||
return withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val result = mutableListOf<FindroidEpisode>()
|
val result = mutableListOf<FindroidEpisode>()
|
||||||
val shows = database.getShowsByServerId(appPreferences.currentServer!!).filter {
|
val shows =
|
||||||
|
database.getShowsByServerId(appPreferences.currentServer!!).filter {
|
||||||
if (seriesId != null) it.id == seriesId else true
|
if (seriesId != null) it.id == seriesId else true
|
||||||
}
|
}
|
||||||
for (show in shows) {
|
for (show in shows) {
|
||||||
|
@ -158,7 +174,6 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
}
|
}
|
||||||
result.filter { it.playbackPositionTicks == 0L }
|
result.filter { it.playbackPositionTicks == 0L }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getEpisodes(
|
override suspend fun getEpisodes(
|
||||||
seriesId: UUID,
|
seriesId: UUID,
|
||||||
|
@ -174,12 +189,19 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> =
|
override suspend fun getMediaSources(
|
||||||
|
itemId: UUID,
|
||||||
|
includePath: Boolean,
|
||||||
|
): List<FindroidSource> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
database.getSources(itemId).map { it.toFindroidSource(database) }
|
database.getSources(itemId).map { it.toFindroidSource(database) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String {
|
override suspend fun getStreamUrl(
|
||||||
|
itemId: UUID,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String?,
|
||||||
|
): String {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +210,11 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
database.getSegments(itemId)?.toFindroidSegments()
|
database.getSegments(itemId)?.toFindroidSegments()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? =
|
override suspend fun getTrickplayData(
|
||||||
|
itemId: UUID,
|
||||||
|
width: Int,
|
||||||
|
index: Int,
|
||||||
|
): ByteArray? =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null
|
val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null
|
||||||
|
@ -202,7 +228,11 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
|
|
||||||
override suspend fun postPlaybackStart(itemId: UUID) {}
|
override suspend fun postPlaybackStart(itemId: UUID) {}
|
||||||
|
|
||||||
override suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int) {
|
override suspend fun postPlaybackStop(
|
||||||
|
itemId: UUID,
|
||||||
|
positionTicks: Long,
|
||||||
|
playedPercentage: Int,
|
||||||
|
) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
when {
|
when {
|
||||||
playedPercentage < 10 -> {
|
playedPercentage < 10 -> {
|
||||||
|
@ -262,35 +292,31 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getBaseUrl(): String {
|
override fun getBaseUrl(): String = ""
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun updateDeviceName(name: String) {
|
override suspend fun updateDeviceName(name: String) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUserConfiguration(): UserConfiguration? {
|
override suspend fun getUserConfiguration(): UserConfiguration? = null
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDownloads(): List<FindroidItem> =
|
override suspend fun getDownloads(): List<FindroidItem> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val items = mutableListOf<FindroidItem>()
|
val items = mutableListOf<FindroidItem>()
|
||||||
items.addAll(
|
items.addAll(
|
||||||
database.getMoviesByServerId(appPreferences.currentServer!!)
|
database
|
||||||
|
.getMoviesByServerId(appPreferences.currentServer!!)
|
||||||
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
|
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
|
||||||
)
|
)
|
||||||
items.addAll(
|
items.addAll(
|
||||||
database.getShowsByServerId(appPreferences.currentServer!!)
|
database
|
||||||
|
.getShowsByServerId(appPreferences.currentServer!!)
|
||||||
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
|
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
|
||||||
)
|
)
|
||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUserId(): UUID {
|
override fun getUserId(): UUID = jellyfinApi.userId!!
|
||||||
return jellyfinApi.userId!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getDeviceId(): String {
|
override suspend fun getDeviceId(): String {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
|
@ -299,7 +325,7 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
override suspend fun buildDeviceProfile(
|
override suspend fun buildDeviceProfile(
|
||||||
maxBitrate: Int,
|
maxBitrate: Int,
|
||||||
container: String,
|
container: String,
|
||||||
context: EncodingContext
|
context: EncodingContext,
|
||||||
): DeviceProfile {
|
): DeviceProfile {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
@ -310,7 +336,8 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
mediaSourceId: String,
|
mediaSourceId: String,
|
||||||
playSessionId: String,
|
playSessionId: String,
|
||||||
videoBitrate: Int,
|
videoBitrate: Int,
|
||||||
container: String
|
maxHeight: Int,
|
||||||
|
container: String,
|
||||||
): String {
|
): String {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
@ -320,7 +347,7 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
deviceId: String,
|
deviceId: String,
|
||||||
mediaSourceId: String,
|
mediaSourceId: String,
|
||||||
playSessionId: String,
|
playSessionId: String,
|
||||||
videoBitrate: Int
|
videoBitrate: Int,
|
||||||
): String {
|
): String {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
@ -329,7 +356,7 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
enableDirectStream: Boolean,
|
enableDirectStream: Boolean,
|
||||||
deviceProfile: DeviceProfile,
|
deviceProfile: DeviceProfile,
|
||||||
maxBitrate: Int
|
maxBitrate: Int,
|
||||||
): Response<PlaybackInfoResponse> {
|
): Response<PlaybackInfoResponse> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
@ -337,8 +364,4 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
override suspend fun stopEncodingProcess(playSessionId: String) {
|
override suspend fun stopEncodingProcess(playSessionId: String) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import com.nomadics9.ananas.models.FindroidSegment
|
||||||
import com.nomadics9.ananas.models.PlayerChapter
|
import com.nomadics9.ananas.models.PlayerChapter
|
||||||
import com.nomadics9.ananas.models.PlayerItem
|
import com.nomadics9.ananas.models.PlayerItem
|
||||||
import com.nomadics9.ananas.models.Trickplay
|
import com.nomadics9.ananas.models.Trickplay
|
||||||
|
import com.nomadics9.ananas.models.VideoQuality
|
||||||
import com.nomadics9.ananas.mpv.MPVPlayer
|
import com.nomadics9.ananas.mpv.MPVPlayer
|
||||||
import com.nomadics9.ananas.player.video.R
|
import com.nomadics9.ananas.player.video.R
|
||||||
import com.nomadics9.ananas.repository.JellyfinRepository
|
import com.nomadics9.ananas.repository.JellyfinRepository
|
||||||
|
@ -58,11 +59,13 @@ constructor(
|
||||||
private val appPreferences: AppPreferences,
|
private val appPreferences: AppPreferences,
|
||||||
private val jellyfinApi: JellyfinApi,
|
private val jellyfinApi: JellyfinApi,
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
) : ViewModel(), Player.Listener {
|
) : ViewModel(),
|
||||||
|
Player.Listener {
|
||||||
val player: Player
|
val player: Player
|
||||||
private var originalHeight: Int = 0
|
private var originalHeight: Int = 0
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(
|
private val _uiState =
|
||||||
|
MutableStateFlow(
|
||||||
UiState(
|
UiState(
|
||||||
currentItemTitle = "",
|
currentItemTitle = "",
|
||||||
currentSegment = null,
|
currentSegment = null,
|
||||||
|
@ -101,11 +104,14 @@ constructor(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (appPreferences.playerMpv) {
|
if (appPreferences.playerMpv) {
|
||||||
val trackSelectionParameters = TrackSelectionParameters.Builder(application)
|
val trackSelectionParameters =
|
||||||
|
TrackSelectionParameters
|
||||||
|
.Builder(application)
|
||||||
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
|
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
|
||||||
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage)
|
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage)
|
||||||
.build()
|
.build()
|
||||||
player = MPVPlayer(
|
player =
|
||||||
|
MPVPlayer(
|
||||||
context = application,
|
context = application,
|
||||||
requestAudioFocus = true,
|
requestAudioFocus = true,
|
||||||
trackSelectionParameters = trackSelectionParameters,
|
trackSelectionParameters = trackSelectionParameters,
|
||||||
|
@ -121,30 +127,31 @@ constructor(
|
||||||
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON,
|
DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON,
|
||||||
)
|
)
|
||||||
trackSelector.setParameters(
|
trackSelector.setParameters(
|
||||||
trackSelector.buildUponParameters()
|
trackSelector
|
||||||
|
.buildUponParameters()
|
||||||
.setTunnelingEnabled(true)
|
.setTunnelingEnabled(true)
|
||||||
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
|
.setPreferredAudioLanguage(appPreferences.preferredAudioLanguage)
|
||||||
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage),
|
.setPreferredTextLanguage(appPreferences.preferredSubtitleLanguage),
|
||||||
)
|
)
|
||||||
player = ExoPlayer.Builder(application, renderersFactory)
|
player =
|
||||||
|
ExoPlayer
|
||||||
|
.Builder(application, renderersFactory)
|
||||||
.setTrackSelector(trackSelector)
|
.setTrackSelector(trackSelector)
|
||||||
.setAudioAttributes(
|
.setAudioAttributes(
|
||||||
AudioAttributes.Builder()
|
AudioAttributes
|
||||||
|
.Builder()
|
||||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||||
.setUsage(C.USAGE_MEDIA)
|
.setUsage(C.USAGE_MEDIA)
|
||||||
.build(),
|
.build(),
|
||||||
/* handleAudioFocus = */
|
// handleAudioFocus =
|
||||||
true,
|
true,
|
||||||
)
|
).setSeekBackIncrementMs(appPreferences.playerSeekBackIncrement)
|
||||||
.setSeekBackIncrementMs(appPreferences.playerSeekBackIncrement)
|
|
||||||
.setSeekForwardIncrementMs(appPreferences.playerSeekForwardIncrement)
|
.setSeekForwardIncrementMs(appPreferences.playerSeekForwardIncrement)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initializePlayer(
|
fun initializePlayer(items: Array<PlayerItem>) {
|
||||||
items: Array<PlayerItem>,
|
|
||||||
) {
|
|
||||||
this.items = items
|
this.items = items
|
||||||
player.addListener(this)
|
player.addListener(this)
|
||||||
|
|
||||||
|
@ -153,8 +160,10 @@ constructor(
|
||||||
try {
|
try {
|
||||||
for (item in items) {
|
for (item in items) {
|
||||||
val streamUrl = item.mediaSourceUri
|
val streamUrl = item.mediaSourceUri
|
||||||
val mediaSubtitles = item.externalSubtitles.map { externalSubtitle ->
|
val mediaSubtitles =
|
||||||
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
|
item.externalSubtitles.map { externalSubtitle ->
|
||||||
|
MediaItem.SubtitleConfiguration
|
||||||
|
.Builder(externalSubtitle.uri)
|
||||||
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
|
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
|
||||||
.setMimeType(externalSubtitle.mimeType)
|
.setMimeType(externalSubtitle.mimeType)
|
||||||
.setLanguage(externalSubtitle.language)
|
.setLanguage(externalSubtitle.language)
|
||||||
|
@ -170,24 +179,25 @@ constructor(
|
||||||
|
|
||||||
Timber.d("Stream url: $streamUrl")
|
Timber.d("Stream url: $streamUrl")
|
||||||
val mediaItem =
|
val mediaItem =
|
||||||
MediaItem.Builder()
|
MediaItem
|
||||||
|
.Builder()
|
||||||
.setMediaId(item.itemId.toString())
|
.setMediaId(item.itemId.toString())
|
||||||
.setUri(streamUrl)
|
.setUri(streamUrl)
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata
|
||||||
|
.Builder()
|
||||||
.setTitle(item.name)
|
.setTitle(item.name)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
).setSubtitleConfigurations(mediaSubtitles)
|
||||||
.setSubtitleConfigurations(mediaSubtitles)
|
|
||||||
.build()
|
.build()
|
||||||
mediaItems.add(mediaItem)
|
mediaItems.add(mediaItem)
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
val startPosition = if (playbackPosition == 0L) {
|
val startPosition =
|
||||||
|
if (playbackPosition == 0L) {
|
||||||
items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET
|
items.getOrNull(currentMediaItemIndex)?.playbackPosition ?: C.TIME_UNSET
|
||||||
} else {
|
} else {
|
||||||
playbackPosition
|
playbackPosition
|
||||||
|
@ -231,7 +241,8 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pollPosition(player: Player) {
|
private fun pollPosition(player: Player) {
|
||||||
val playbackProgressRunnable = object : Runnable {
|
val playbackProgressRunnable =
|
||||||
|
object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
savedStateHandle["position"] = player.currentPosition
|
savedStateHandle["position"] = player.currentPosition
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
@ -251,7 +262,8 @@ constructor(
|
||||||
handler.postDelayed(this, 5000L)
|
handler.postDelayed(this, 5000L)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val segmentCheckRunnable = object : Runnable {
|
val segmentCheckRunnable =
|
||||||
|
object : Runnable {
|
||||||
override fun run() {
|
override fun run() {
|
||||||
val currentMediaItem = player.currentMediaItem
|
val currentMediaItem = player.currentMediaItem
|
||||||
if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) {
|
if (currentMediaItem != null && currentMediaItem.mediaId.isNotEmpty()) {
|
||||||
|
@ -276,12 +288,16 @@ constructor(
|
||||||
if (segments.isNotEmpty()) handler.post(segmentCheckRunnable)
|
if (segments.isNotEmpty()) handler.post(segmentCheckRunnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(
|
||||||
|
mediaItem: MediaItem?,
|
||||||
|
reason: Int,
|
||||||
|
) {
|
||||||
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
|
Timber.d("Playing MediaItem: ${mediaItem?.mediaId}")
|
||||||
savedStateHandle["mediaItemIndex"] = player.currentMediaItemIndex
|
savedStateHandle["mediaItemIndex"] = player.currentMediaItemIndex
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
items.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
|
items
|
||||||
|
.first { it.itemId.toString() == player.currentMediaItem?.mediaId }
|
||||||
.let { item ->
|
.let { item ->
|
||||||
val itemTitle =
|
val itemTitle =
|
||||||
if (item.parentIndexNumber != null && item.indexNumber != null) {
|
if (item.parentIndexNumber != null && item.indexNumber != null) {
|
||||||
|
@ -345,24 +361,30 @@ constructor(
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun switchToTrack(trackType: @C.TrackType Int, index: Int) {
|
fun switchToTrack(
|
||||||
|
trackType: @C.TrackType Int,
|
||||||
|
index: Int,
|
||||||
|
) {
|
||||||
// Index -1 equals disable track
|
// Index -1 equals disable track
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
player.trackSelectionParameters = player.trackSelectionParameters
|
player.trackSelectionParameters =
|
||||||
|
player.trackSelectionParameters
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.clearOverridesOfType(trackType)
|
.clearOverridesOfType(trackType)
|
||||||
.setTrackTypeDisabled(trackType, true)
|
.setTrackTypeDisabled(trackType, true)
|
||||||
.build()
|
.build()
|
||||||
} else {
|
} else {
|
||||||
player.trackSelectionParameters = player.trackSelectionParameters
|
player.trackSelectionParameters =
|
||||||
|
player.trackSelectionParameters
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.setOverrideForType(
|
.setOverrideForType(
|
||||||
TrackSelectionOverride(
|
TrackSelectionOverride(
|
||||||
player.currentTracks.groups.filter { it.type == trackType && it.isSupported }[index].mediaTrackGroup,
|
player.currentTracks.groups
|
||||||
0
|
.filter { it.type == trackType && it.isSupported }[index]
|
||||||
|
.mediaTrackGroup,
|
||||||
|
0,
|
||||||
),
|
),
|
||||||
)
|
).setTrackTypeDisabled(trackType, false)
|
||||||
.setTrackTypeDisabled(trackType, false)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -377,14 +399,17 @@ constructor(
|
||||||
Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
|
Timber.d("Trickplay Resolution: ${trickplayInfo.width}")
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val maxIndex = ceil(
|
val maxIndex =
|
||||||
trickplayInfo.thumbnailCount.toDouble()
|
ceil(
|
||||||
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)
|
trickplayInfo.thumbnailCount
|
||||||
|
.toDouble()
|
||||||
|
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
|
||||||
).toInt()
|
).toInt()
|
||||||
val bitmaps = mutableListOf<Bitmap>()
|
val bitmaps = mutableListOf<Bitmap>()
|
||||||
|
|
||||||
for (i in 0..maxIndex) {
|
for (i in 0..maxIndex) {
|
||||||
jellyfinRepository.getTrickplayData(
|
jellyfinRepository
|
||||||
|
.getTrickplayData(
|
||||||
item.itemId,
|
item.itemId,
|
||||||
trickplayInfo.width,
|
trickplayInfo.width,
|
||||||
i,
|
i,
|
||||||
|
@ -392,12 +417,13 @@ constructor(
|
||||||
val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
val fullBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size)
|
||||||
for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) {
|
for (offsetY in 0..<trickplayInfo.height * trickplayInfo.tileHeight step trickplayInfo.height) {
|
||||||
for (offsetX in 0..<trickplayInfo.width * trickplayInfo.tileWidth step trickplayInfo.width) {
|
for (offsetX in 0..<trickplayInfo.width * trickplayInfo.tileWidth step trickplayInfo.width) {
|
||||||
val bitmap = Bitmap.createBitmap(
|
val bitmap =
|
||||||
|
Bitmap.createBitmap(
|
||||||
fullBitmap,
|
fullBitmap,
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
trickplayInfo.width,
|
trickplayInfo.width,
|
||||||
trickplayInfo.height
|
trickplayInfo.height,
|
||||||
)
|
)
|
||||||
bitmaps.add(bitmap)
|
bitmaps.add(bitmap)
|
||||||
}
|
}
|
||||||
|
@ -406,10 +432,11 @@ constructor(
|
||||||
}
|
}
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
currentTrickplay = Trickplay(
|
currentTrickplay =
|
||||||
|
Trickplay(
|
||||||
trickplayInfo.interval,
|
trickplayInfo.interval,
|
||||||
bitmaps
|
bitmaps,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -420,9 +447,7 @@ constructor(
|
||||||
*
|
*
|
||||||
* @return list of [PlayerChapter]
|
* @return list of [PlayerChapter]
|
||||||
*/
|
*/
|
||||||
private fun getChapters(): List<PlayerChapter>? {
|
private fun getChapters(): List<PlayerChapter>? = uiState.value.currentChapters
|
||||||
return uiState.value.currentChapters
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the index of the current chapter
|
* Get the index of the current chapter
|
||||||
|
@ -473,8 +498,8 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 }
|
fun isFirstChapter(): Boolean? = getChapters()?.let { getCurrentChapterIndex() == 0 }
|
||||||
fun isLastChapter(): Boolean? =
|
|
||||||
getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 }
|
fun isLastChapter(): Boolean? = getChapters()?.let { chapters -> getCurrentChapterIndex() == chapters.size - 1 }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek to chapter
|
* Seek to chapter
|
||||||
|
@ -482,20 +507,17 @@ constructor(
|
||||||
* @param chapterIndex the index of the chapter to seek to
|
* @param chapterIndex the index of the chapter to seek to
|
||||||
* @return the [PlayerChapter] which has been sought to
|
* @return the [PlayerChapter] which has been sought to
|
||||||
*/
|
*/
|
||||||
private fun seekToChapter(chapterIndex: Int): PlayerChapter? {
|
private fun seekToChapter(chapterIndex: Int): PlayerChapter? =
|
||||||
return getChapters()?.getOrNull(chapterIndex)?.also { chapter ->
|
getChapters()?.getOrNull(chapterIndex)?.also { chapter ->
|
||||||
player.seekTo(chapter.startPosition)
|
player.seekTo(chapter.startPosition)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek to the next chapter
|
* Seek to the next chapter
|
||||||
*
|
*
|
||||||
* @return the [PlayerChapter] which has been sought to
|
* @return the [PlayerChapter] which has been sought to
|
||||||
*/
|
*/
|
||||||
fun seekToNextChapter(): PlayerChapter? {
|
fun seekToNextChapter(): PlayerChapter? = getNextChapterIndex()?.let { seekToChapter(it) }
|
||||||
return getNextChapterIndex()?.let { seekToChapter(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek to the previous chapter Will seek to start of current chapter if
|
* Seek to the previous chapter Will seek to start of current chapter if
|
||||||
|
@ -503,55 +525,54 @@ constructor(
|
||||||
*
|
*
|
||||||
* @return the [PlayerChapter] which has been sought to
|
* @return the [PlayerChapter] which has been sought to
|
||||||
*/
|
*/
|
||||||
fun seekToPreviousChapter(): PlayerChapter? {
|
fun seekToPreviousChapter(): PlayerChapter? = getPreviousChapterIndex()?.let { seekToChapter(it) }
|
||||||
return getPreviousChapterIndex()?.let { seekToChapter(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
super.onIsPlayingChanged(isPlaying)
|
super.onIsPlayingChanged(isPlaying)
|
||||||
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
|
eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTranscodeResolutions(preferredQuality: String): Int {
|
|
||||||
return when (preferredQuality) {
|
|
||||||
"1080p" -> 1080
|
|
||||||
"720p - 2Mbps" -> 720
|
|
||||||
"480p - 1Mbps" -> 480
|
|
||||||
"360p - 800kbps" -> 360
|
|
||||||
"Auto" -> 1
|
|
||||||
else -> 1080
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun changeVideoQuality(quality: String) {
|
fun changeVideoQuality(quality: String) {
|
||||||
val mediaId = player.currentMediaItem?.mediaId ?: return
|
val mediaId = player.currentMediaItem?.mediaId ?: return
|
||||||
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
|
val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return
|
||||||
val currentPosition = player.currentPosition
|
val currentPosition = player.currentPosition
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val videoQuality = VideoQuality.fromString(quality)!!
|
||||||
try {
|
try {
|
||||||
val transcodingResolution = getTranscodeResolutions(quality)
|
val deviceProfile =
|
||||||
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
|
jellyfinRepository.buildDeviceProfile(
|
||||||
transcodingResolution
|
VideoQuality.getBitrate(videoQuality),
|
||||||
|
"ts",
|
||||||
|
EncodingContext.STREAMING,
|
||||||
|
)
|
||||||
|
|
||||||
|
val playbackInfo =
|
||||||
|
jellyfinRepository.getPostedPlaybackInfo(
|
||||||
|
currentItem.itemId,
|
||||||
|
true,
|
||||||
|
deviceProfile,
|
||||||
|
VideoQuality.getBitrate(videoQuality),
|
||||||
)
|
)
|
||||||
val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "mkv", EncodingContext.STREAMING)
|
|
||||||
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,videoBitRate)
|
|
||||||
val playSessionId = playbackInfo.content.playSessionId
|
val playSessionId = playbackInfo.content.playSessionId
|
||||||
if (playSessionId != null) {
|
if (playSessionId != null) {
|
||||||
jellyfinRepository.stopEncodingProcess(playSessionId)
|
jellyfinRepository.stopEncodingProcess(playSessionId)
|
||||||
}
|
}
|
||||||
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
|
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
|
||||||
|
|
||||||
|
val externalSubtitles =
|
||||||
val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
|
currentItem.externalSubtitles.map { externalSubtitle ->
|
||||||
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
|
MediaItem.SubtitleConfiguration
|
||||||
|
.Builder(externalSubtitle.uri)
|
||||||
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
|
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
|
||||||
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
|
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
|
||||||
.setMimeType(externalSubtitle.mimeType)
|
.setMimeType(externalSubtitle.mimeType)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams
|
val embeddedSubtitles =
|
||||||
|
mediaSources[currentMediaItemIndex]
|
||||||
|
.mediaStreams
|
||||||
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
|
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
|
||||||
.map { mediaStream ->
|
.map { mediaStream ->
|
||||||
val test = mediaStream.codec
|
val test = mediaStream.codec
|
||||||
|
@ -559,8 +580,10 @@ constructor(
|
||||||
var deliveryUrl = mediaStream.path
|
var deliveryUrl = mediaStream.path
|
||||||
Timber.d("Deliverurl: %s", deliveryUrl)
|
Timber.d("Deliverurl: %s", deliveryUrl)
|
||||||
if (mediaStream.codec == "webvtt") {
|
if (mediaStream.codec == "webvtt") {
|
||||||
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")}
|
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")
|
||||||
MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl))
|
}
|
||||||
|
MediaItem.SubtitleConfiguration
|
||||||
|
.Builder(Uri.parse(deliveryUrl))
|
||||||
.setMimeType(
|
.setMimeType(
|
||||||
when (mediaStream.codec) {
|
when (mediaStream.codec) {
|
||||||
"subrip" -> MimeTypes.APPLICATION_SUBRIP
|
"subrip" -> MimeTypes.APPLICATION_SUBRIP
|
||||||
|
@ -575,23 +598,29 @@ constructor(
|
||||||
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
|
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
|
||||||
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
|
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
|
||||||
else -> MimeTypes.TEXT_UNKNOWN
|
else -> MimeTypes.TEXT_UNKNOWN
|
||||||
}
|
},
|
||||||
)
|
).setLanguage(mediaStream.language.ifBlank { "Unknown" })
|
||||||
.setLanguage(mediaStream.language.ifBlank { "Unknown" })
|
|
||||||
.setLabel("Embedded")
|
.setLabel("Embedded")
|
||||||
.build()
|
.build()
|
||||||
}
|
}.toMutableList()
|
||||||
.toMutableList()
|
|
||||||
|
|
||||||
|
|
||||||
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) }
|
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) }
|
||||||
|
|
||||||
val url = if (transcodingResolution == 1080){
|
val url =
|
||||||
|
if (VideoQuality.getQualityString(videoQuality) == "Original") {
|
||||||
jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId)
|
jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId)
|
||||||
} else {
|
} else {
|
||||||
val mediaSourceId = mediaSources[currentMediaItemIndex].id
|
val mediaSourceId = mediaSources[currentMediaItemIndex].id
|
||||||
val deviceId = jellyfinRepository.getDeviceId()
|
val deviceId = jellyfinApi.api.deviceInfo.id
|
||||||
val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, videoBitRate)
|
Timber.d("deviceid = %s", deviceId)
|
||||||
|
val url =
|
||||||
|
jellyfinRepository.getTranscodedVideoStream(
|
||||||
|
currentItem.itemId,
|
||||||
|
deviceId,
|
||||||
|
mediaSourceId,
|
||||||
|
playSessionId!!,
|
||||||
|
VideoQuality.getBitrate(videoQuality),
|
||||||
|
)
|
||||||
val uriBuilder = url.toUri().buildUpon()
|
val uriBuilder = url.toUri().buildUpon()
|
||||||
val apiKey = jellyfinApi.api.accessToken
|
val apiKey = jellyfinApi.api.accessToken
|
||||||
uriBuilder.appendQueryParameter("api_key", apiKey)
|
uriBuilder.appendQueryParameter("api_key", apiKey)
|
||||||
|
@ -599,20 +628,20 @@ constructor(
|
||||||
newUri.toString()
|
newUri.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Timber.e("URI IS %s", url)
|
Timber.e("URI IS %s", url)
|
||||||
val mediaItemBuilder = MediaItem.Builder()
|
val mediaItemBuilder =
|
||||||
|
MediaItem
|
||||||
|
.Builder()
|
||||||
.setMediaId(currentItem.itemId.toString())
|
.setMediaId(currentItem.itemId.toString())
|
||||||
.setUri(url)
|
.setUri(url)
|
||||||
.setSubtitleConfigurations(allSubtitles)
|
.setSubtitleConfigurations(allSubtitles)
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
MediaMetadata.Builder()
|
MediaMetadata
|
||||||
|
.Builder()
|
||||||
.setTitle(currentItem.name)
|
.setTitle(currentItem.name)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
player.pause()
|
player.pause()
|
||||||
player.setMediaItem(mediaItemBuilder.build())
|
player.setMediaItem(mediaItemBuilder.build())
|
||||||
player.prepare()
|
player.prepare()
|
||||||
|
@ -620,10 +649,12 @@ constructor(
|
||||||
playWhenReady = true
|
playWhenReady = true
|
||||||
player.play()
|
player.play()
|
||||||
|
|
||||||
val originalHeight = mediaSources[currentMediaItemIndex].mediaStreams
|
val originalHeight =
|
||||||
|
mediaSources[currentMediaItemIndex]
|
||||||
|
.mediaStreams
|
||||||
.filter { it.type == MediaStreamType.VIDEO }
|
.filter { it.type == MediaStreamType.VIDEO }
|
||||||
.map {mediaStream -> mediaStream.height}.first() ?: 1080
|
.map { mediaStream -> mediaStream.height }
|
||||||
|
.first() ?: 1080
|
||||||
|
|
||||||
// Store the original height
|
// Store the original height
|
||||||
this@PlayerActivityViewModel.originalHeight = originalHeight
|
this@PlayerActivityViewModel.originalHeight = originalHeight
|
||||||
|
@ -635,13 +666,13 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOriginalHeight(): Int {
|
fun getOriginalHeight(): Int = originalHeight
|
||||||
return originalHeight
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
sealed interface PlayerEvents {
|
sealed interface PlayerEvents {
|
||||||
data object NavigateBack : PlayerEvents
|
data object NavigateBack : PlayerEvents
|
||||||
data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents
|
|
||||||
|
data class IsPlayingChanged(
|
||||||
|
val isPlaying: Boolean,
|
||||||
|
) : PlayerEvents
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue