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 {
|
||||
handleDownload()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun handleDownload() {
|
||||
if (viewModel.item.isDownloaded()) {
|
||||
viewModel.deleteEpisode()
|
||||
binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download)
|
||||
} else if (viewModel.item.isDownloading()) {
|
||||
createCancelDialog()
|
||||
}else if (!appPreferences.downloadQualityDefault) {
|
||||
createPickQualityDialog()
|
||||
} else {
|
||||
binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent)
|
||||
binding.itemActions.progressDownload.isIndeterminate = true
|
||||
|
@ -195,7 +204,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
},
|
||||
)
|
||||
storageDialog.show()
|
||||
return@setOnClickListener
|
||||
return
|
||||
}
|
||||
if (viewModel.item.sources.size > 1) {
|
||||
val dialog = getVideoVersionDialog(
|
||||
|
@ -211,16 +220,13 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
},
|
||||
)
|
||||
dialog.show()
|
||||
return@setOnClickListener
|
||||
return
|
||||
}
|
||||
createDownloadPreparingDialog()
|
||||
viewModel.download()
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
dialog?.let {
|
||||
val sheet = it as BottomSheetDialog
|
||||
|
@ -402,6 +408,31 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
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(
|
||||
playerItems: Array<PlayerItem>,
|
||||
) {
|
||||
|
|
|
@ -192,11 +192,22 @@ class MovieFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding.itemActions.downloadButton.setOnClickListener {
|
||||
handleDownload()
|
||||
}
|
||||
|
||||
binding.peopleRecyclerView.adapter = PersonListAdapter { person ->
|
||||
navigateToPersonDetail(person.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDownload() {
|
||||
if (viewModel.item.isDownloaded()) {
|
||||
viewModel.deleteItem()
|
||||
binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download)
|
||||
} else if (viewModel.item.isDownloading()) {
|
||||
createCancelDialog()
|
||||
} else if (!appPreferences.downloadQualityDefault) {
|
||||
createPickQualityDialog()
|
||||
} else {
|
||||
binding.itemActions.downloadButton.setIconResource(android.R.color.transparent)
|
||||
binding.itemActions.progressDownload.isIndeterminate = true
|
||||
|
@ -230,7 +241,7 @@ class MovieFragment : Fragment() {
|
|||
},
|
||||
)
|
||||
storageDialog.show()
|
||||
return@setOnClickListener
|
||||
return
|
||||
}
|
||||
if (viewModel.item.sources.size > 1) {
|
||||
val dialog = getVideoVersionDialog(
|
||||
|
@ -246,18 +257,13 @@ class MovieFragment : Fragment() {
|
|||
},
|
||||
)
|
||||
dialog.show()
|
||||
return@setOnClickListener
|
||||
return
|
||||
}
|
||||
createDownloadPreparingDialog()
|
||||
viewModel.download()
|
||||
}
|
||||
}
|
||||
|
||||
binding.peopleRecyclerView.adapter = PersonListAdapter { person ->
|
||||
navigateToPersonDetail(person.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
|
@ -495,6 +501,31 @@ class MovieFragment : Fragment() {
|
|||
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(
|
||||
playerItems: Array<PlayerItem>,
|
||||
) {
|
||||
|
|
|
@ -31,7 +31,9 @@ import timber.log.Timber
|
|||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.nomadics9.ananas.AppPreferences
|
||||
import com.nomadics9.ananas.models.UiText
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -44,6 +46,9 @@ class SeasonFragment : Fragment() {
|
|||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
private lateinit var downloadPreparingDialog: AlertDialog
|
||||
|
||||
@Inject
|
||||
lateinit var appPreferences: AppPreferences
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -195,6 +200,15 @@ class SeasonFragment : Fragment() {
|
|||
}
|
||||
|
||||
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 dialog = builder
|
||||
.setTitle(com.nomadics9.ananas.core.R.string.download_season_dialog_title)
|
||||
|
@ -211,6 +225,33 @@ class SeasonFragment : Fragment() {
|
|||
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.components.SingletonComponent
|
||||
import com.nomadics9.ananas.AppPreferences
|
||||
import com.nomadics9.ananas.api.JellyfinApi
|
||||
import com.nomadics9.ananas.database.ServerDatabaseDao
|
||||
import com.nomadics9.ananas.repository.JellyfinRepository
|
||||
import com.nomadics9.ananas.utils.Downloader
|
||||
|
|
|
@ -6,12 +6,15 @@ import android.net.Uri
|
|||
import android.os.Environment
|
||||
import android.os.StatFs
|
||||
import android.text.format.Formatter
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import com.nomadics9.ananas.AppPreferences
|
||||
import com.nomadics9.ananas.api.JellyfinApi
|
||||
import com.nomadics9.ananas.database.ServerDatabaseDao
|
||||
import com.nomadics9.ananas.models.FindroidEpisode
|
||||
import com.nomadics9.ananas.models.FindroidItem
|
||||
import com.nomadics9.ananas.models.FindroidMovie
|
||||
import com.nomadics9.ananas.models.FindroidSegment
|
||||
import com.nomadics9.ananas.models.FindroidSource
|
||||
import com.nomadics9.ananas.models.FindroidSources
|
||||
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.toFindroidUserDataDto
|
||||
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.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
import kotlin.Exception
|
||||
import kotlin.math.ceil
|
||||
|
@ -46,7 +76,11 @@ class DownloaderImpl(
|
|||
storageIndex: Int,
|
||||
): Pair<Long, UiText?> {
|
||||
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 trickplayInfo = if (item is FindroidSources) {
|
||||
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) {
|
||||
is FindroidMovie -> {
|
||||
database.insertMovie(item.toFindroidMovieDto(appPreferences.currentServer!!))
|
||||
|
@ -77,7 +146,70 @@ class DownloaderImpl(
|
|||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||
downloadExternalMediaStreams(item, source, storageIndex)
|
||||
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) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
|
@ -106,7 +238,7 @@ class DownloaderImpl(
|
|||
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
|
||||
downloadExternalMediaStreams(item, source, storageIndex)
|
||||
if (trickplayInfo != null) {
|
||||
downloadTrickplayData(item.id, sourceId, trickplayInfo)
|
||||
downloadTrickplayData(item.id, source.id, trickplayInfo)
|
||||
}
|
||||
if (segments != null) {
|
||||
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
|
||||
|
@ -123,15 +255,8 @@ class DownloaderImpl(
|
|||
}
|
||||
}
|
||||
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) {
|
||||
if (source.downloadId != null) {
|
||||
|
@ -193,9 +318,11 @@ class DownloaderImpl(
|
|||
)
|
||||
when (downloadStatus) {
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -217,8 +344,19 @@ 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"))
|
||||
database.insertMediaStream(mediaStream.toFindroidMediaStreamDto(id, source.id, streamPath.path.orEmpty()))
|
||||
val streamPath = Uri.fromFile(
|
||||
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))
|
||||
.setTitle(mediaStream.title)
|
||||
.setAllowedOverMetered(appPreferences.downloadOverMobileData)
|
||||
|
@ -235,7 +373,10 @@ class DownloaderImpl(
|
|||
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(
|
||||
|
@ -263,4 +404,52 @@ class DownloaderImpl(
|
|||
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>opensles</item>
|
||||
</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>
|
|
@ -9,4 +9,17 @@
|
|||
android:defaultValue="false"
|
||||
app:key="pref_downloads_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>
|
|
@ -11,9 +11,14 @@ import com.nomadics9.ananas.models.FindroidShow
|
|||
import com.nomadics9.ananas.models.FindroidSource
|
||||
import com.nomadics9.ananas.models.SortBy
|
||||
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.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.PlaybackInfoResponse
|
||||
import org.jellyfin.sdk.model.api.PublicSystemInfo
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||
|
@ -113,5 +118,15 @@ interface JellyfinRepository {
|
|||
|
||||
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.flow.Flow
|
||||
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.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
|
||||
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.ItemFields
|
||||
import org.jellyfin.sdk.model.api.ItemFilter
|
||||
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.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.SortOrder
|
||||
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 org.jellyfin.sdk.model.api.UserConfiguration
|
||||
import timber.log.Timber
|
||||
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) {
|
||||
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 -> 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.flow.Flow
|
||||
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.DeviceProfile
|
||||
import org.jellyfin.sdk.model.api.EncodingContext
|
||||
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.SortOrder
|
||||
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||
|
@ -286,7 +290,42 @@ class JellyfinRepositoryOfflineImpl(
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,6 @@ class PlayerActivityViewModel
|
|||
constructor(
|
||||
private val application: Application,
|
||||
private val jellyfinRepository: JellyfinRepository,
|
||||
private val jellyfinApi: JellyfinApi,
|
||||
private val appPreferences: AppPreferences,
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), Player.Listener {
|
||||
|
@ -547,77 +546,11 @@ constructor(
|
|||
val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate(
|
||||
transcodingResolution
|
||||
)
|
||||
val deviceProfile = ClientCapabilitiesDto(
|
||||
supportedCommands = emptyList(),
|
||||
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 deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "ts", EncodingContext.STREAMING)
|
||||
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,true,deviceProfile,videoBitRate)
|
||||
val playSessionId = playbackInfo.content.playSessionId
|
||||
val getDeviceId =
|
||||
jellyfinApi.devicesApi.getDeviceOptions(jellyfinApi.userId.toString())
|
||||
val deviceId = getDeviceId.content.deviceId
|
||||
if (playSessionId != null) {
|
||||
jellyfinApi.api.hlsSegmentApi.stopEncodingProcess(
|
||||
deviceId,
|
||||
playSessionId
|
||||
)
|
||||
jellyfinRepository.stopEncodingProcess(playSessionId)
|
||||
}
|
||||
val mediaSource = playbackInfo.content.mediaSources.firstOrNull()
|
||||
if (mediaSource == null) {
|
||||
|
@ -632,7 +565,7 @@ constructor(
|
|||
.setMimeType(externalSubtitle.mimeType)
|
||||
.build()
|
||||
}
|
||||
// Timber.tag("MediaStreams").d("Media Streams: %s", mediaSource?.mediaStreams)
|
||||
|
||||
// TODO: Embedded sub support
|
||||
// val embeddedSubtitles = mediaSource?.mediaStreams
|
||||
// ?.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal }
|
||||
|
@ -651,18 +584,11 @@ constructor(
|
|||
// .build()
|
||||
// }
|
||||
// ?.toMutableList() ?: mutableListOf()
|
||||
|
||||
// val allSubtitles = embeddedSubtitles.apply { addAll(mediaSubtitles) }
|
||||
val baseUrl = jellyfinApi.api.baseUrl
|
||||
val cleanBaseUrl = baseUrl?.removePrefix("http://")?.removePrefix("https://")
|
||||
val staticUrl = jellyfinApi.videosApi.getVideoStreamUrl(
|
||||
itemId,
|
||||
static = true,
|
||||
playSessionId = playSessionId,
|
||||
deviceId = deviceId,
|
||||
mediaSourceId = currentItem.mediaSourceId,
|
||||
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL
|
||||
)
|
||||
|
||||
val baseUrl = jellyfinRepository.getBaseUrl()
|
||||
val cleanBaseUrl = baseUrl.removePrefix("http://").removePrefix("https://")
|
||||
val staticUrl = jellyfinRepository.getStreamUrl(itemId, currentItem.mediaSourceId)
|
||||
|
||||
|
||||
val uri =
|
||||
|
@ -719,6 +645,7 @@ constructor(
|
|||
uriBuilder.setOrReplaceQueryParameter("VideoBitrate", videoBitRate.toString())
|
||||
uriBuilder.setOrReplaceQueryParameter("AudioBitrate", audioBitRate.toString())
|
||||
uriBuilder.setOrReplaceQueryParameter("Static", "false")
|
||||
uriBuilder.appendQueryParameter("PlaySessionId", playSessionId)
|
||||
uriBuilder.appendQueryParameter(
|
||||
"MaxVideoHeight",
|
||||
transcodingResolution.toString()
|
||||
|
|
|
@ -141,6 +141,18 @@ constructor(
|
|||
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
|
||||
var sortBy: String
|
||||
get() = sharedPreferences.getString(
|
||||
|
|
|
@ -45,6 +45,8 @@ object Constants {
|
|||
const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout"
|
||||
const val PREF_DOWNLOADS_MOBILE_DATA = "pref_downloads_mobile_data"
|
||||
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_ORDER = "pref_sort_order"
|
||||
const val PREF_DISPLAY_EXTRA_INFO = "pref_display_extra_info"
|
||||
|
|
Loading…
Reference in a new issue