feat: Transcoding Download / code: Cleanup
Some checks failed
Build / Assemble (push) Has been cancelled
Some checks failed
Build / Assemble (push) Has been cancelled
This commit is contained in:
parent
afbf68937d
commit
362201eddf
14 changed files with 694 additions and 304 deletions
|
@ -157,11 +157,20 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.itemActions.downloadButton.setOnClickListener {
|
binding.itemActions.downloadButton.setOnClickListener {
|
||||||
|
handleDownload()
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDownload() {
|
||||||
if (viewModel.item.isDownloaded()) {
|
if (viewModel.item.isDownloaded()) {
|
||||||
viewModel.deleteEpisode()
|
viewModel.deleteEpisode()
|
||||||
binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download)
|
binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download)
|
||||||
} else if (viewModel.item.isDownloading()) {
|
} else if (viewModel.item.isDownloading()) {
|
||||||
createCancelDialog()
|
createCancelDialog()
|
||||||
|
}else if (!appPreferences.downloadQualityDefault) {
|
||||||
|
createPickQualityDialog()
|
||||||
} else {
|
} else {
|
||||||
binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent)
|
binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent)
|
||||||
binding.itemActions.progressDownload.isIndeterminate = true
|
binding.itemActions.progressDownload.isIndeterminate = true
|
||||||
|
@ -195,7 +204,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
storageDialog.show()
|
storageDialog.show()
|
||||||
return@setOnClickListener
|
return
|
||||||
}
|
}
|
||||||
if (viewModel.item.sources.size > 1) {
|
if (viewModel.item.sources.size > 1) {
|
||||||
val dialog = getVideoVersionDialog(
|
val dialog = getVideoVersionDialog(
|
||||||
|
@ -211,16 +220,13 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
return@setOnClickListener
|
return
|
||||||
}
|
}
|
||||||
createDownloadPreparingDialog()
|
createDownloadPreparingDialog()
|
||||||
viewModel.download()
|
viewModel.download()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
dialog?.let {
|
dialog?.let {
|
||||||
val sheet = it as BottomSheetDialog
|
val sheet = it as BottomSheetDialog
|
||||||
|
@ -402,6 +408,31 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createPickQualityDialog() {
|
||||||
|
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries)
|
||||||
|
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values)
|
||||||
|
val quality = appPreferences.downloadQuality
|
||||||
|
val currentQualityIndex = qualityValues.indexOf(quality)
|
||||||
|
var selectedQuality = quality
|
||||||
|
|
||||||
|
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
builder.setTitle("Download Quality")
|
||||||
|
builder.setSingleChoiceItems(qualityEntries, currentQualityIndex) { _, which ->
|
||||||
|
selectedQuality = qualityValues[which]
|
||||||
|
}
|
||||||
|
builder.setPositiveButton("Download") { dialog, _ ->
|
||||||
|
appPreferences.downloadQuality = selectedQuality
|
||||||
|
dialog.dismiss()
|
||||||
|
handleDownload()
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun navigateToPlayerActivity(
|
private fun navigateToPlayerActivity(
|
||||||
playerItems: Array<PlayerItem>,
|
playerItems: Array<PlayerItem>,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -192,11 +192,22 @@ class MovieFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.itemActions.downloadButton.setOnClickListener {
|
binding.itemActions.downloadButton.setOnClickListener {
|
||||||
|
handleDownload()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.peopleRecyclerView.adapter = PersonListAdapter { person ->
|
||||||
|
navigateToPersonDetail(person.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDownload() {
|
||||||
if (viewModel.item.isDownloaded()) {
|
if (viewModel.item.isDownloaded()) {
|
||||||
viewModel.deleteItem()
|
viewModel.deleteItem()
|
||||||
binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download)
|
binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download)
|
||||||
} else if (viewModel.item.isDownloading()) {
|
} else if (viewModel.item.isDownloading()) {
|
||||||
createCancelDialog()
|
createCancelDialog()
|
||||||
|
} else if (!appPreferences.downloadQualityDefault) {
|
||||||
|
createPickQualityDialog()
|
||||||
} else {
|
} else {
|
||||||
binding.itemActions.downloadButton.setIconResource(android.R.color.transparent)
|
binding.itemActions.downloadButton.setIconResource(android.R.color.transparent)
|
||||||
binding.itemActions.progressDownload.isIndeterminate = true
|
binding.itemActions.progressDownload.isIndeterminate = true
|
||||||
|
@ -230,7 +241,7 @@ class MovieFragment : Fragment() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
storageDialog.show()
|
storageDialog.show()
|
||||||
return@setOnClickListener
|
return
|
||||||
}
|
}
|
||||||
if (viewModel.item.sources.size > 1) {
|
if (viewModel.item.sources.size > 1) {
|
||||||
val dialog = getVideoVersionDialog(
|
val dialog = getVideoVersionDialog(
|
||||||
|
@ -246,18 +257,13 @@ class MovieFragment : Fragment() {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
dialog.show()
|
dialog.show()
|
||||||
return@setOnClickListener
|
return
|
||||||
}
|
}
|
||||||
createDownloadPreparingDialog()
|
createDownloadPreparingDialog()
|
||||||
viewModel.download()
|
viewModel.download()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.peopleRecyclerView.adapter = PersonListAdapter { person ->
|
|
||||||
navigateToPersonDetail(person.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
@ -495,6 +501,31 @@ class MovieFragment : Fragment() {
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createPickQualityDialog() {
|
||||||
|
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries)
|
||||||
|
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values)
|
||||||
|
val quality = appPreferences.downloadQuality
|
||||||
|
val currentQualityIndex = qualityValues.indexOf(quality)
|
||||||
|
var selectedQuality = quality
|
||||||
|
|
||||||
|
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
builder.setTitle("Download Quality")
|
||||||
|
builder.setSingleChoiceItems(qualityEntries, currentQualityIndex) { _, which ->
|
||||||
|
selectedQuality = qualityValues[which]
|
||||||
|
}
|
||||||
|
builder.setPositiveButton("Download") { dialog, _ ->
|
||||||
|
appPreferences.downloadQuality = selectedQuality
|
||||||
|
dialog.dismiss()
|
||||||
|
handleDownload()
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun navigateToPlayerActivity(
|
private fun navigateToPlayerActivity(
|
||||||
playerItems: Array<PlayerItem>,
|
playerItems: Array<PlayerItem>,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -31,7 +31,9 @@ import timber.log.Timber
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.nomadics9.ananas.AppPreferences
|
||||||
import com.nomadics9.ananas.models.UiText
|
import com.nomadics9.ananas.models.UiText
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -44,6 +46,9 @@ class SeasonFragment : Fragment() {
|
||||||
private lateinit var errorDialog: ErrorDialogFragment
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
private lateinit var downloadPreparingDialog: AlertDialog
|
private lateinit var downloadPreparingDialog: AlertDialog
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appPreferences: AppPreferences
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -195,6 +200,15 @@ class SeasonFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createEpisodesToDownloadDialog(storageIndex: Int = 0) {
|
private fun createEpisodesToDownloadDialog(storageIndex: Int = 0) {
|
||||||
|
if (!appPreferences.downloadQualityDefault)
|
||||||
|
createPickQualityDialog {
|
||||||
|
showDownloadDialog(storageIndex)
|
||||||
|
} else {
|
||||||
|
showDownloadDialog(storageIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showDownloadDialog(storageIndex: Int = 0) {
|
||||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
val dialog = builder
|
val dialog = builder
|
||||||
.setTitle(com.nomadics9.ananas.core.R.string.download_season_dialog_title)
|
.setTitle(com.nomadics9.ananas.core.R.string.download_season_dialog_title)
|
||||||
|
@ -211,6 +225,33 @@ class SeasonFragment : Fragment() {
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createPickQualityDialog(onQualitySelected: () -> Unit) {
|
||||||
|
val qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_entries)
|
||||||
|
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.quality_values)
|
||||||
|
val quality = appPreferences.downloadQuality
|
||||||
|
val currentQualityIndex = qualityValues.indexOf(quality)
|
||||||
|
|
||||||
|
var selectedQuality = quality
|
||||||
|
|
||||||
|
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
builder.setTitle("Download Quality")
|
||||||
|
builder.setSingleChoiceItems(qualityEntries, currentQualityIndex) { _, which ->
|
||||||
|
selectedQuality = qualityValues[which]
|
||||||
|
}
|
||||||
|
builder.setPositiveButton("Download") { dialog, _ ->
|
||||||
|
appPreferences.downloadQuality = selectedQuality
|
||||||
|
dialog.dismiss()
|
||||||
|
onQualitySelected()
|
||||||
|
}
|
||||||
|
builder.setNegativeButton("Cancel") { dialog, _ ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Select Quality"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnQuality1080"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="1080p" />
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnQuality720"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="720p"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnQuality480"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="480p"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnQuality360"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="360p"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
|
@ -6,6 +6,7 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import com.nomadics9.ananas.AppPreferences
|
import com.nomadics9.ananas.AppPreferences
|
||||||
|
import com.nomadics9.ananas.api.JellyfinApi
|
||||||
import com.nomadics9.ananas.database.ServerDatabaseDao
|
import com.nomadics9.ananas.database.ServerDatabaseDao
|
||||||
import com.nomadics9.ananas.repository.JellyfinRepository
|
import com.nomadics9.ananas.repository.JellyfinRepository
|
||||||
import com.nomadics9.ananas.utils.Downloader
|
import com.nomadics9.ananas.utils.Downloader
|
||||||
|
|
|
@ -6,12 +6,15 @@ import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.StatFs
|
import android.os.StatFs
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.nomadics9.ananas.AppPreferences
|
import com.nomadics9.ananas.AppPreferences
|
||||||
|
import com.nomadics9.ananas.api.JellyfinApi
|
||||||
import com.nomadics9.ananas.database.ServerDatabaseDao
|
import com.nomadics9.ananas.database.ServerDatabaseDao
|
||||||
import com.nomadics9.ananas.models.FindroidEpisode
|
import com.nomadics9.ananas.models.FindroidEpisode
|
||||||
import com.nomadics9.ananas.models.FindroidItem
|
import com.nomadics9.ananas.models.FindroidItem
|
||||||
import com.nomadics9.ananas.models.FindroidMovie
|
import com.nomadics9.ananas.models.FindroidMovie
|
||||||
|
import com.nomadics9.ananas.models.FindroidSegment
|
||||||
import com.nomadics9.ananas.models.FindroidSource
|
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
|
||||||
|
@ -26,7 +29,34 @@ import com.nomadics9.ananas.models.toFindroidSourceDto
|
||||||
import com.nomadics9.ananas.models.toFindroidTrickplayInfoDto
|
import com.nomadics9.ananas.models.toFindroidTrickplayInfoDto
|
||||||
import com.nomadics9.ananas.models.toFindroidUserDataDto
|
import com.nomadics9.ananas.models.toFindroidUserDataDto
|
||||||
import com.nomadics9.ananas.repository.JellyfinRepository
|
import com.nomadics9.ananas.repository.JellyfinRepository
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.dynamicHlsApi
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.videosApi
|
||||||
|
import org.jellyfin.sdk.model.api.ClientCapabilitiesDto
|
||||||
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
|
import org.jellyfin.sdk.model.api.DirectPlayProfile
|
||||||
|
import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||||
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||||
|
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||||
|
import org.jellyfin.sdk.model.api.ProfileCondition
|
||||||
|
import org.jellyfin.sdk.model.api.ProfileConditionType
|
||||||
|
import org.jellyfin.sdk.model.api.ProfileConditionValue
|
||||||
|
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||||
|
import org.jellyfin.sdk.model.api.SubtitleProfile
|
||||||
|
import org.jellyfin.sdk.model.api.TranscodeSeekInfo
|
||||||
|
import org.jellyfin.sdk.model.api.TranscodingProfile
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.net.URL
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.Exception
|
import kotlin.Exception
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
@ -46,7 +76,11 @@ class DownloaderImpl(
|
||||||
storageIndex: Int,
|
storageIndex: Int,
|
||||||
): Pair<Long, UiText?> {
|
): Pair<Long, UiText?> {
|
||||||
try {
|
try {
|
||||||
val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId }
|
|
||||||
|
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 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)
|
||||||
|
@ -70,6 +104,41 @@ class DownloaderImpl(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val qualityPreference = appPreferences.downloadQuality!!
|
||||||
|
Timber.d("Quality preference: $qualityPreference")
|
||||||
|
return if (qualityPreference != "Original") {
|
||||||
|
Timber.d("Handling Transcoding download for item: ${item.id}")
|
||||||
|
handleTranscodeDownload(item, source, storageIndex, trickplayInfo, segments, path, qualityPreference)
|
||||||
|
} else {
|
||||||
|
Timber.d("Handling original download for item: ${item.id}")
|
||||||
|
downloadOriginalItem(item, source, storageIndex, trickplayInfo, segments, path)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
try {
|
||||||
|
val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId }
|
||||||
|
deleteItem(item, source)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair(
|
||||||
|
-1,
|
||||||
|
if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(
|
||||||
|
CoreR.string.unknown_error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleTranscodeDownload(
|
||||||
|
item: FindroidItem,
|
||||||
|
source: FindroidSource,
|
||||||
|
storageIndex: Int,
|
||||||
|
trickplayInfo: FindroidTrickplayInfo?,
|
||||||
|
segments: List<FindroidSegment>?,
|
||||||
|
path: Uri,
|
||||||
|
quality: String
|
||||||
|
): Pair<Long, UiText?> {
|
||||||
|
val transcodingUrl = getTranscodedUrl(item.id, quality)
|
||||||
when (item) {
|
when (item) {
|
||||||
is FindroidMovie -> {
|
is FindroidMovie -> {
|
||||||
database.insertMovie(item.toFindroidMovieDto(appPreferences.currentServer!!))
|
database.insertMovie(item.toFindroidMovieDto(appPreferences.currentServer!!))
|
||||||
|
@ -77,7 +146,70 @@ class DownloaderImpl(
|
||||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||||
downloadExternalMediaStreams(item, source, storageIndex)
|
downloadExternalMediaStreams(item, source, storageIndex)
|
||||||
if (trickplayInfo != null) {
|
if (trickplayInfo != null) {
|
||||||
downloadTrickplayData(item.id, sourceId, trickplayInfo)
|
downloadTrickplayData(item.id, source.id, trickplayInfo)
|
||||||
|
}
|
||||||
|
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 downloadId = downloadManager.enqueue(request)
|
||||||
|
database.setSourceDownloadId(source.id, downloadId)
|
||||||
|
return Pair(downloadId, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
is FindroidEpisode -> {
|
||||||
|
database.insertShow(
|
||||||
|
jellyfinRepository.getShow(item.seriesId)
|
||||||
|
.toFindroidShowDto(appPreferences.currentServer!!),
|
||||||
|
)
|
||||||
|
database.insertSeason(
|
||||||
|
jellyfinRepository.getSeason(item.seasonId).toFindroidSeasonDto(),
|
||||||
|
)
|
||||||
|
database.insertEpisode(item.toFindroidEpisodeDto(appPreferences.currentServer!!))
|
||||||
|
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||||
|
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||||
|
downloadExternalMediaStreams(item, source, storageIndex)
|
||||||
|
if (trickplayInfo != null) {
|
||||||
|
downloadTrickplayData(item.id, source.id, trickplayInfo)
|
||||||
|
}
|
||||||
|
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 downloadId = downloadManager.enqueue(request)
|
||||||
|
database.setSourceDownloadId(source.id, downloadId)
|
||||||
|
return Pair(downloadId, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Pair(-1, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun downloadOriginalItem(
|
||||||
|
item: FindroidItem,
|
||||||
|
source: FindroidSource,
|
||||||
|
storageIndex: Int,
|
||||||
|
trickplayInfo: FindroidTrickplayInfo?,
|
||||||
|
segments: List<FindroidSegment>?,
|
||||||
|
path: Uri,
|
||||||
|
): Pair<Long, UiText?> {
|
||||||
|
when (item) {
|
||||||
|
is FindroidMovie -> {
|
||||||
|
database.insertMovie(item.toFindroidMovieDto(appPreferences.currentServer!!))
|
||||||
|
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
|
||||||
|
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||||
|
downloadExternalMediaStreams(item, source, storageIndex)
|
||||||
|
if (trickplayInfo != null) {
|
||||||
|
downloadTrickplayData(item.id, source.id, trickplayInfo)
|
||||||
}
|
}
|
||||||
if (segments != null) {
|
if (segments != null) {
|
||||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||||
|
@ -106,7 +238,7 @@ class DownloaderImpl(
|
||||||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||||
downloadExternalMediaStreams(item, source, storageIndex)
|
downloadExternalMediaStreams(item, source, storageIndex)
|
||||||
if (trickplayInfo != null) {
|
if (trickplayInfo != null) {
|
||||||
downloadTrickplayData(item.id, sourceId, trickplayInfo)
|
downloadTrickplayData(item.id, source.id, trickplayInfo)
|
||||||
}
|
}
|
||||||
if (segments != null) {
|
if (segments != null) {
|
||||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||||
|
@ -123,15 +255,8 @@ class DownloaderImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Pair(-1, null)
|
return Pair(-1, null)
|
||||||
} catch (e: Exception) {
|
}
|
||||||
try {
|
|
||||||
val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId }
|
|
||||||
deleteItem(item, source)
|
|
||||||
} catch (_: Exception) {}
|
|
||||||
|
|
||||||
return Pair(-1, if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(CoreR.string.unknown_error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) {
|
override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) {
|
||||||
if (source.downloadId != null) {
|
if (source.downloadId != null) {
|
||||||
|
@ -193,9 +318,11 @@ class DownloaderImpl(
|
||||||
)
|
)
|
||||||
when (downloadStatus) {
|
when (downloadStatus) {
|
||||||
DownloadManager.STATUS_RUNNING -> {
|
DownloadManager.STATUS_RUNNING -> {
|
||||||
val totalBytes = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
val totalBytes =
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||||
if (totalBytes > 0) {
|
if (totalBytes > 0) {
|
||||||
val downloadedBytes = cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
val downloadedBytes =
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||||
progress = downloadedBytes.times(100).div(totalBytes).toInt()
|
progress = downloadedBytes.times(100).div(totalBytes).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,8 +344,19 @@ 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(File(storageLocation, "downloads/${item.id}.${source.id}.$id.download"))
|
val streamPath = Uri.fromFile(
|
||||||
database.insertMediaStream(mediaStream.toFindroidMediaStreamDto(id, source.id, streamPath.path.orEmpty()))
|
File(
|
||||||
|
storageLocation,
|
||||||
|
"downloads/${item.id}.${source.id}.$id.download"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
database.insertMediaStream(
|
||||||
|
mediaStream.toFindroidMediaStreamDto(
|
||||||
|
id,
|
||||||
|
source.id,
|
||||||
|
streamPath.path.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
val request = DownloadManager.Request(Uri.parse(mediaStream.path))
|
val request = DownloadManager.Request(Uri.parse(mediaStream.path))
|
||||||
.setTitle(mediaStream.title)
|
.setTitle(mediaStream.title)
|
||||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||||
|
@ -235,7 +373,10 @@ class DownloaderImpl(
|
||||||
sourceId: String,
|
sourceId: String,
|
||||||
trickplayInfo: FindroidTrickplayInfo,
|
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>()
|
val byteArrays = mutableListOf<ByteArray>()
|
||||||
for (i in 0..maxIndex) {
|
for (i in 0..maxIndex) {
|
||||||
jellyfinRepository.getTrickplayData(
|
jellyfinRepository.getTrickplayData(
|
||||||
|
@ -263,4 +404,52 @@ class DownloaderImpl(
|
||||||
file.writeBytes(byteArray)
|
file.writeBytes(byteArray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 playSessionId = playbackInfo.content.playSessionId!!
|
||||||
|
val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, mediaSourceId, playSessionId, maxBitrate, "ts")
|
||||||
|
|
||||||
|
val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality)
|
||||||
|
Timber.d("Constructed Transcode URL: $transcodeUri")
|
||||||
|
transcodeUri
|
||||||
|
} 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -25,4 +25,16 @@
|
||||||
<item>audiotrack</item>
|
<item>audiotrack</item>
|
||||||
<item>opensles</item>
|
<item>opensles</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="quality_entries">
|
||||||
|
<item>Original</item>
|
||||||
|
<item>720p - 2Mbps</item>
|
||||||
|
<item>480p - 1Mbps</item>
|
||||||
|
<item>360p - 800Kbps</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="quality_values">
|
||||||
|
<item>Original</item>
|
||||||
|
<item>720p</item>
|
||||||
|
<item>480p</item>
|
||||||
|
<item>360p</item>
|
||||||
|
</string-array>
|
||||||
</resources>
|
</resources>
|
|
@ -9,4 +9,17 @@
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
app:key="pref_downloads_roaming"
|
app:key="pref_downloads_roaming"
|
||||||
app:title="@string/download_roaming" />
|
app:title="@string/download_roaming" />
|
||||||
|
<ListPreference
|
||||||
|
android:key="pref_downloads_quality"
|
||||||
|
android:title="Download Quality"
|
||||||
|
android:defaultValue="Original"
|
||||||
|
android:entries="@array/quality_entries"
|
||||||
|
android:entryValues="@array/quality_values"
|
||||||
|
android:summary="%s" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
app:key="pref_downloads_quality_default"
|
||||||
|
app:summary="Default to picked Download Quality" />
|
||||||
|
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
|
@ -11,9 +11,14 @@ 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 kotlinx.coroutines.flow.Flow
|
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.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
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
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
import org.jellyfin.sdk.model.api.PlaybackInfoResponse
|
||||||
import org.jellyfin.sdk.model.api.PublicSystemInfo
|
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.UserConfiguration
|
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||||
|
@ -113,5 +118,15 @@ interface JellyfinRepository {
|
||||||
|
|
||||||
fun getUserId(): UUID
|
fun getUserId(): UUID
|
||||||
|
|
||||||
fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?>
|
suspend fun getDeviceId(): String
|
||||||
|
|
||||||
|
suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int>
|
||||||
|
|
||||||
|
suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile
|
||||||
|
|
||||||
|
suspend fun getVideoStreambyContainerUrl(itemId: UUID, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String
|
||||||
|
|
||||||
|
suspend fun getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response<PlaybackInfoResponse>
|
||||||
|
|
||||||
|
suspend fun stopEncodingProcess(playSessionId: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,23 +29,35 @@ import io.ktor.util.toByteArray
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.Response
|
||||||
import org.jellyfin.sdk.api.client.extensions.get
|
import org.jellyfin.sdk.api.client.extensions.get
|
||||||
|
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.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
|
||||||
import org.jellyfin.sdk.model.api.DlnaProfileType
|
import org.jellyfin.sdk.model.api.DlnaProfileType
|
||||||
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
import org.jellyfin.sdk.model.api.GeneralCommandType
|
import org.jellyfin.sdk.model.api.GeneralCommandType
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
import org.jellyfin.sdk.model.api.ItemFilter
|
import org.jellyfin.sdk.model.api.ItemFilter
|
||||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||||
|
import org.jellyfin.sdk.model.api.PlaybackInfoResponse
|
||||||
|
import org.jellyfin.sdk.model.api.ProfileCondition
|
||||||
|
import org.jellyfin.sdk.model.api.ProfileConditionType
|
||||||
|
import org.jellyfin.sdk.model.api.ProfileConditionValue
|
||||||
import org.jellyfin.sdk.model.api.PublicSystemInfo
|
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.TranscodeSeekInfo
|
||||||
|
import org.jellyfin.sdk.model.api.TranscodingProfile
|
||||||
import org.jellyfin.sdk.model.api.UserConfiguration
|
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -566,13 +578,117 @@ class JellyfinRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?> {
|
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
|
||||||
return when (transcodeResolution) {
|
return when (transcodeResolution) {
|
||||||
1080 -> 8000000 to 384000 // Adjusted for 1080p
|
1080 -> 8000000 to 384000 // Adjusted for 1080p
|
||||||
720 -> 2000000 to 384000 // Adjusted for 720p
|
720 -> 2000000 to 384000 // Adjusted for 720p
|
||||||
480 -> 1000000 to 384000 // Adjusted for 480p
|
480 -> 1000000 to 384000 // Adjusted for 480p
|
||||||
360 -> 800000 to 128000 // Adjusted for 360p
|
360 -> 800000 to 128000 // Adjusted for 360p
|
||||||
else -> null to null
|
else -> 8000000 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return playbackInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getVideoStreambyContainerUrl(itemId: UUID, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String {
|
||||||
|
val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl(
|
||||||
|
itemId,
|
||||||
|
static = false,
|
||||||
|
mediaSourceId = mediaSourceId,
|
||||||
|
playSessionId = playSessionId,
|
||||||
|
videoBitRate = videoBitrate,
|
||||||
|
audioBitRate = 384000,
|
||||||
|
videoCodec = "hevc",
|
||||||
|
audioCodec = "aac,ac3,eac3",
|
||||||
|
container = container,
|
||||||
|
startTimeTicks = 0,
|
||||||
|
copyTimestamps = true,
|
||||||
|
)
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDeviceId(): String {
|
||||||
|
val deviceId = jellyfinApi.devicesApi.getDevices(getUserId())
|
||||||
|
return deviceId.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun stopEncodingProcess(playSessionId: String) {
|
||||||
|
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
|
||||||
|
deviceId = getDeviceId(),
|
||||||
|
playSessionId = playSessionId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,13 @@ import com.nomadics9.ananas.models.toFindroidSource
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
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.DeviceProfile
|
||||||
|
import org.jellyfin.sdk.model.api.EncodingContext
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
|
import org.jellyfin.sdk.model.api.PlaybackInfoResponse
|
||||||
import org.jellyfin.sdk.model.api.PublicSystemInfo
|
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.UserConfiguration
|
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||||
|
@ -286,7 +290,42 @@ class JellyfinRepositoryOfflineImpl(
|
||||||
return jellyfinApi.userId!!
|
return jellyfinApi.userId!!
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int?, Int?> {
|
override suspend fun getDeviceId(): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun buildDeviceProfile(
|
||||||
|
maxBitrate: Int,
|
||||||
|
container: String,
|
||||||
|
context: EncodingContext
|
||||||
|
): DeviceProfile {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getVideoStreambyContainerUrl(
|
||||||
|
itemId: UUID,
|
||||||
|
mediaSourceId: String,
|
||||||
|
playSessionId: String,
|
||||||
|
videoBitrate: Int,
|
||||||
|
container: String
|
||||||
|
): String {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPostedPlaybackInfo(
|
||||||
|
itemId: UUID,
|
||||||
|
enableDirectStream: Boolean,
|
||||||
|
deviceProfile: DeviceProfile,
|
||||||
|
maxBitrate: Int
|
||||||
|
): Response<PlaybackInfoResponse> {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun stopEncodingProcess(playSessionId: String) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair<Int, Int> {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,6 @@ class PlayerActivityViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val jellyfinRepository: JellyfinRepository,
|
private val jellyfinRepository: JellyfinRepository,
|
||||||
private val jellyfinApi: JellyfinApi,
|
|
||||||
private val appPreferences: AppPreferences,
|
private val appPreferences: AppPreferences,
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
) : ViewModel(), Player.Listener {
|
) : ViewModel(), Player.Listener {
|
||||||
|
@ -547,77 +546,11 @@ constructor(
|
||||||
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
|
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
|
||||||
transcodingResolution
|
transcodingResolution
|
||||||
)
|
)
|
||||||
val deviceProfile = ClientCapabilitiesDto(
|
val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "ts", EncodingContext.STREAMING)
|
||||||
supportedCommands = emptyList(),
|
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,true,deviceProfile,videoBitRate)
|
||||||
playableMediaTypes = emptyList(),
|
|
||||||
supportsMediaControl = true,
|
|
||||||
supportsPersistentIdentifier = true,
|
|
||||||
deviceProfile = DeviceProfile(
|
|
||||||
name = "AnanasUser",
|
|
||||||
id = jellyfinRepository.getUserId().toString(),
|
|
||||||
maxStaticBitrate = videoBitRate,
|
|
||||||
maxStreamingBitrate = videoBitRate,
|
|
||||||
codecProfiles = emptyList(),
|
|
||||||
containerProfiles = listOf(),
|
|
||||||
directPlayProfiles = listOf(
|
|
||||||
DirectPlayProfile(type = DlnaProfileType.VIDEO),
|
|
||||||
DirectPlayProfile(type = DlnaProfileType.AUDIO),
|
|
||||||
),
|
|
||||||
transcodingProfiles = listOf(
|
|
||||||
TranscodingProfile(
|
|
||||||
container = "ts",
|
|
||||||
context = EncodingContext.STREAMING,
|
|
||||||
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)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val playbackInfo =
|
|
||||||
jellyfinApi.mediaInfoApi.getPostedPlaybackInfo(
|
|
||||||
itemId,
|
|
||||||
PlaybackInfoDto(
|
|
||||||
userId = jellyfinApi.userId!!,
|
|
||||||
enableTranscoding = true,
|
|
||||||
enableDirectPlay = false,
|
|
||||||
enableDirectStream = true,
|
|
||||||
autoOpenLiveStream = true,
|
|
||||||
deviceProfile = deviceProfile.deviceProfile,
|
|
||||||
maxStreamingBitrate = videoBitRate,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val playSessionId = playbackInfo.content.playSessionId
|
val playSessionId = playbackInfo.content.playSessionId
|
||||||
val getDeviceId =
|
|
||||||
jellyfinApi.devicesApi.getDeviceOptions(jellyfinApi.userId.toString())
|
|
||||||
val deviceId = getDeviceId.content.deviceId
|
|
||||||
if (playSessionId != null) {
|
if (playSessionId != null) {
|
||||||
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
|
jellyfinRepository.stopEncodingProcess(playSessionId)
|
||||||
deviceId,
|
|
||||||
playSessionId
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
val mediaSource = playbackInfo.content.mediaSources.firstOrNull()
|
val mediaSource = playbackInfo.content.mediaSources.firstOrNull()
|
||||||
if (mediaSource == null) {
|
if (mediaSource == null) {
|
||||||
|
@ -632,8 +565,8 @@ constructor(
|
||||||
.setMimeType(externalSubtitle.mimeType)
|
.setMimeType(externalSubtitle.mimeType)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
// Timber.tag("MediaStreams").d("Media Streams: %s", mediaSource?.mediaStreams)
|
|
||||||
//TODO: Embedded sub support
|
// TODO: Embedded sub support
|
||||||
// val embeddedSubtitles = mediaSource?.mediaStreams
|
// val embeddedSubtitles = mediaSource?.mediaStreams
|
||||||
// ?.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal }
|
// ?.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal }
|
||||||
// ?.map { mediaStream ->
|
// ?.map { mediaStream ->
|
||||||
|
@ -651,18 +584,11 @@ constructor(
|
||||||
// .build()
|
// .build()
|
||||||
// }
|
// }
|
||||||
// ?.toMutableList() ?: mutableListOf()
|
// ?.toMutableList() ?: mutableListOf()
|
||||||
|
// val allSubtitles = embeddedSubtitles.apply { addAll(mediaSubtitles) }
|
||||||
|
|
||||||
// val allSubtitles = embeddedSubtitles.apply { addAll(mediaSubtitles) }
|
val baseUrl = jellyfinRepository.getBaseUrl()
|
||||||
val baseUrl = jellyfinApi.api.baseUrl
|
val cleanBaseUrl = baseUrl.removePrefix("http://").removePrefix("https://")
|
||||||
val cleanBaseUrl = baseUrl?.removePrefix("http://")?.removePrefix("https://")
|
val staticUrl = jellyfinRepository.getStreamUrl(itemId, currentItem.mediaSourceId)
|
||||||
val staticUrl = jellyfinApi.videosApi.getVideoStreamUrl(
|
|
||||||
itemId,
|
|
||||||
static = true,
|
|
||||||
playSessionId = playSessionId,
|
|
||||||
deviceId = deviceId,
|
|
||||||
mediaSourceId = currentItem.mediaSourceId,
|
|
||||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
val uri =
|
val uri =
|
||||||
|
@ -719,6 +645,7 @@ constructor(
|
||||||
uriBuilder.setOrReplaceQueryParameter("VideoBitrate", videoBitRate.toString())
|
uriBuilder.setOrReplaceQueryParameter("VideoBitrate", videoBitRate.toString())
|
||||||
uriBuilder.setOrReplaceQueryParameter("AudioBitrate", audioBitRate.toString())
|
uriBuilder.setOrReplaceQueryParameter("AudioBitrate", audioBitRate.toString())
|
||||||
uriBuilder.setOrReplaceQueryParameter("Static", "false")
|
uriBuilder.setOrReplaceQueryParameter("Static", "false")
|
||||||
|
uriBuilder.appendQueryParameter("PlaySessionId", playSessionId)
|
||||||
uriBuilder.appendQueryParameter(
|
uriBuilder.appendQueryParameter(
|
||||||
"MaxVideoHeight",
|
"MaxVideoHeight",
|
||||||
transcodingResolution.toString()
|
transcodingResolution.toString()
|
||||||
|
|
|
@ -141,6 +141,18 @@ constructor(
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var downloadQuality get() = sharedPreferences.getString(
|
||||||
|
Constants.PREF_DOWNLOADS_QUALITY,
|
||||||
|
"Original")
|
||||||
|
set(value) {
|
||||||
|
sharedPreferences.edit().putString(Constants.PREF_DOWNLOADS_QUALITY, value).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadQualityDefault get() = sharedPreferences.getBoolean(
|
||||||
|
Constants.PREF_DOWNLOADS_QUALITY_DEFAULT,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
var sortBy: String
|
var sortBy: String
|
||||||
get() = sharedPreferences.getString(
|
get() = sharedPreferences.getString(
|
||||||
|
|
|
@ -45,6 +45,8 @@ object Constants {
|
||||||
const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout"
|
const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout"
|
||||||
const val PREF_DOWNLOADS_MOBILE_DATA = "pref_downloads_mobile_data"
|
const val PREF_DOWNLOADS_MOBILE_DATA = "pref_downloads_mobile_data"
|
||||||
const val PREF_DOWNLOADS_ROAMING = "pref_downloads_roaming"
|
const val PREF_DOWNLOADS_ROAMING = "pref_downloads_roaming"
|
||||||
|
const val PREF_DOWNLOADS_QUALITY = "pref_downloads_quality"
|
||||||
|
const val PREF_DOWNLOADS_QUALITY_DEFAULT = "pref_downloads_quality_default"
|
||||||
const val PREF_SORT_BY = "pref_sort_by"
|
const val PREF_SORT_BY = "pref_sort_by"
|
||||||
const val PREF_SORT_ORDER = "pref_sort_order"
|
const val PREF_SORT_ORDER = "pref_sort_order"
|
||||||
const val PREF_DISPLAY_EXTRA_INFO = "pref_display_extra_info"
|
const val PREF_DISPLAY_EXTRA_INFO = "pref_display_extra_info"
|
||||||
|
|
Loading…
Reference in a new issue