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.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.ui.DefaultTimeBar
|
||||
import androidx.media3.ui.PlayerControlView
|
||||
import androidx.media3.ui.PlayerView
|
||||
import androidx.navigation.navArgs
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import com.nomadics9.ananas.databinding.ActivityPlayerBinding
|
||||
import com.nomadics9.ananas.dialogs.SpeedSelectionDialogFragment
|
||||
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.viewmodels.PlayerActivityViewModel
|
||||
import com.nomadics9.ananas.viewmodels.PlayerEvents
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
@ -54,7 +52,6 @@ var isControlsLocked: Boolean = false
|
|||
|
||||
@AndroidEntryPoint
|
||||
class PlayerActivity : BasePlayerActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
|
@ -115,12 +112,13 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
configureInsets(lockedControls)
|
||||
|
||||
if (appPreferences.playerGestures) {
|
||||
playerGestureHelper = PlayerGestureHelper(
|
||||
appPreferences,
|
||||
this,
|
||||
binding.playerView,
|
||||
getSystemService(AUDIO_SERVICE) as AudioManager,
|
||||
)
|
||||
playerGestureHelper =
|
||||
PlayerGestureHelper(
|
||||
appPreferences,
|
||||
this,
|
||||
binding.playerView,
|
||||
getSystemService(AUDIO_SERVICE) as AudioManager,
|
||||
)
|
||||
}
|
||||
|
||||
binding.playerView.findViewById<View>(R.id.back_button).setOnClickListener {
|
||||
|
@ -155,7 +153,12 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
skipButton.text =
|
||||
getString(CoreR.string.skip_intro_button)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -167,7 +170,10 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
getString(CoreR.string.skip_credit_button_last)
|
||||
}
|
||||
skipButton.isVisible =
|
||||
!isInPictureInPictureMode && !buttonPressed && currentSegment?.skip == true && !binding.playerView.isControllerFullyVisible
|
||||
!isInPictureInPictureMode &&
|
||||
!buttonPressed &&
|
||||
currentSegment?.skip == true &&
|
||||
!binding.playerView.isControllerFullyVisible
|
||||
watchCreditsButton.isVisible = skipButton.isVisible
|
||||
}
|
||||
|
||||
|
@ -181,12 +187,15 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
when (currentSegment?.type) {
|
||||
"intro" -> {
|
||||
skipButton.isVisible =
|
||||
!buttonPressed && (showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
|
||||
!buttonPressed &&
|
||||
(showSkip == true || (visibility == View.VISIBLE && currentSegment?.skip == true))
|
||||
}
|
||||
|
||||
"credit" -> {
|
||||
skipButton.isVisible =
|
||||
!buttonPressed && currentSegment?.skip == true && visibility == View.GONE
|
||||
!buttonPressed &&
|
||||
currentSegment?.skip == true &&
|
||||
visibility == View.GONE
|
||||
watchCreditsButton.isVisible = skipButton.isVisible
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +277,8 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
if (appPreferences.playerPipGesture) {
|
||||
try {
|
||||
setPictureInPictureParams(pipParams(event.isPlaying))
|
||||
} catch (_: IllegalArgumentException) { }
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -346,11 +356,12 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
|
||||
if (appPreferences.playerTrickplay) {
|
||||
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
|
||||
previewScrubListener = PreviewScrubListener(
|
||||
imagePreview,
|
||||
timeBar,
|
||||
viewModel.player,
|
||||
)
|
||||
previewScrubListener =
|
||||
PreviewScrubListener(
|
||||
imagePreview,
|
||||
timeBar,
|
||||
viewModel.player,
|
||||
)
|
||||
|
||||
timeBar.addListener(previewScrubListener!!)
|
||||
}
|
||||
|
@ -381,34 +392,38 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
private fun pipParams(enableAutoEnter: Boolean = viewModel.player.isPlaying): PictureInPictureParams {
|
||||
val displayAspectRatio = Rational(binding.playerView.width, binding.playerView.height)
|
||||
|
||||
val aspectRatio = binding.playerView.player?.videoSize?.let {
|
||||
Rational(
|
||||
it.width.coerceAtMost((it.height * 2.39f).toInt()),
|
||||
it.height.coerceAtMost((it.width * 2.39f).toInt()),
|
||||
)
|
||||
}
|
||||
val aspectRatio =
|
||||
binding.playerView.player?.videoSize?.let {
|
||||
Rational(
|
||||
it.width.coerceAtMost((it.height * 2.39f).toInt()),
|
||||
it.height.coerceAtMost((it.width * 2.39f).toInt()),
|
||||
)
|
||||
}
|
||||
|
||||
val sourceRectHint = if (displayAspectRatio < aspectRatio!!) {
|
||||
val space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
|
||||
Rect(
|
||||
0,
|
||||
space,
|
||||
binding.playerView.width,
|
||||
(binding.playerView.width.toFloat() / aspectRatio.toFloat()).toInt() + space,
|
||||
)
|
||||
} else {
|
||||
val space = ((binding.playerView.width - (binding.playerView.height.toFloat() * aspectRatio.toFloat())) / 2).toInt()
|
||||
Rect(
|
||||
space,
|
||||
0,
|
||||
(binding.playerView.height.toFloat() * aspectRatio.toFloat()).toInt() + space,
|
||||
binding.playerView.height,
|
||||
)
|
||||
}
|
||||
val sourceRectHint =
|
||||
if (displayAspectRatio < aspectRatio!!) {
|
||||
val space = ((binding.playerView.height - (binding.playerView.width.toFloat() / aspectRatio.toFloat())) / 2).toInt()
|
||||
Rect(
|
||||
0,
|
||||
space,
|
||||
binding.playerView.width,
|
||||
(binding.playerView.width.toFloat() / aspectRatio.toFloat()).toInt() + space,
|
||||
)
|
||||
} else {
|
||||
val space = ((binding.playerView.width - (binding.playerView.height.toFloat() * aspectRatio.toFloat())) / 2).toInt()
|
||||
Rect(
|
||||
space,
|
||||
0,
|
||||
(binding.playerView.height.toFloat() * aspectRatio.toFloat()).toInt() + space,
|
||||
binding.playerView.height,
|
||||
)
|
||||
}
|
||||
|
||||
val builder = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(aspectRatio)
|
||||
.setSourceRectHint(sourceRectHint)
|
||||
val builder =
|
||||
PictureInPictureParams
|
||||
.Builder()
|
||||
.setAspectRatio(aspectRatio)
|
||||
.setSourceRectHint(sourceRectHint)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
builder.setAutoEnterEnabled(enableAutoEnter)
|
||||
|
@ -424,31 +439,52 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
|
||||
try {
|
||||
enterPictureInPictureMode(pipParams())
|
||||
} catch (_: IllegalArgumentException) { }
|
||||
} catch (_: IllegalArgumentException) {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
// Map entries to values
|
||||
val qualityMap = qualityEntries.zip(qualityValues).toMap()
|
||||
|
||||
val qualities: List<String> =
|
||||
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
|
||||
}
|
||||
|
||||
val qualities = when (height) {
|
||||
0 -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
|
||||
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")
|
||||
else -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps")
|
||||
}
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Select Video Quality")
|
||||
.setItems(qualities) { _, which ->
|
||||
val selectedQuality = qualities[which]
|
||||
viewModel.changeVideoQuality(selectedQuality)
|
||||
}
|
||||
.show()
|
||||
.setItems(qualities.toTypedArray()) { _, which ->
|
||||
val selectedQualityEntry = qualities[which]
|
||||
val selectedQualityValue =
|
||||
qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry
|
||||
viewModel.changeVideoQuality(selectedQualityValue)
|
||||
}.show()
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration,
|
||||
|
@ -463,25 +499,29 @@ class PlayerActivity : BasePlayerActivity() {
|
|||
playerGestureHelper?.updateZoomMode(false)
|
||||
|
||||
// Brightness mode Auto
|
||||
window.attributes = window.attributes.apply {
|
||||
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
window.attributes =
|
||||
window.attributes.apply {
|
||||
screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
}
|
||||
false -> {
|
||||
binding.playerView.useController = true
|
||||
playerGestureHelper?.updateZoomMode(wasZoom)
|
||||
|
||||
// Override auto brightness
|
||||
window.attributes = window.attributes.apply {
|
||||
screenBrightness = if (appPreferences.playerBrightnessRemember) {
|
||||
appPreferences.playerBrightness
|
||||
} else {
|
||||
Settings.System.getInt(
|
||||
contentResolver,
|
||||
Settings.System.SCREEN_BRIGHTNESS,
|
||||
).toFloat() / 255
|
||||
window.attributes =
|
||||
window.attributes.apply {
|
||||
screenBrightness =
|
||||
if (appPreferences.playerBrightnessRemember) {
|
||||
appPreferences.playerBrightness
|
||||
} else {
|
||||
Settings.System
|
||||
.getInt(
|
||||
contentResolver,
|
||||
Settings.System.SCREEN_BRIGHTNESS,
|
||||
).toFloat() / 255
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.nomadics9.ananas.models.FindroidSource
|
|||
import com.nomadics9.ananas.models.FindroidSources
|
||||
import com.nomadics9.ananas.models.FindroidTrickplayInfo
|
||||
import com.nomadics9.ananas.models.UiText
|
||||
import com.nomadics9.ananas.models.VideoQuality
|
||||
import com.nomadics9.ananas.models.toFindroidEpisodeDto
|
||||
import com.nomadics9.ananas.models.toFindroidMediaStreamDto
|
||||
import com.nomadics9.ananas.models.toFindroidMovieDto
|
||||
|
@ -50,17 +51,17 @@ class DownloaderImpl(
|
|||
storageIndex: Int,
|
||||
): Pair<Long, UiText?> {
|
||||
try {
|
||||
|
||||
Timber.d("Downloading item: ${item.id} with sourceId: $sourceId")
|
||||
|
||||
val source =
|
||||
jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
|
||||
val segments = jellyfinRepository.getSegmentsTimestamps(item.id)
|
||||
val trickplayInfo = if (item is FindroidSources) {
|
||||
item.trickplayInfo?.get(sourceId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val trickplayInfo =
|
||||
if (item is FindroidSources) {
|
||||
item.trickplayInfo?.get(sourceId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
|
||||
if (storageLocation == null || Environment.getExternalStorageState(storageLocation) != Environment.MEDIA_MOUNTED) {
|
||||
return Pair(-1, UiText.StringResource(CoreR.string.storage_unavailable))
|
||||
|
@ -96,9 +97,13 @@ class DownloaderImpl(
|
|||
|
||||
return Pair(
|
||||
-1,
|
||||
if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(
|
||||
CoreR.string.unknown_error
|
||||
)
|
||||
if (e.message != null) {
|
||||
UiText.DynamicString(e.message!!)
|
||||
} else {
|
||||
UiText.StringResource(
|
||||
CoreR.string.unknown_error,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +115,7 @@ class DownloaderImpl(
|
|||
trickplayInfo: FindroidTrickplayInfo?,
|
||||
segments: List<FindroidSegment>?,
|
||||
path: Uri,
|
||||
quality: String
|
||||
quality: String,
|
||||
): Pair<Long, UiText?> {
|
||||
val transcodingUrl = getTranscodedUrl(item.id, quality)
|
||||
when (item) {
|
||||
|
@ -126,12 +131,14 @@ class DownloaderImpl(
|
|||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
val request = DownloadManager.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
|
@ -139,7 +146,8 @@ class DownloaderImpl(
|
|||
|
||||
is FindroidEpisode -> {
|
||||
database.insertShow(
|
||||
jellyfinRepository.getShow(item.seriesId)
|
||||
jellyfinRepository
|
||||
.getShow(item.seriesId)
|
||||
.toFindroidShowDto(appPreferences.currentServer!!),
|
||||
)
|
||||
database.insertSeason(
|
||||
|
@ -156,12 +164,14 @@ class DownloaderImpl(
|
|||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
val request = DownloadManager.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(transcodingUrl)
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
|
@ -190,12 +200,14 @@ class DownloaderImpl(
|
|||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
val request = DownloadManager.Request(source.path.toUri())
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(source.path.toUri())
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
|
@ -203,7 +215,8 @@ class DownloaderImpl(
|
|||
|
||||
is FindroidEpisode -> {
|
||||
database.insertShow(
|
||||
jellyfinRepository.getShow(item.seriesId)
|
||||
jellyfinRepository
|
||||
.getShow(item.seriesId)
|
||||
.toFindroidShowDto(appPreferences.currentServer!!),
|
||||
)
|
||||
database.insertSeason(
|
||||
|
@ -219,12 +232,14 @@ class DownloaderImpl(
|
|||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
}
|
||||
val request = DownloadManager.Request(source.path.toUri())
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(source.path.toUri())
|
||||
.setTitle(item.name)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationUri(path)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setSourceDownloadId(source.id, downloadId)
|
||||
return Pair(downloadId, null)
|
||||
|
@ -233,15 +248,20 @@ class DownloaderImpl(
|
|||
return Pair(-1, null)
|
||||
}
|
||||
|
||||
|
||||
override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) {
|
||||
override suspend fun cancelDownload(
|
||||
item: FindroidItem,
|
||||
source: FindroidSource,
|
||||
) {
|
||||
if (source.downloadId != null) {
|
||||
downloadManager.remove(source.downloadId!!)
|
||||
}
|
||||
deleteItem(item, source)
|
||||
}
|
||||
|
||||
override suspend fun deleteItem(item: FindroidItem, source: FindroidSource) {
|
||||
override suspend fun deleteItem(
|
||||
item: FindroidItem,
|
||||
source: FindroidSource,
|
||||
) {
|
||||
when (item) {
|
||||
is FindroidMovie -> {
|
||||
database.deleteMovie(item.id)
|
||||
|
@ -283,15 +303,18 @@ class DownloaderImpl(
|
|||
if (downloadId == null) {
|
||||
return Pair(downloadStatus, progress)
|
||||
}
|
||||
val query = DownloadManager.Query()
|
||||
.setFilterById(downloadId)
|
||||
val query =
|
||||
DownloadManager
|
||||
.Query()
|
||||
.setFilterById(downloadId)
|
||||
val cursor = downloadManager.query(query)
|
||||
if (cursor.moveToFirst()) {
|
||||
downloadStatus = cursor.getInt(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
DownloadManager.COLUMN_STATUS,
|
||||
),
|
||||
)
|
||||
downloadStatus =
|
||||
cursor.getInt(
|
||||
cursor.getColumnIndexOrThrow(
|
||||
DownloadManager.COLUMN_STATUS,
|
||||
),
|
||||
)
|
||||
when (downloadStatus) {
|
||||
DownloadManager.STATUS_RUNNING -> {
|
||||
val totalBytes =
|
||||
|
@ -320,25 +343,28 @@ class DownloaderImpl(
|
|||
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
|
||||
for (mediaStream in source.mediaStreams.filter { it.isExternal }) {
|
||||
val id = UUID.randomUUID()
|
||||
val streamPath = Uri.fromFile(
|
||||
File(
|
||||
storageLocation,
|
||||
"downloads/${item.id}.${source.id}.$id.download"
|
||||
val streamPath =
|
||||
Uri.fromFile(
|
||||
File(
|
||||
storageLocation,
|
||||
"downloads/${item.id}.${source.id}.$id.download",
|
||||
),
|
||||
)
|
||||
)
|
||||
database.insertMediaStream(
|
||||
mediaStream.toFindroidMediaStreamDto(
|
||||
id,
|
||||
source.id,
|
||||
streamPath.path.orEmpty()
|
||||
)
|
||||
streamPath.path.orEmpty(),
|
||||
),
|
||||
)
|
||||
val request = DownloadManager.Request(Uri.parse(mediaStream.path))
|
||||
.setTitle(mediaStream.title)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||
.setDestinationUri(streamPath)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(Uri.parse(mediaStream.path))
|
||||
.setTitle(mediaStream.title)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||
.setDestinationUri(streamPath)
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setMediaStreamDownloadId(id, downloadId)
|
||||
}
|
||||
|
@ -347,7 +373,7 @@ class DownloaderImpl(
|
|||
private fun downloadEmbeddedMediaStreams(
|
||||
item: FindroidItem,
|
||||
source: FindroidSource,
|
||||
storageIndex: Int = 0
|
||||
storageIndex: Int = 0,
|
||||
) {
|
||||
val storageLocation = context.getExternalFilesDirs(null)[storageIndex]
|
||||
val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null }
|
||||
|
@ -357,50 +383,55 @@ class DownloaderImpl(
|
|||
deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt")
|
||||
}
|
||||
val id = UUID.randomUUID()
|
||||
val streamPath = Uri.fromFile(
|
||||
File(
|
||||
storageLocation,
|
||||
"downloads/${item.id}.${source.id}.$id.download"
|
||||
val streamPath =
|
||||
Uri.fromFile(
|
||||
File(
|
||||
storageLocation,
|
||||
"downloads/${item.id}.${source.id}.$id.download",
|
||||
),
|
||||
)
|
||||
)
|
||||
database.insertMediaStream(
|
||||
mediaStream.toFindroidMediaStreamDto(
|
||||
id,
|
||||
source.id,
|
||||
streamPath.path.orEmpty()
|
||||
)
|
||||
streamPath.path.orEmpty(),
|
||||
),
|
||||
)
|
||||
val request = DownloadManager.Request(Uri.parse(deliveryUrl))
|
||||
.setTitle(mediaStream.title)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||
.setDestinationUri(streamPath)
|
||||
val request =
|
||||
DownloadManager
|
||||
.Request(Uri.parse(deliveryUrl))
|
||||
.setTitle(mediaStream.title)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
.setAllowedOverRoaming(appPreferences.downloadWhenRoaming)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||
.setDestinationUri(streamPath)
|
||||
|
||||
val downloadId = downloadManager.enqueue(request)
|
||||
database.setMediaStreamDownloadId(id, downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadTrickplayData(
|
||||
itemId: UUID,
|
||||
sourceId: String,
|
||||
trickplayInfo: FindroidTrickplayInfo,
|
||||
) {
|
||||
val maxIndex = ceil(
|
||||
trickplayInfo.thumbnailCount.toDouble()
|
||||
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)
|
||||
).toInt()
|
||||
val maxIndex =
|
||||
ceil(
|
||||
trickplayInfo.thumbnailCount
|
||||
.toDouble()
|
||||
.div(trickplayInfo.tileWidth * trickplayInfo.tileHeight),
|
||||
).toInt()
|
||||
val byteArrays = mutableListOf<ByteArray>()
|
||||
for (i in 0..maxIndex) {
|
||||
jellyfinRepository.getTrickplayData(
|
||||
itemId,
|
||||
trickplayInfo.width,
|
||||
i,
|
||||
)?.let { byteArray ->
|
||||
byteArrays.add(byteArray)
|
||||
}
|
||||
jellyfinRepository
|
||||
.getTrickplayData(
|
||||
itemId,
|
||||
trickplayInfo.width,
|
||||
i,
|
||||
)?.let { byteArray ->
|
||||
byteArrays.add(byteArray)
|
||||
}
|
||||
}
|
||||
saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays)
|
||||
}
|
||||
|
@ -420,52 +451,46 @@ class DownloaderImpl(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getTranscodedUrl(itemId: UUID, quality: String): Uri? {
|
||||
val maxBitrate = when (quality) {
|
||||
"720p" -> 2000000 // 2 Mbps
|
||||
"480p" -> 1000000 // 1 Mbps
|
||||
"360p" -> 800000 // 800Kbps
|
||||
else -> 2000000 // Default to 2 Mbps if not specified
|
||||
}
|
||||
|
||||
private suspend fun getTranscodedUrl(
|
||||
itemId: UUID,
|
||||
quality: String,
|
||||
): Uri? {
|
||||
val videoQuality = VideoQuality.fromString(quality)!!
|
||||
return try {
|
||||
|
||||
val deviceProfile = jellyfinRepository.buildDeviceProfile(maxBitrate,"mkv", EncodingContext.STATIC)
|
||||
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,false,deviceProfile,maxBitrate)
|
||||
val mediaSourceId = playbackInfo.content.mediaSources.firstOrNull()?.id!!
|
||||
val deviceProfile =
|
||||
jellyfinRepository.buildDeviceProfile(
|
||||
VideoQuality.getBitrate(videoQuality),
|
||||
"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 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)
|
||||
Timber.d("Constructed Transcode URL: $transcodeUri")
|
||||
transcodeUri
|
||||
return downloadUrl.toUri()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
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>
|
||||
</string-array>
|
||||
<string-array name="quality_entries">
|
||||
<item>Auto</item>
|
||||
<item>Original</item>
|
||||
<item>1080p - 8Mbps</item>
|
||||
<item>720p - 2Mbps</item>
|
||||
<item>480p - 1Mbps</item>
|
||||
<item>360p - 800Kbps</item>
|
||||
</string-array>
|
||||
<string-array name="quality_values">
|
||||
<item>Auto</item>
|
||||
<item>Original</item>
|
||||
<item>1080p</item>
|
||||
<item>720p</item>
|
||||
<item>480p</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.model.api.BaseItemDto
|
||||
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.EncodingContext
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
|
@ -31,7 +29,9 @@ interface JellyfinRepository {
|
|||
suspend fun getUserViews(): List<BaseItemDto>
|
||||
|
||||
suspend fun getItem(itemId: UUID): BaseItemDto
|
||||
|
||||
suspend fun getEpisode(itemId: UUID): FindroidEpisode
|
||||
|
||||
suspend fun getMovie(itemId: UUID): FindroidMovie
|
||||
|
||||
suspend fun getShow(itemId: UUID): FindroidShow
|
||||
|
@ -72,7 +72,10 @@ interface JellyfinRepository {
|
|||
|
||||
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>
|
||||
|
||||
|
@ -85,21 +88,40 @@ interface JellyfinRepository {
|
|||
offline: Boolean = false,
|
||||
): 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 getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?
|
||||
suspend fun getTrickplayData(
|
||||
itemId: UUID,
|
||||
width: Int,
|
||||
index: Int,
|
||||
): ByteArray?
|
||||
|
||||
suspend fun postCapabilities()
|
||||
|
||||
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)
|
||||
|
||||
|
@ -121,15 +143,36 @@ interface JellyfinRepository {
|
|||
|
||||
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, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse>
|
||||
suspend fun getPostedPlaybackInfo(
|
||||
itemId: UUID,
|
||||
enableDirectStream: Boolean,
|
||||
deviceProfile: DeviceProfile,
|
||||
maxBitrate: Int,
|
||||
): Response<PlaybackInfoResponse>
|
||||
|
||||
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.FindroidSource
|
||||
import com.nomadics9.ananas.models.SortBy
|
||||
import com.nomadics9.ananas.models.VideoQuality
|
||||
import com.nomadics9.ananas.models.toFindroidCollection
|
||||
import com.nomadics9.ananas.models.toFindroidEpisode
|
||||
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.BaseItemKind
|
||||
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.DeviceProfile
|
||||
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.SubtitleDeliveryMethod
|
||||
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.TranscodingProfile
|
||||
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||
|
@ -71,55 +70,70 @@ class JellyfinRepositoryImpl(
|
|||
private val database: ServerDatabaseDao,
|
||||
private val appPreferences: AppPreferences,
|
||||
) : JellyfinRepository {
|
||||
override suspend fun getPublicSystemInfo(): PublicSystemInfo = withContext(Dispatchers.IO) {
|
||||
jellyfinApi.systemApi.getPublicSystemInfo().content
|
||||
}
|
||||
override suspend fun getPublicSystemInfo(): PublicSystemInfo =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.systemApi.getPublicSystemInfo().content
|
||||
}
|
||||
|
||||
override suspend fun getUserViews(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty()
|
||||
}
|
||||
override suspend fun getUserViews(): List<BaseItemDto> =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.viewsApi
|
||||
.getUserViews(jellyfinApi.userId!!)
|
||||
.content.items
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
override suspend fun getItem(itemId: UUID): BaseItemDto = withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
|
||||
}
|
||||
override suspend fun getItem(itemId: UUID): BaseItemDto =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content
|
||||
}
|
||||
|
||||
override suspend fun getEpisode(itemId: UUID): FindroidEpisode =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
|
||||
jellyfinApi.userLibraryApi
|
||||
.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content
|
||||
.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!!
|
||||
}
|
||||
|
||||
override suspend fun getMovie(itemId: UUID): FindroidMovie =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content.toFindroidMovie(this@JellyfinRepositoryImpl, database)
|
||||
jellyfinApi.userLibraryApi
|
||||
.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content
|
||||
.toFindroidMovie(this@JellyfinRepositoryImpl, database)
|
||||
}
|
||||
|
||||
override suspend fun getShow(itemId: UUID): FindroidShow =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content.toFindroidShow(this@JellyfinRepositoryImpl)
|
||||
jellyfinApi.userLibraryApi
|
||||
.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content
|
||||
.toFindroidShow(this@JellyfinRepositoryImpl)
|
||||
}
|
||||
|
||||
override suspend fun getSeason(itemId: UUID): FindroidSeason =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content.toFindroidSeason(this@JellyfinRepositoryImpl)
|
||||
jellyfinApi.userLibraryApi
|
||||
.getItem(
|
||||
itemId,
|
||||
jellyfinApi.userId!!,
|
||||
).content
|
||||
.toFindroidSeason(this@JellyfinRepositoryImpl)
|
||||
}
|
||||
|
||||
override suspend fun getLibraries(): List<FindroidCollection> =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
).content.items
|
||||
jellyfinApi.itemsApi
|
||||
.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull { it.toFindroidCollection(this@JellyfinRepositoryImpl) }
|
||||
}
|
||||
|
@ -134,16 +148,17 @@ class JellyfinRepositoryImpl(
|
|||
limit: Int?,
|
||||
): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
parentId = parentId,
|
||||
includeItemTypes = includeTypes,
|
||||
recursive = recursive,
|
||||
sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)),
|
||||
sortOrder = listOf(sortOrder),
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
).content.items
|
||||
jellyfinApi.itemsApi
|
||||
.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
parentId = parentId,
|
||||
includeItemTypes = includeTypes,
|
||||
recursive = recursive,
|
||||
sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)),
|
||||
sortOrder = listOf(sortOrder),
|
||||
startIndex = startIndex,
|
||||
limit = limit,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
|
||||
}
|
||||
|
@ -154,13 +169,14 @@ class JellyfinRepositoryImpl(
|
|||
recursive: Boolean,
|
||||
sortBy: SortBy,
|
||||
sortOrder: SortOrder,
|
||||
): Flow<PagingData<FindroidItem>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 10,
|
||||
maxSize = 100,
|
||||
enablePlaceholders = false,
|
||||
),
|
||||
): Flow<PagingData<FindroidItem>> =
|
||||
Pager(
|
||||
config =
|
||||
PagingConfig(
|
||||
pageSize = 10,
|
||||
maxSize = 100,
|
||||
enablePlaceholders = false,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
ItemsPagingSource(
|
||||
this,
|
||||
|
@ -172,87 +188,102 @@ class JellyfinRepositoryImpl(
|
|||
)
|
||||
},
|
||||
).flow
|
||||
}
|
||||
|
||||
override suspend fun getPersonItems(
|
||||
personIds: List<UUID>,
|
||||
includeTypes: List<BaseItemKind>?,
|
||||
recursive: Boolean,
|
||||
): List<FindroidItem> = withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
personIds = personIds,
|
||||
includeItemTypes = includeTypes,
|
||||
recursive = recursive,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull {
|
||||
it.toFindroidItem(this@JellyfinRepositoryImpl, database)
|
||||
}
|
||||
}
|
||||
): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi
|
||||
.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
personIds = personIds,
|
||||
includeItemTypes = includeTypes,
|
||||
recursive = recursive,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull {
|
||||
it.toFindroidItem(this@JellyfinRepositoryImpl, database)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFavoriteItems(): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
filters = listOf(ItemFilter.IS_FAVORITE),
|
||||
includeItemTypes = listOf(
|
||||
BaseItemKind.MOVIE,
|
||||
BaseItemKind.SERIES,
|
||||
BaseItemKind.EPISODE,
|
||||
),
|
||||
recursive = true,
|
||||
).content.items
|
||||
jellyfinApi.itemsApi
|
||||
.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
filters = listOf(ItemFilter.IS_FAVORITE),
|
||||
includeItemTypes =
|
||||
listOf(
|
||||
BaseItemKind.MOVIE,
|
||||
BaseItemKind.SERIES,
|
||||
BaseItemKind.EPISODE,
|
||||
),
|
||||
recursive = true,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
|
||||
}
|
||||
|
||||
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
searchTerm = searchQuery,
|
||||
includeItemTypes = listOf(
|
||||
BaseItemKind.MOVIE,
|
||||
BaseItemKind.SERIES,
|
||||
BaseItemKind.EPISODE,
|
||||
),
|
||||
recursive = true,
|
||||
).content.items
|
||||
jellyfinApi.itemsApi
|
||||
.getItems(
|
||||
jellyfinApi.userId!!,
|
||||
searchTerm = searchQuery,
|
||||
includeItemTypes =
|
||||
listOf(
|
||||
BaseItemKind.MOVIE,
|
||||
BaseItemKind.SERIES,
|
||||
BaseItemKind.EPISODE,
|
||||
),
|
||||
recursive = true,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) }
|
||||
}
|
||||
|
||||
override suspend fun getResumeItems(): List<FindroidItem> {
|
||||
val items = withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi.getResumeItems(
|
||||
jellyfinApi.userId!!,
|
||||
limit = 12,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
||||
).content.items.orEmpty()
|
||||
}
|
||||
val items =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.itemsApi
|
||||
.getResumeItems(
|
||||
jellyfinApi.userId!!,
|
||||
limit = 12,
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE),
|
||||
).content.items
|
||||
.orEmpty()
|
||||
}
|
||||
return items.mapNotNull {
|
||||
it.toFindroidItem(this, database)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
|
||||
val items = withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi.getLatestMedia(
|
||||
jellyfinApi.userId!!,
|
||||
parentId = parentId,
|
||||
limit = 16,
|
||||
).content
|
||||
}
|
||||
val items =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userLibraryApi
|
||||
.getLatestMedia(
|
||||
jellyfinApi.userId!!,
|
||||
parentId = parentId,
|
||||
limit = 16,
|
||||
).content
|
||||
}
|
||||
return items.mapNotNull {
|
||||
it.toFindroidItem(this, database)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> =
|
||||
override suspend fun getSeasons(
|
||||
seriesId: UUID,
|
||||
offline: Boolean,
|
||||
): List<FindroidSeason> =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!offline) {
|
||||
jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items
|
||||
jellyfinApi.showsApi
|
||||
.getSeasons(seriesId, jellyfinApi.userId!!)
|
||||
.content.items
|
||||
.orEmpty()
|
||||
.map { it.toFindroidSeason(this@JellyfinRepositoryImpl) }
|
||||
} else {
|
||||
|
@ -262,12 +293,13 @@ class JellyfinRepositoryImpl(
|
|||
|
||||
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.showsApi.getNextUp(
|
||||
jellyfinApi.userId!!,
|
||||
limit = 24,
|
||||
seriesId = seriesId,
|
||||
enableResumable = false,
|
||||
).content.items
|
||||
jellyfinApi.showsApi
|
||||
.getNextUp(
|
||||
jellyfinApi.userId!!,
|
||||
limit = 24,
|
||||
seriesId = seriesId,
|
||||
enableResumable = false,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl) }
|
||||
}
|
||||
|
@ -282,14 +314,15 @@ class JellyfinRepositoryImpl(
|
|||
): List<FindroidEpisode> =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!offline) {
|
||||
jellyfinApi.showsApi.getEpisodes(
|
||||
seriesId,
|
||||
jellyfinApi.userId!!,
|
||||
seasonId = seasonId,
|
||||
fields = fields,
|
||||
startItemId = startItemId,
|
||||
limit = limit,
|
||||
).content.items
|
||||
jellyfinApi.showsApi
|
||||
.getEpisodes(
|
||||
seriesId,
|
||||
jellyfinApi.userId!!,
|
||||
seasonId = seasonId,
|
||||
fields = fields,
|
||||
startItemId = startItemId,
|
||||
limit = limit,
|
||||
).content.items
|
||||
.orEmpty()
|
||||
.mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl, database) }
|
||||
} else {
|
||||
|
@ -297,39 +330,47 @@ 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) {
|
||||
val sources = mutableListOf<FindroidSource>()
|
||||
sources.addAll(
|
||||
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
||||
itemId,
|
||||
PlaybackInfoDto(
|
||||
userId = jellyfinApi.userId!!,
|
||||
deviceProfile = DeviceProfile(
|
||||
name = "Direct play all",
|
||||
maxStaticBitrate = 1_000_000_000,
|
||||
maxStreamingBitrate = 1_000_000_000,
|
||||
codecProfiles = emptyList(),
|
||||
containerProfiles = emptyList(),
|
||||
directPlayProfiles = listOf(
|
||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||
),
|
||||
transcodingProfiles = emptyList(),
|
||||
subtitleProfiles = listOf(
|
||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||
),
|
||||
),
|
||||
maxStreamingBitrate = 1_000_000_000,
|
||||
),
|
||||
).content.mediaSources.map {
|
||||
it.toFindroidSource(
|
||||
this@JellyfinRepositoryImpl,
|
||||
jellyfinApi.mediaInfoApi
|
||||
.getPostedPlaybackInfo(
|
||||
itemId,
|
||||
includePath,
|
||||
)
|
||||
},
|
||||
PlaybackInfoDto(
|
||||
userId = jellyfinApi.userId!!,
|
||||
deviceProfile =
|
||||
DeviceProfile(
|
||||
name = "Direct play all",
|
||||
maxStaticBitrate = 1_000_000_000,
|
||||
maxStreamingBitrate = 1_000_000_000,
|
||||
codecProfiles = emptyList(),
|
||||
containerProfiles = emptyList(),
|
||||
directPlayProfiles =
|
||||
listOf(
|
||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||
),
|
||||
transcodingProfiles = emptyList(),
|
||||
subtitleProfiles =
|
||||
listOf(
|
||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||
),
|
||||
),
|
||||
maxStreamingBitrate = 1_000_000_000,
|
||||
),
|
||||
).content.mediaSources
|
||||
.map {
|
||||
it.toFindroidSource(
|
||||
this@JellyfinRepositoryImpl,
|
||||
itemId,
|
||||
includePath,
|
||||
)
|
||||
},
|
||||
)
|
||||
sources.addAll(
|
||||
database.getSources(itemId).map { it.toFindroidSource(database) },
|
||||
|
@ -337,26 +378,32 @@ class JellyfinRepositoryImpl(
|
|||
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) {
|
||||
// val deviceId = getDeviceId()
|
||||
try {
|
||||
val url = if (playSessionId != null) {
|
||||
jellyfinApi.videosApi.getVideoStreamUrl(
|
||||
itemId,
|
||||
static = true,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
deviceId = getDeviceId(),
|
||||
context = EncodingContext.STATIC
|
||||
)
|
||||
} else {
|
||||
jellyfinApi.videosApi.getVideoStreamUrl(
|
||||
itemId,
|
||||
static = true,
|
||||
mediaSourceId = mediaSourceId,
|
||||
deviceId = getDeviceId(),
|
||||
)
|
||||
}
|
||||
val url =
|
||||
if (playSessionId != null) {
|
||||
jellyfinApi.videosApi.getVideoStreamUrl(
|
||||
itemId,
|
||||
static = true,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
// deviceId = deviceId,
|
||||
context = EncodingContext.STREAMING,
|
||||
)
|
||||
} else {
|
||||
jellyfinApi.videosApi.getVideoStreamUrl(
|
||||
itemId,
|
||||
static = true,
|
||||
mediaSourceId = mediaSourceId,
|
||||
// deviceId = deviceId,
|
||||
)
|
||||
}
|
||||
url
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
|
@ -377,33 +424,36 @@ class JellyfinRepositoryImpl(
|
|||
pathParameters["itemId"] = itemId
|
||||
|
||||
try {
|
||||
val segmentToConvert = jellyfinApi.api.get<FindroidSegments>(
|
||||
"/Episode/{itemId}/IntroSkipperSegments",
|
||||
pathParameters,
|
||||
).content
|
||||
val segmentToConvert =
|
||||
jellyfinApi.api
|
||||
.get<FindroidSegments>(
|
||||
"/Episode/{itemId}/IntroSkipperSegments",
|
||||
pathParameters,
|
||||
).content
|
||||
|
||||
val segmentConverted = mutableListOf(
|
||||
segmentToConvert.intro!!.let {
|
||||
FindroidSegment(
|
||||
type = "intro",
|
||||
skip = true,
|
||||
startTime = it.startTime,
|
||||
endTime = it.endTime,
|
||||
showAt = it.showAt,
|
||||
hideAt = it.hideAt,
|
||||
)
|
||||
},
|
||||
segmentToConvert.credit!!.let {
|
||||
FindroidSegment(
|
||||
type = "credit",
|
||||
skip = true,
|
||||
startTime = it.startTime,
|
||||
endTime = it.endTime,
|
||||
showAt = it.showAt,
|
||||
hideAt = it.hideAt,
|
||||
)
|
||||
},
|
||||
)
|
||||
val segmentConverted =
|
||||
mutableListOf(
|
||||
segmentToConvert.intro!!.let {
|
||||
FindroidSegment(
|
||||
type = "intro",
|
||||
skip = true,
|
||||
startTime = it.startTime,
|
||||
endTime = it.endTime,
|
||||
showAt = it.showAt,
|
||||
hideAt = it.hideAt,
|
||||
)
|
||||
},
|
||||
segmentToConvert.credit!!.let {
|
||||
FindroidSegment(
|
||||
type = "credit",
|
||||
skip = true,
|
||||
startTime = it.startTime,
|
||||
endTime = it.endTime,
|
||||
showAt = it.showAt,
|
||||
hideAt = it.hideAt,
|
||||
)
|
||||
},
|
||||
)
|
||||
Timber.tag("SegmentInfo").d("segmentToConvert: %s", segmentToConvert)
|
||||
Timber.tag("SegmentInfo").d("segmentConverted: %s", segmentConverted)
|
||||
|
||||
|
@ -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) {
|
||||
try {
|
||||
try {
|
||||
|
@ -421,9 +475,13 @@ class JellyfinRepositoryImpl(
|
|||
if (sources != null) {
|
||||
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) {
|
||||
return@withContext null
|
||||
}
|
||||
|
@ -434,21 +492,22 @@ class JellyfinRepositoryImpl(
|
|||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.sessionApi.postCapabilities(
|
||||
playableMediaTypes = listOf(MediaType.VIDEO),
|
||||
supportedCommands = listOf(
|
||||
GeneralCommandType.VOLUME_UP,
|
||||
GeneralCommandType.VOLUME_DOWN,
|
||||
GeneralCommandType.TOGGLE_MUTE,
|
||||
GeneralCommandType.SET_AUDIO_STREAM_INDEX,
|
||||
GeneralCommandType.SET_SUBTITLE_STREAM_INDEX,
|
||||
GeneralCommandType.MUTE,
|
||||
GeneralCommandType.UNMUTE,
|
||||
GeneralCommandType.SET_VOLUME,
|
||||
GeneralCommandType.DISPLAY_MESSAGE,
|
||||
GeneralCommandType.PLAY,
|
||||
GeneralCommandType.PLAY_STATE,
|
||||
GeneralCommandType.PLAY_NEXT,
|
||||
GeneralCommandType.PLAY_MEDIA_SOURCE,
|
||||
),
|
||||
supportedCommands =
|
||||
listOf(
|
||||
GeneralCommandType.VOLUME_UP,
|
||||
GeneralCommandType.VOLUME_DOWN,
|
||||
GeneralCommandType.TOGGLE_MUTE,
|
||||
GeneralCommandType.SET_AUDIO_STREAM_INDEX,
|
||||
GeneralCommandType.SET_SUBTITLE_STREAM_INDEX,
|
||||
GeneralCommandType.MUTE,
|
||||
GeneralCommandType.UNMUTE,
|
||||
GeneralCommandType.SET_VOLUME,
|
||||
GeneralCommandType.DISPLAY_MESSAGE,
|
||||
GeneralCommandType.PLAY,
|
||||
GeneralCommandType.PLAY_STATE,
|
||||
GeneralCommandType.PLAY_NEXT,
|
||||
GeneralCommandType.PLAY_MEDIA_SOURCE,
|
||||
),
|
||||
supportsMediaControl = true,
|
||||
)
|
||||
}
|
||||
|
@ -570,186 +629,214 @@ class JellyfinRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getUserConfiguration(): UserConfiguration = withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userApi.getCurrentUser().content.configuration!!
|
||||
}
|
||||
override suspend fun getUserConfiguration(): UserConfiguration =
|
||||
withContext(Dispatchers.IO) {
|
||||
jellyfinApi.userApi
|
||||
.getCurrentUser()
|
||||
.content.configuration!!
|
||||
}
|
||||
|
||||
override suspend fun getDownloads(): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val items = mutableListOf<FindroidItem>()
|
||||
items.addAll(
|
||||
database.getMoviesByServerId(appPreferences.currentServer!!)
|
||||
database
|
||||
.getMoviesByServerId(appPreferences.currentServer!!)
|
||||
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
|
||||
)
|
||||
items.addAll(
|
||||
database.getShowsByServerId(appPreferences.currentServer!!)
|
||||
database
|
||||
.getShowsByServerId(appPreferences.currentServer!!)
|
||||
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
|
||||
)
|
||||
items
|
||||
}
|
||||
|
||||
override fun getUserId(): UUID {
|
||||
return jellyfinApi.userId!!
|
||||
}
|
||||
override fun getUserId(): UUID = jellyfinApi.userId!!
|
||||
|
||||
|
||||
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
|
||||
return when (transcodeResolution) {
|
||||
1080 -> 8000000 to 384000 // Adjusted for 1080p
|
||||
720 -> 2000000 to 384000 // Adjusted for 720p
|
||||
480 -> 1000000 to 384000 // Adjusted for 480p
|
||||
360 -> 800000 to 128000 // Adjusted for 360p
|
||||
else -> 12000000 to 384000
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile {
|
||||
val deviceProfile = ClientCapabilitiesDto(
|
||||
supportedCommands = emptyList(),
|
||||
playableMediaTypes = emptyList(),
|
||||
supportsMediaControl = true,
|
||||
supportsPersistentIdentifier = true,
|
||||
deviceProfile = DeviceProfile(
|
||||
name = "AnanasUser",
|
||||
id = getUserId().toString(),
|
||||
maxStaticBitrate = maxBitrate,
|
||||
maxStreamingBitrate = maxBitrate,
|
||||
codecProfiles = emptyList(),
|
||||
containerProfiles = listOf(),
|
||||
directPlayProfiles = listOf(
|
||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||
),
|
||||
transcodingProfiles = listOf(
|
||||
TranscodingProfile(
|
||||
container = container,
|
||||
context = context,
|
||||
protocol = MediaStreamProtocol.HLS,
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
videoCodec = "hevc,h264",
|
||||
type = DlnaProfileType.VIDEO,
|
||||
conditions = listOf(
|
||||
ProfileCondition(
|
||||
condition = ProfileConditionType.LESS_THAN_EQUAL,
|
||||
property = ProfileConditionValue.VIDEO_BITRATE,
|
||||
value = "8000000",
|
||||
isRequired = true,
|
||||
)
|
||||
),
|
||||
copyTimestamps = true,
|
||||
enableSubtitlesInManifest = true,
|
||||
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
|
||||
override suspend fun buildDeviceProfile(
|
||||
maxBitrate: Int,
|
||||
container: String,
|
||||
context: EncodingContext,
|
||||
): DeviceProfile {
|
||||
val deviceProfile =
|
||||
ClientCapabilitiesDto(
|
||||
supportedCommands = emptyList(),
|
||||
playableMediaTypes =
|
||||
listOf(
|
||||
MediaType.VIDEO,
|
||||
MediaType.AUDIO,
|
||||
MediaType.UNKNOWN,
|
||||
),
|
||||
supportsMediaControl = true,
|
||||
supportsPersistentIdentifier = true,
|
||||
deviceProfile =
|
||||
DeviceProfile(
|
||||
name = "AnanasUser",
|
||||
id = getUserId().toString(),
|
||||
maxStaticBitrate = maxBitrate,
|
||||
maxStreamingBitrate = maxBitrate,
|
||||
codecProfiles = emptyList(),
|
||||
containerProfiles = listOf(),
|
||||
directPlayProfiles =
|
||||
listOf(
|
||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
||||
),
|
||||
transcodingProfiles =
|
||||
listOf(
|
||||
TranscodingProfile(
|
||||
container = container,
|
||||
context = context,
|
||||
protocol = MediaStreamProtocol.HLS,
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
videoCodec = "hevc,h264",
|
||||
type = DlnaProfileType.VIDEO,
|
||||
conditions =
|
||||
listOf(
|
||||
ProfileCondition(
|
||||
condition = ProfileConditionType.LESS_THAN_EQUAL,
|
||||
property = ProfileConditionValue.VIDEO_BITRATE,
|
||||
value = "8000000",
|
||||
isRequired = true,
|
||||
),
|
||||
),
|
||||
copyTimestamps = true,
|
||||
enableSubtitlesInManifest = true,
|
||||
transcodeSeekInfo = TranscodeSeekInfo.AUTO,
|
||||
),
|
||||
),
|
||||
subtitleProfiles =
|
||||
listOf(
|
||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL),
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitleProfiles = listOf(
|
||||
SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("sub", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("vtt", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("ssa", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("pgs", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("dvb_teletext", SubtitleDeliveryMethod.EXTERNAL),
|
||||
SubtitleProfile("dvd_subtitle", SubtitleDeliveryMethod.EXTERNAL)
|
||||
),
|
||||
)
|
||||
)
|
||||
return deviceProfile.deviceProfile!!
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getPostedPlaybackInfo(itemId: UUID ,enableDirectStream: Boolean ,deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse> {
|
||||
val playbackInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
||||
itemId = itemId,
|
||||
PlaybackInfoDto(
|
||||
userId = jellyfinApi.userId!!,
|
||||
enableTranscoding = true,
|
||||
enableDirectPlay = false,
|
||||
enableDirectStream = enableDirectStream,
|
||||
autoOpenLiveStream = true,
|
||||
deviceProfile = deviceProfile,
|
||||
allowAudioStreamCopy = true,
|
||||
allowVideoStreamCopy = true,
|
||||
maxStreamingBitrate = maxBitrate,
|
||||
override suspend fun getPostedPlaybackInfo(
|
||||
itemId: UUID,
|
||||
enableDirectStream: Boolean,
|
||||
deviceProfile: DeviceProfile,
|
||||
maxBitrate: Int,
|
||||
): Response<PlaybackInfoResponse> {
|
||||
val playbackInfo =
|
||||
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
||||
itemId = itemId,
|
||||
PlaybackInfoDto(
|
||||
userId = jellyfinApi.userId!!,
|
||||
enableTranscoding = true,
|
||||
enableDirectPlay = false,
|
||||
enableDirectStream = enableDirectStream,
|
||||
autoOpenLiveStream = true,
|
||||
deviceProfile = buildDeviceProfile(maxBitrate, "ts", EncodingContext.STREAMING),
|
||||
allowAudioStreamCopy = true,
|
||||
allowVideoStreamCopy = true,
|
||||
maxStreamingBitrate = maxBitrate,
|
||||
),
|
||||
)
|
||||
)
|
||||
return playbackInfo
|
||||
}
|
||||
|
||||
override suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String {
|
||||
val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl(
|
||||
itemId,
|
||||
static = false,
|
||||
deviceId = deviceId,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
videoBitRate = videoBitrate,
|
||||
audioBitRate = 384000,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
container = container,
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL
|
||||
)
|
||||
return url
|
||||
}
|
||||
|
||||
override suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String {
|
||||
val isAuto = videoBitrate == 12000000
|
||||
val url = if (!isAuto) {
|
||||
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
|
||||
override suspend fun getVideoStreambyContainerUrl(
|
||||
itemId: UUID,
|
||||
deviceId: String,
|
||||
mediaSourceId: String,
|
||||
playSessionId: String,
|
||||
videoBitrate: Int,
|
||||
maxHeight: Int,
|
||||
container: String,
|
||||
): String {
|
||||
val url =
|
||||
jellyfinApi.videosApi.getVideoStreamByContainerUrl(
|
||||
itemId,
|
||||
static = false,
|
||||
deviceId = deviceId,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
videoBitRate = videoBitrate,
|
||||
enableAdaptiveBitrateStreaming = false,
|
||||
audioBitRate = 384000,
|
||||
maxHeight = maxHeight,
|
||||
audioBitRate = 128000,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
audioCodec = "aac",
|
||||
container = container,
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||
context = EncodingContext.STREAMING,
|
||||
segmentContainer = "ts",
|
||||
transcodeReasons = "ContainerBitrateExceedsLimit",
|
||||
)
|
||||
} else {
|
||||
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
|
||||
itemId,
|
||||
static = false,
|
||||
deviceId = deviceId,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
enableAdaptiveBitrateStreaming = true,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac,ac3,eac3",
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||
context = EncodingContext.STREAMING,
|
||||
segmentContainer = "ts",
|
||||
transcodeReasons = "ContainerBitrateExceedsLimit",
|
||||
)
|
||||
return url
|
||||
}
|
||||
|
||||
override suspend fun getTranscodedVideoStream(
|
||||
itemId: UUID,
|
||||
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(
|
||||
itemId,
|
||||
static = false,
|
||||
deviceId = deviceId,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
videoBitRate = videoBitrate,
|
||||
enableAdaptiveBitrateStreaming = false,
|
||||
audioBitRate = 128000,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac",
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||
context = EncodingContext.STREAMING,
|
||||
segmentContainer = "ts",
|
||||
transcodeReasons = "ContainerBitrateExceedsLimit",
|
||||
)
|
||||
} else {
|
||||
jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl(
|
||||
itemId,
|
||||
static = false,
|
||||
deviceId = deviceId,
|
||||
mediaSourceId = mediaSourceId,
|
||||
playSessionId = playSessionId,
|
||||
enableAdaptiveBitrateStreaming = true,
|
||||
videoCodec = "hevc",
|
||||
audioCodec = "aac",
|
||||
startTimeTicks = 0,
|
||||
copyTimestamps = true,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
|
||||
context = EncodingContext.STREAMING,
|
||||
segmentContainer = "ts",
|
||||
transcodeReasons = "ContainerBitrateExceedsLimit",
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
throw e
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
|
||||
override suspend fun getDeviceId(): String {
|
||||
val devices = jellyfinApi.devicesApi.getDevices(getUserId())
|
||||
return devices.content.items?.firstOrNull()?.id!!
|
||||
}
|
||||
override suspend fun getDeviceId(): String = jellyfinApi.api.deviceInfo.id
|
||||
|
||||
override suspend fun stopEncodingProcess(playSessionId: String) {
|
||||
val deviceId = getDeviceId()
|
||||
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
|
||||
deviceId = deviceId,
|
||||
playSessionId = playSessionId
|
||||
playSessionId = playSessionId,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.nomadics9.ananas.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.devicelock.DeviceId
|
||||
import androidx.paging.PagingData
|
||||
import com.nomadics9.ananas.AppPreferences
|
||||
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.model.api.BaseItemDto
|
||||
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.EncodingContext
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
|
@ -44,14 +42,9 @@ class JellyfinRepositoryOfflineImpl(
|
|||
private val database: ServerDatabaseDao,
|
||||
private val appPreferences: AppPreferences,
|
||||
) : JellyfinRepository {
|
||||
override suspend fun getPublicSystemInfo(): PublicSystemInfo = throw Exception("System info not available in offline mode")
|
||||
|
||||
override suspend fun getPublicSystemInfo(): PublicSystemInfo {
|
||||
throw Exception("System info not available in offline mode")
|
||||
}
|
||||
|
||||
override suspend fun getUserViews(): List<BaseItemDto> {
|
||||
return emptyList()
|
||||
}
|
||||
override suspend fun getUserViews(): List<BaseItemDto> = emptyList()
|
||||
|
||||
override suspend fun getItem(itemId: UUID): BaseItemDto {
|
||||
TODO("Not yet implemented")
|
||||
|
@ -115,38 +108,61 @@ class JellyfinRepositoryOfflineImpl(
|
|||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val movies = database.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!!) }
|
||||
override suspend fun getSearchItems(searchQuery: String): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val movies =
|
||||
database
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getResumeItems(): List<FindroidItem> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val movies = 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 }
|
||||
override suspend fun getResumeItems(): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val movies =
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> {
|
||||
return emptyList()
|
||||
}
|
||||
override suspend fun getLatestMedia(parentId: UUID): List<FindroidItem> = emptyList()
|
||||
|
||||
override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List<FindroidSeason> =
|
||||
override suspend fun getSeasons(
|
||||
seriesId: UUID,
|
||||
offline: Boolean,
|
||||
): List<FindroidSeason> =
|
||||
withContext(Dispatchers.IO) {
|
||||
database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) }
|
||||
}
|
||||
|
||||
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
override suspend fun getNextUp(seriesId: UUID?): List<FindroidEpisode> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = mutableListOf<FindroidEpisode>()
|
||||
val shows = database.getShowsByServerId(appPreferences.currentServer!!).filter {
|
||||
if (seriesId != null) it.id == seriesId else true
|
||||
}
|
||||
val shows =
|
||||
database.getShowsByServerId(appPreferences.currentServer!!).filter {
|
||||
if (seriesId != null) it.id == seriesId else true
|
||||
}
|
||||
for (show in shows) {
|
||||
val episodes = database.getEpisodesByShowId(show.id).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }
|
||||
val indexOfLastPlayed = episodes.indexOfLast { it.played }
|
||||
|
@ -158,7 +174,6 @@ class JellyfinRepositoryOfflineImpl(
|
|||
}
|
||||
result.filter { it.playbackPositionTicks == 0L }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getEpisodes(
|
||||
seriesId: UUID,
|
||||
|
@ -174,12 +189,19 @@ class JellyfinRepositoryOfflineImpl(
|
|||
items
|
||||
}
|
||||
|
||||
override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List<FindroidSource> =
|
||||
override suspend fun getMediaSources(
|
||||
itemId: UUID,
|
||||
includePath: Boolean,
|
||||
): List<FindroidSource> =
|
||||
withContext(Dispatchers.IO) {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -188,7 +210,11 @@ class JellyfinRepositoryOfflineImpl(
|
|||
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) {
|
||||
try {
|
||||
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 postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int) {
|
||||
override suspend fun postPlaybackStop(
|
||||
itemId: UUID,
|
||||
positionTicks: Long,
|
||||
playedPercentage: Int,
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
when {
|
||||
playedPercentage < 10 -> {
|
||||
|
@ -262,35 +292,31 @@ class JellyfinRepositoryOfflineImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getBaseUrl(): String {
|
||||
return ""
|
||||
}
|
||||
override fun getBaseUrl(): String = ""
|
||||
|
||||
override suspend fun updateDeviceName(name: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getUserConfiguration(): UserConfiguration? {
|
||||
return null
|
||||
}
|
||||
override suspend fun getUserConfiguration(): UserConfiguration? = null
|
||||
|
||||
override suspend fun getDownloads(): List<FindroidItem> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val items = mutableListOf<FindroidItem>()
|
||||
items.addAll(
|
||||
database.getMoviesByServerId(appPreferences.currentServer!!)
|
||||
database
|
||||
.getMoviesByServerId(appPreferences.currentServer!!)
|
||||
.map { it.toFindroidMovie(database, jellyfinApi.userId!!) },
|
||||
)
|
||||
items.addAll(
|
||||
database.getShowsByServerId(appPreferences.currentServer!!)
|
||||
database
|
||||
.getShowsByServerId(appPreferences.currentServer!!)
|
||||
.map { it.toFindroidShow(database, jellyfinApi.userId!!) },
|
||||
)
|
||||
items
|
||||
}
|
||||
|
||||
override fun getUserId(): UUID {
|
||||
return jellyfinApi.userId!!
|
||||
}
|
||||
override fun getUserId(): UUID = jellyfinApi.userId!!
|
||||
|
||||
override suspend fun getDeviceId(): String {
|
||||
TODO("Not yet implemented")
|
||||
|
@ -299,7 +325,7 @@ class JellyfinRepositoryOfflineImpl(
|
|||
override suspend fun buildDeviceProfile(
|
||||
maxBitrate: Int,
|
||||
container: String,
|
||||
context: EncodingContext
|
||||
context: EncodingContext,
|
||||
): DeviceProfile {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -310,7 +336,8 @@ class JellyfinRepositoryOfflineImpl(
|
|||
mediaSourceId: String,
|
||||
playSessionId: String,
|
||||
videoBitrate: Int,
|
||||
container: String
|
||||
maxHeight: Int,
|
||||
container: String,
|
||||
): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -320,7 +347,7 @@ class JellyfinRepositoryOfflineImpl(
|
|||
deviceId: String,
|
||||
mediaSourceId: String,
|
||||
playSessionId: String,
|
||||
videoBitrate: Int
|
||||
videoBitrate: Int,
|
||||
): String {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -329,7 +356,7 @@ class JellyfinRepositoryOfflineImpl(
|
|||
itemId: UUID,
|
||||
enableDirectStream: Boolean,
|
||||
deviceProfile: DeviceProfile,
|
||||
maxBitrate: Int
|
||||
maxBitrate: Int,
|
||||
): Response<PlaybackInfoResponse> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
@ -337,8 +364,4 @@ class JellyfinRepositoryOfflineImpl(
|
|||
override suspend fun stopEncodingProcess(playSessionId: String) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue