Compare commits

...

42 commits

Author SHA1 Message Date
nomadics9
e009b86153 build: 16 version-0.10.6-0.14.2
Some checks failed
Build / Assemble (push) Has been cancelled
2024-07-26 05:13:37 +03:00
nomadics9
d669dcb618 hotfix 2024-07-26 05:13:08 +03:00
nomadics9
ff5bce074e build: 16 version-0.10.6-0.14.2 2024-07-26 05:02:31 +03:00
nomadics9
d85684317b feat: very simple update checker 2024-07-26 04:58:50 +03:00
nomadics9
3cc52938f2 bugfix: Download season dialog strings 2024-07-26 02:42:48 +03:00
nomadics9
3ff71ae489 build: 15 version-0.10.5-0.14.2 2024-07-21 06:23:54 +03:00
nomadics9
b511b26aa1 feat: Markdown support for disclamer 2024-07-21 06:18:21 +03:00
nomadics9
059b17af9a personal 2024-07-21 05:16:04 +03:00
nomadics9
c293c906d4 build: 15 version-0.10.5-0.14.2 2024-07-21 04:38:32 +03:00
nomadics9
b5d31a6c72 feat: select transcoding codec in network settings / code: clean up, refactors & rework alot of transcoding stuff / bugfixes: mainly deviceId 2024-07-21 04:18:08 +03:00
nomadics9
5609f7368d code: cleanup
Some checks failed
Build / Lint (push) Has been cancelled
Build / Assemble (push) Has been cancelled
2024-07-21 01:49:30 +03:00
nomadics9
d70253140d refactor: string 2024-07-21 00:43:12 +03:00
nomadics9
8482df9733 feat: choice of codec in network settings / bugfix: nullsafe fix 2024-07-21 00:42:35 +03:00
nomadics9
21ae815223 rework: getting original resolution for quality selection dialog 2024-07-20 23:55:34 +03:00
nomadics9
c79342523b refactor: strings & naming standard for icon 2024-07-20 23:12:12 +03:00
nomadics9
7adcc50d75 rework: Enum 2024-07-20 22:28:38 +03:00
nomadics9
0ace01f5f8 klint 2024-07-20 08:40:23 +03:00
nomadics9
6dded2e726 bugfixes: deviceId / code: New Enum VideoQuality 2024-07-20 08:36:23 +03:00
nomadics9
ba580f8769 lint: fix 2024-07-19 05:27:17 +03:00
nomadics9
4baa7bc046 lint: klint standard 2024-07-19 05:10:32 +03:00
nomadics9
633ee6b8c4 lint: klint standard 2024-07-19 05:01:09 +03:00
nomadics9
062781a43d feat: Download transcoded media 2024-07-19 03:44:43 +03:00
nomadics9
ccc6788a02 feat: Transcoding stream in player selection /code: prep repo for next commit transcoding downloads 2024-07-19 02:20:55 +03:00
renovate[bot]
db79b50629
chore(deps): update dependency com.google.devtools.ksp to v2.0.0-1.0.23 (#788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 22:20:48 +02:00
renovate[bot]
45d4b88738
chore(deps): update dependency gradle to v8.9 (#789)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 22:08:26 +02:00
xxzp3
eabe738136 chore(translate): (Danish)
Currently translated at 65.2% (126 of 193 strings)

Translation: Findroid/core
Translate-URL: https://weblate.jdtech.dev/projects/findroid/core/da/
2024-07-16 12:30:01 +02:00
Jarne Demeulemeester
48d8b18bae
lint: run ktlintFormat 2024-07-15 23:20:42 +02:00
Jarne Demeulemeester
15c1ac9593
refactor(tv): replace deprecated tv lazy layouts with normal lazy layouts
Use beta version of compose for now (1.7.x)
TV compose foundation library removed
No longer using bom to specify dependencies (doesn't work with the beta versions)
2024-07-15 22:18:09 +02:00
Jarne Demeulemeester
307ce957c2
chore: target SDK 35 2024-07-13 17:12:56 +02:00
Jarne Demeulemeester
1267f9809d
chore(deps): upgrade agp, tv, tv-material3 and jellyfin
agp 8.5.0 -> 8.5.1
tv 1.0.0-alpha10 -> 1.0.0-alpha11
tv-material3 1.0.0-beta01 -> 1.0.0-rc01
jellyfin 1.5.0-beta.4 -> 1.5.0
2024-07-13 16:52:03 +02:00
Suyash Mahar
e00156cd1c chore(translate): (Hindi)
Currently translated at 59.5% (115 of 193 strings)

Translation: Findroid/core
Translate-URL: https://weblate.jdtech.dev/projects/findroid/core/hi/
2024-07-13 04:30:01 +02:00
Suyash Mahar
ea1163d25d chore(translate): (French)
Currently translated at 99.4% (192 of 193 strings)

Translation: Findroid/core
Translate-URL: https://weblate.jdtech.dev/projects/findroid/core/fr/
2024-07-13 04:30:01 +02:00
Suyash Mahar
0c94c3c7dc chore(translate): add (Hindi) 2024-07-12 04:20:24 +02:00
Jasper
03023d8c9f chore(translate): (Dutch)
Currently translated at 98.9% (191 of 193 strings)

Translation: Findroid/core
Translate-URL: https://weblate.jdtech.dev/projects/findroid/core/nl/
2024-07-10 12:41:17 +02:00
Jarne Demeulemeester
785db44744
chore(deps): update androidx lifecyle to 2.8.3
lifecycle 2.8.2 -> 2.8.3
2024-07-07 14:01:18 +02:00
renovate[bot]
2dd65705af
fix(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.7.1 (#786)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 12:39:35 +02:00
renovate[bot]
2bfe4388ea
chore(deps): update aboutlibraries to v11.2.2 (#785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 12:38:31 +02:00
adiskill
44fe7dac35 chore(translate): (Slovak)
Currently translated at 100.0% (193 of 193 strings)

Translation: Findroid/core
Translate-URL: https://weblate.jdtech.dev/projects/findroid/core/sk/
2024-06-30 20:30:01 +02:00
Jarne Demeulemeester
544e6432f5
chore(deps): update libmpv
libmpv 0.2.0 -> 0.3.0
2024-06-30 18:47:03 +02:00
Jarne Demeulemeester
36891e7682
test: fix main flow test and update dependencies 2024-06-29 23:00:11 +02:00
nomadics9
32c6d22035 chore(translate): (Arabic)
Currently translated at 8.8% (17 of 193 strings)

Translation: Findroid/core
Translate-URL: https://weblate.jdtech.dev/projects/findroid/core/ar/
2024-06-29 14:30:01 +02:00
Tio
ba03ef4e9f chore(translate): (Portuguese (Brazil))
Currently translated at 98.9% (191 of 193 strings)

Translation: Findroid/core
Translate-URL: https://weblate.jdtech.dev/projects/findroid/core/pt_BR/
2024-06-29 14:30:01 +02:00
33 changed files with 605 additions and 425 deletions

View file

@ -25,6 +25,7 @@ jobs:
assemble:
name: Assemble
runs-on: ubuntu-22.04
if: startsWith(github.event.head_commit.message, 'build:')
steps:
- name: Checkout repository
uses: actions/checkout@v4

1
.gitignore vendored
View file

@ -41,3 +41,4 @@ fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
push.sh

View file

@ -16,35 +16,9 @@
}
],
"attributes": [],
"versionCode": 14,
"versionName": "0.10.4-0.14.2",
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-armeabi-v7a.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86_64"
}
],
"attributes": [],
"versionCode": 14,
"versionName": "0.10.4-0.14.2",
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-x86_64.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86"
}
],
"attributes": [],
"versionCode": 14,
"versionName": "0.10.4-0.14.2",
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-x86.apk"
"versionCode": 16,
"versionName": "0.10.6-0.14.2",
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-armeabi-v7a.apk"
},
{
"type": "ONE_OF_MANY",
@ -55,9 +29,35 @@
}
],
"attributes": [],
"versionCode": 14,
"versionName": "0.10.4-0.14.2",
"outputFile": "ananas-v0.10.4-0.14.2-Ananas-arm64-v8a.apk"
"versionCode": 16,
"versionName": "0.10.6-0.14.2",
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-arm64-v8a.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86_64"
}
],
"attributes": [],
"versionCode": 16,
"versionName": "0.10.6-0.14.2",
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-x86_64.apk"
},
{
"type": "ONE_OF_MANY",
"filters": [
{
"filterType": "ABI",
"value": "x86"
}
],
"attributes": [],
"versionCode": 16,
"versionName": "0.10.6-0.14.2",
"outputFile": "ananas-v0.10.6-0.14.2-Ananas-x86.apk"
}
],
"elementType": "File",
@ -66,20 +66,20 @@
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-armeabi-v7a.dm",
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-x86_64.dm",
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-x86.dm",
"baselineProfiles/1/ananas-v0.10.4-0.14.2-Ananas-arm64-v8a.dm"
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-armeabi-v7a.dm",
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-arm64-v8a.dm",
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-x86_64.dm",
"baselineProfiles/1/ananas-v0.10.6-0.14.2-Ananas-x86.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-armeabi-v7a.dm",
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-x86_64.dm",
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-x86.dm",
"baselineProfiles/0/ananas-v0.10.4-0.14.2-Ananas-arm64-v8a.dm"
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-armeabi-v7a.dm",
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-arm64-v8a.dm",
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-x86_64.dm",
"baselineProfiles/0/ananas-v0.10.6-0.14.2-Ananas-x86.dm"
]
}
],

View file

@ -25,6 +25,8 @@ android {
testInstrumentationRunner = "com.nomadics9.ananas.HiltTestRunner"
buildConfigField( "String", "DEFAULT_SERVER_ADDRESS", "\" \"")
buildConfigField( "String", "REQUEST_SERVER_ADDRESS", "\" \"")
buildConfigField("String", "FORGET_PASSWORD_ADDRESS", "\" \"")
buildConfigField("String", "UPDATE_ADDRESS", "\" \"")
}
applicationVariants.all {
@ -68,6 +70,8 @@ android {
isDefault = false
buildConfigField( "String", "DEFAULT_SERVER_ADDRESS", "\"https://askar.tv\"")
buildConfigField( "String", "REQUEST_SERVER_ADDRESS", "\"https://r.askar.tv\"")
buildConfigField("String", "FORGET_PASSWORD_ADDRESS", "\"https://user.askar.tv/my/account\"")
buildConfigField("String", "UPDATE_ADDRESS", "\"https://fs.nmd.mov/p/ananas.apk\"")
}
}
@ -130,6 +134,7 @@ dependencies {
implementation(libs.material)
implementation(libs.media3.ffmpeg.decoder)
implementation(libs.timber)
implementation(libs.markwon)
coreLibraryDesugaring(libs.android.desugar.jdk)

View file

@ -47,6 +47,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import com.nomadics9.ananas.core.R as CoreR
import com.nomadics9.ananas.models.VideoQuality
var isControlsLocked: Boolean = false
@ -86,12 +87,10 @@ class PlayerActivity : BasePlayerActivity() {
binding = ActivityPlayerBinding.inflate(layoutInflater)
setContentView(binding.root)
val changeQualityButton: ImageButton = findViewById(R.id.btnChangeQuality)
changeQualityButton.setOnClickListener {
showQualitySelectionDialog()
}
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
binding.playerView.player = viewModel.player
@ -356,8 +355,7 @@ class PlayerActivity : BasePlayerActivity() {
if (appPreferences.playerTrickplay) {
val imagePreview = binding.playerView.findViewById<ImageView>(R.id.image_preview)
previewScrubListener =
PreviewScrubListener(
previewScrubListener = PreviewScrubListener(
imagePreview,
timeBar,
viewModel.player,
@ -439,50 +437,32 @@ class PlayerActivity : BasePlayerActivity() {
try {
enterPictureInPictureMode(pipParams())
} catch (_: IllegalArgumentException) {
}
} catch (_: IllegalArgumentException) { }
}
private var selectedIndex = 1 // Default to "Original" (index 1)
private fun showQualitySelectionDialog() {
val height = viewModel.getOriginalHeight()
val originalResolution = viewModel.getOriginalResolution() ?: 0
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList()
val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList()
// Map entries to values
val qualityMap = qualityEntries.zip(qualityValues).toMap()
val qualities = qualityEntries.toMutableList()
val closestQuality = VideoQuality.entries
.filter { it != VideoQuality.Auto && it != VideoQuality.Original }
.minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution) }
val qualities: List<String> =
when (height) {
0 -> qualityEntries
in 1001..1999 ->
listOf(
qualityEntries[0],
"${qualityEntries[1]} (1080p)",
qualityEntries[2],
qualityEntries[3],
qualityEntries[4],
qualityEntries[5],
)
in 2000..3000 ->
listOf(
qualityEntries[0],
"${qualityEntries[1]} (4K)",
qualityEntries[2],
qualityEntries[3],
qualityEntries[4],
qualityEntries[5],
)
else -> qualityEntries
if (closestQuality != null) {
qualities[1] = "${qualities[1]} (${closestQuality})"
}
MaterialAlertDialogBuilder(this)
.setTitle("Select Video Quality")
.setItems(qualities.toTypedArray()) { _, which ->
val selectedQualityEntry = qualities[which]
val selectedQualityValue =
qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry
.setTitle(CoreR.string.select_quality)
.setSingleChoiceItems(qualities.toTypedArray(), selectedIndex) { dialog, which ->
selectedIndex = which
val selectedQualityValue = qualityValues[which]
viewModel.changeVideoQuality(selectedQualityValue)
}.show()
dialog.dismiss()
}
.show()
}
override fun onPictureInPictureModeChanged(

View file

@ -172,11 +172,11 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
}else if (!appPreferences.downloadQualityDefault) {
createPickQualityDialog()
} else {
download()
startDownload()
}
}
private fun download(){
private fun startDownload(){
binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent)
binding.itemActions.progressDownload.isIndeterminate = true
binding.itemActions.progressDownload.isVisible = true
@ -413,8 +413,8 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
}
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 qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries)
val qualityValues = resources.getStringArray(CoreR.array.download_quality_values)
val quality = appPreferences.downloadQuality
val currentQualityIndex = qualityValues.indexOf(quality)
var selectedQuality = quality
@ -428,7 +428,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
builder.setPositiveButton("Download") { dialog, _ ->
appPreferences.downloadQuality = selectedQuality
dialog.dismiss()
download()
startDownload()
}
builder.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()

View file

@ -1,5 +1,7 @@
package com.nomadics9.ananas.fragments
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.Html.fromHtml
import android.view.LayoutInflater
@ -17,11 +19,13 @@ import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.BuildConfig
import com.nomadics9.ananas.adapters.UserLoginListAdapter
import com.nomadics9.ananas.database.ServerDatabaseDao
import com.nomadics9.ananas.databinding.FragmentLoginBinding
import com.nomadics9.ananas.viewmodels.LoginEvent
import com.nomadics9.ananas.viewmodels.LoginViewModel
import io.noties.markwon.Markwon
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -78,6 +82,17 @@ class LoginFragment : Fragment() {
(binding.editTextPassword as AppCompatEditText).requestFocus()
}
if (BuildConfig.FLAVOR == "Ananas") {
binding.buttonForgetPassword.setOnClickListener {
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(BuildConfig.FORGET_PASSWORD_ADDRESS))
startActivity(browserIntent)
}
binding.buttonForgetPassword.isVisible = true
} else {
binding.buttonForgetPassword.isVisible = false
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { uiState ->
@ -143,7 +158,21 @@ class LoginFragment : Fragment() {
binding.editTextPasswordLayout.isEnabled = true
uiState.disclaimer?.let { disclaimer ->
binding.loginDisclaimer.text = fromHtml(disclaimer, 0)
if (BuildConfig.FLAVOR == "Ananas") {
val lines = disclaimer.lines()
val lineToRemoveIndex = 3
val filteredLines = lines.toMutableList().apply {
if (size > lineToRemoveIndex) {
removeAt(lineToRemoveIndex)
}
}
val filteredDisclaimer = filteredLines.joinToString("\n")
val markwon = Markwon.create(requireContext())
markwon.setMarkdown(binding.loginDisclaimer, filteredDisclaimer)
} else {
val markwon = Markwon.create(requireContext())
markwon.setMarkdown(binding.loginDisclaimer, disclaimer)
}
}
}

View file

@ -209,11 +209,11 @@ class MovieFragment : Fragment() {
} else if (!appPreferences.downloadQualityDefault) {
createPickQualityDialog()
} else {
download()
startDownload()
}
}
private fun download() {
private fun startDownload() {
binding.itemActions.downloadButton.setIconResource(android.R.color.transparent)
binding.itemActions.progressDownload.isIndeterminate = true
binding.itemActions.progressDownload.isVisible = true
@ -506,8 +506,8 @@ class MovieFragment : Fragment() {
}
private fun createPickQualityDialog() {
val qualityEntries = resources.getStringArray(CoreR.array.quality_entries)
val qualityValues = resources.getStringArray(CoreR.array.quality_values)
val qualityEntries = resources.getStringArray(CoreR.array.download_quality_entries)
val qualityValues = resources.getStringArray(CoreR.array.download_quality_values)
val quality = appPreferences.downloadQuality
val currentQualityIndex = qualityValues.indexOf(quality)
var selectedQuality = quality
@ -520,7 +520,7 @@ class MovieFragment : Fragment() {
}
builder.setPositiveButton("Download") { dialog, _ ->
appPreferences.downloadQuality = selectedQuality
download()
startDownload()
dialog.dismiss()
}
builder.setNegativeButton("Cancel") { dialog, _ ->

View file

@ -226,8 +226,8 @@ class SeasonFragment : Fragment() {
}
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 qualityEntries = resources.getStringArray(com.nomadics9.ananas.core.R.array.download_quality_entries)
val qualityValues = resources.getStringArray(com.nomadics9.ananas.core.R.array.download_quality_values)
val quality = appPreferences.downloadQuality
val currentQualityIndex = qualityValues.indexOf(quality)

View file

@ -3,12 +3,23 @@ package com.nomadics9.ananas.fragments
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import dagger.hilt.android.AndroidEntryPoint
import com.nomadics9.ananas.AppPreferences
import com.nomadics9.ananas.BuildConfig
import com.nomadics9.ananas.utils.restart
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
import com.nomadics9.ananas.core.R as CoreR
@ -17,6 +28,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
@Inject
lateinit var appPreferences: AppPreferences
private val updateUrl = BuildConfig.UPDATE_ADDRESS
private var isUpdateAvailable: Boolean = false
private var newLastModifiedDate: Date? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(CoreR.xml.fragment_settings, rootKey)
@ -27,13 +42,21 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference<Preference>("switchUser")?.setOnPreferenceClickListener {
val serverId = appPreferences.currentServer!!
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToUsersFragment(serverId))
findNavController().navigate(
TwoPaneSettingsFragmentDirections.actionNavigationSettingsToUsersFragment(
serverId
)
)
true
}
findPreference<Preference>("switchAddress")?.setOnPreferenceClickListener {
val serverId = appPreferences.currentServer!!
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToServerAddressesFragment(serverId))
findNavController().navigate(
TwoPaneSettingsFragmentDirections.actionNavigationSettingsToServerAddressesFragment(
serverId
)
)
true
}
@ -51,14 +74,102 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}
findPreference<Preference>("appInfo")?.setOnPreferenceClickListener {
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionSettingsFragmentToAboutLibraries())
if (isUpdateAvailable && newLastModifiedDate != null) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(updateUrl))
startActivity(intent)
storeDate(newLastModifiedDate!!)
true
} else {
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionSettingsFragmentToAboutLibraries())
false
}
}
findPreference<Preference>("requests")?.setOnPreferenceClickListener {
findNavController().navigate(TwoPaneSettingsFragmentDirections.actionNavigationSettingsToRequestsWebFragment())
true
}
// Check for updates when the settings screen is opened
checkForUpdates()
}
private fun checkForUpdates() {
lifecycleScope.launch {
val lastModifiedDate = fetchLastModifiedDate(updateUrl)
if (lastModifiedDate != null) {
Timber.d("Fetched Last-Modified date: $lastModifiedDate")
val storedDate = getStoredDate()
Timber.d("Stored date: $storedDate")
if (storedDate == Date(0L) || lastModifiedDate.after(storedDate)) {
Timber.d("Update available")
isUpdateAvailable = true
newLastModifiedDate = lastModifiedDate
showUpdateAvailable()
} else {
Timber.d("No update available")
isUpdateAvailable = false
}
} else {
Timber.d("Failed to fetch Last-Modified date")
isUpdateAvailable = false
}
}
}
private suspend fun fetchLastModifiedDate(urlString: String): Date? {
return withContext(Dispatchers.IO) {
var urlConnection: HttpURLConnection? = null
try {
val url = URL(urlString)
urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "HEAD"
val lastModified = urlConnection.getHeaderField("Last-Modified")
if (lastModified != null) {
val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US)
dateFormat.parse(lastModified)
} else {
null
}
} catch (e: Exception) {
Timber.e(e, "Error fetching Last-Modified date")
null
} finally {
urlConnection?.disconnect()
}
}
}
private fun getStoredDate(): Date {
val sharedPreferences = preferenceManager.sharedPreferences
val storedDateString = sharedPreferences?.getString("stored_date", null)
Timber.d("Retrieved stored date string: $storedDateString")
return if (storedDateString != null) {
try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).parse(storedDateString) ?: Date(0)
} catch (e: Exception) {
Timber.e(e, "Error parsing stored date string")
Date(0)
}
} else {
Date(0)
}
}
private fun storeDate(date: Date) {
val dateString = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).format(date)
preferenceManager.sharedPreferences?.edit()?.putString("stored_date", dateString)?.apply()
Timber.d("Stored new date: $dateString")
}
private fun showUpdateAvailable() {
val appInfoPreference = findPreference<Preference>("appInfo")
appInfoPreference?.let {
it.summary = "Update available!"
it.icon = ResourcesCompat.getDrawable(resources, CoreR.drawable.ic_download, null) // Ensure this drawable exists
Timber.d("Update available UI shown")
}
}
}

View file

@ -73,6 +73,7 @@
android:layout_height="0dp"
android:layout_weight="1" />
<!--TODO: Content Desc to Strings-->
<ImageButton
android:id="@+id/btnChangeQuality"
android:layout_width="wrap_content"
@ -80,7 +81,7 @@
android:background="@drawable/transparent_circle_background"
android:contentDescription="Quality"
android:padding="16dp"
android:src="@drawable/ic_quality"
android:src="@drawable/ic_monitor_play"
android:layout_gravity="end"
app:tint="@android:color/white"
/>

View file

@ -141,10 +141,24 @@
android:visibility="invisible" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp">
<Button
android:id="@+id/button_forget_password"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/forget_password" />
</RelativeLayout>
<TextView
android:id="@+id/login_disclaimer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginHorizontal="24dp"
android:layout_margin="24dp"
android:textSize="16sp"

View file

@ -1,8 +1,8 @@
import org.gradle.api.JavaVersion
object Versions {
const val appCode = 14
const val appName = "0.10.4-0.14.2"
const val appCode = 16
const val appName = "0.10.6-0.14.2"
const val compileSdk = 34
const val buildTools = "34.0.0"

View file

@ -79,15 +79,8 @@ 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)
}
handleDownload(item, source, storageIndex, trickplayInfo, segments, path)
return Pair(-1, null)
} catch (e: Exception) {
try {
val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId }
@ -108,79 +101,7 @@ class DownloaderImpl(
}
}
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!!))
database.insertSource(source.toFindroidSourceDto(item.id, path.path.orEmpty()))
database.insertUserData(item.toFindroidUserDataDto(jellyfinRepository.getUserId()))
downloadExternalMediaStreams(item, source, storageIndex)
downloadEmbeddedMediaStreams(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)
}
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)
downloadEmbeddedMediaStreams(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(
private suspend fun handleDownload(
item: FindroidItem,
source: FindroidSource,
storageIndex: Int,
@ -200,6 +121,22 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
if (appPreferences.downloadQuality != VideoQuality.Original.toString()) {
downloadEmbeddedMediaStreams(item, source, storageIndex)
val transcodingUrl =
getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
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)
} else {
val request =
DownloadManager
.Request(source.path.toUri())
@ -212,6 +149,7 @@ class DownloaderImpl(
database.setSourceDownloadId(source.id, downloadId)
return Pair(downloadId, null)
}
}
is FindroidEpisode -> {
database.insertShow(
@ -232,6 +170,22 @@ class DownloaderImpl(
if (segments != null) {
database.insertSegments(segments.toFindroidSegmentsDto(item.id))
}
if (appPreferences.downloadQuality != VideoQuality.Original.toString()) {
downloadEmbeddedMediaStreams(item, source, storageIndex)
val transcodingUrl =
getTranscodedUrl(item.id, appPreferences.downloadQuality!!)
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)
} else {
val request =
DownloadManager
.Request(source.path.toUri())
@ -245,6 +199,7 @@ class DownloaderImpl(
return Pair(downloadId, null)
}
}
}
return Pair(-1, null)
}
@ -483,8 +438,8 @@ class DownloaderImpl(
mediaSourceId,
playSessionId,
VideoQuality.getBitrate(videoQuality),
VideoQuality.getQualityInt(videoQuality),
"mkv",
VideoQuality.getHeight(videoQuality),
)
return downloadUrl.toUri()

View file

@ -0,0 +1,35 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M10,7.75a0.75,0.75 0,0 1,1.142 -0.638l3.664,2.249a0.75,0.75 0,0 1,0 1.278l-3.664,2.25a0.75,0.75 0,0 1,-1.142 -0.64z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M12,17v4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M8,21h8"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M4,3L20,3A2,2 0,0 1,22 5L22,15A2,2 0,0 1,20 17L4,17A2,2 0,0 1,2 15L2,5A2,2 0,0 1,4 3z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -1,2 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="add_server_error_outdated">اصدار الخادم قديم: %1$s. الرجاء تحديث الخادم</string>
<string name="add_server_error_not_jellyfin">ليس خادم جيلي فن:%1$s</string>
<string name="add_server_error_version">اصدار الخادم غير مدعوم: %1$s. الرجاء تحديث الخادم</string>
<string name="add_server_error_slow">رد الخادم بطيء: %1$s</string>
<string name="login">تسجيل دخول</string>
<string name="login_error_wrong_username_password">اسم المستخدم او الكلمه السريه غير صحيحه</string>
<string name="select_server">اختر الخادم</string>
<string name="edit_text_server_address_hint">عنوان الخادم</string>
<string name="edit_text_username_hint">اسم المستخدم</string>
<string name="edit_text_password_hint">الكلمه السريه</string>
<string name="button_connect">اتصل</string>
<string name="button_login">تسجيل دخول</string>
<string name="remove_server">حذف الخادم</string>
<string name="add_server">اضافه خادم</string>
<string name="add_server_error_not_found">الخادم غير موجود</string>
<string name="add_server_error_empty_address">عنوان الخادم فاضي</string>
<string name="add_server_error_no_id">الخادم غير معرف بالid , يبدو انه هناك خلل في الخادم</string>
</resources>

View file

@ -26,12 +26,12 @@
<item>opensles</item>
</string-array>
<string-array name="quality_entries">
<item>Auto</item>
<item>Original</item>
<item>1080p - 8Mbps</item>
<item>720p - 2Mbps</item>
<item>480p - 1Mbps</item>
<item>360p - 800Kbps</item>
<item>@string/quality_auto</item>
<item>@string/quality_original</item>
<item>@string/quality_1080p</item>
<item>@string/quality_720p</item>
<item>@string/quality_480p</item>
<item>@string/quality_360p</item>
</string-array>
<string-array name="quality_values">
<item>Auto</item>
@ -41,4 +41,22 @@
<item>480p</item>
<item>360p</item>
</string-array>
<string-array name="download_quality_entries">
<item>@string/quality_original</item>
<item>@string/quality_1080p</item>
<item>@string/quality_720p</item>
<item>@string/quality_480p</item>
<item>@string/quality_360p</item>
</string-array>
<string-array name="download_quality_values">
<item>Original</item>
<item>1080p</item>
<item>720p</item>
<item>480p</item>
<item>360p</item>
</string-array>
<string-array name="codecs">
<item>h264</item>
<item>hevc</item>
</string-array>
</resources>

View file

@ -11,6 +11,7 @@
<string name="add_server_error_not_found">Server not found</string>
<string name="add_server_error_no_id">Server has no id, something seems to be wrong with the server</string>
<string name="login">Login</string>
<string name="forget_password">Forget Password?</string>
<string name="login_error_wrong_username_password">Wrong username or password</string>
<string name="select_server">Select server</string>
<string name="edit_text_server_address_hint">Server address</string>
@ -139,6 +140,7 @@
<string name="settings_request_timeout">Request timeout (ms)</string>
<string name="settings_connect_timeout">Connect timeout (ms)</string>
<string name="settings_socket_timeout">Socket timeout (ms)</string>
<string name="settings_quality_codec">Transcoding codec</string>
<string name="users">Users</string>
<string name="add_user">Add user</string>
<string name="pref_player_mpv_hwdec">Hardware decoding</string>
@ -193,6 +195,15 @@
<string name="unmark_as_played">Unmark as played</string>
<string name="add_to_favorites">Add to favorites</string>
<string name="remove_from_favorites">Remove from favorites</string>
<string name="quality_default">Default to selected download quality</string>
<string name="download_quality">Download Quality</string>
<string name="select_quality">Select Video Quality</string>
<string name="quality_auto">Auto</string>
<string name="quality_original">Original</string>
<string name="quality_1080p">1080p - 8Mbps</string>
<string name="quality_720p">720p - 3Mbps</string>
<string name="quality_480p">480p - 1.5Mbps</string>
<string name="quality_360p">360p - 0.8Mbps</string>
<string name="alaskarTV_requests">AlaskarTV Requests</string>
<string name="pref_player_trickplay_gesture">Trick Play in seek gesture</string>
<string name="pref_player_trickplay_gesture_summary">Requires \'Seek gesture\' and \'Trick Play\'</string>

View file

@ -75,7 +75,8 @@
<Preference
app:key="appInfo"
app:icon="@drawable/ic_logo"
app:title="@string/app_info" />
app:title="@string/app_info"
app:summary="" />
</PreferenceCategory>

View file

@ -11,15 +11,13 @@
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:title="@string/download_quality"
android:defaultValue="@string/quality_original"
android:entries="@array/download_quality_entries"
android:entryValues="@array/download_quality_values"
android:summary="%s" />
<SwitchPreferenceCompat
android:defaultValue="false"
app:key="pref_downloads_quality_default"
app:summary="Default to picked Download Quality" />
app:summary="@string/quality_default" />
</PreferenceScreen>

View file

@ -20,4 +20,11 @@
android:defaultValue="true"
app:key="pref_auto_offline"
app:title="@string/turn_on_offline_mode_automatically" />
<ListPreference
app:defaultValue="hevc"
app:key="pref_network_codec"
app:title="@string/settings_quality_codec"
app:useSimpleSummaryProvider="true"
app:entries="@array/codecs"
app:entryValues="@array/codecs" />
</PreferenceScreen>

View file

@ -2,24 +2,30 @@ package com.nomadics9.ananas.models
enum class VideoQuality(
val bitrate: Int,
val qualityString: String,
val qualityInt: Int,
val height: Int,
val width: Int,
val isOriginalQuality: Boolean,
) {
PAuto(1, "Auto", 1080),
POriginal(1000000000, "Original", 1080),
P1080(8000000, "1080p", 1080),
P720(2000000, "720p", 720),
P480(1000000, "480p", 480),
P360(700000, "360p", 360),
;
Auto(10000000, 1080, 1920, false),
Original(1000000000, 1080, 1920, true),
P3840(12000000,3840, 2160, false), // Here for future proofing and to calculate original resolution only
P1080(8000000, 1080, 1920, false),
P720(3000000, 720, 1280, false),
P480(1500000, 480, 854, false),
P360(800000, 360, 640, false);
override fun toString(): String = when (this) {
Auto -> "Auto"
Original -> "Original"
P3840 -> "4K"
else -> "${height}p"
}
companion object {
fun fromString(quality: String): VideoQuality? = entries.find { it.qualityString == quality }
fun fromString(quality: String): VideoQuality? = entries.find { it.toString() == quality }
fun getBitrate(quality: VideoQuality): Int = quality.bitrate
fun getQualityString(quality: VideoQuality): String = quality.qualityString
fun getQualityInt(quality: VideoQuality): Int = quality.qualityInt
fun getHeight(quality: VideoQuality): Int = quality.height
fun getWidth(quality: VideoQuality): Int = quality.width
fun getIsOriginalQuality(quality: VideoQuality): Boolean = quality.isOriginalQuality
}
}

View file

@ -88,40 +88,21 @@ interface JellyfinRepository {
offline: Boolean = false,
): List<FindroidEpisode>
suspend fun getMediaSources(
itemId: UUID,
includePath: Boolean = false,
): List<FindroidSource>
suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List<FindroidSource>
suspend fun getStreamUrl(
itemId: UUID,
mediaSourceId: String,
playSessionId: String? = null,
): String
suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String
suspend fun getSegmentsTimestamps(itemId: UUID): List<FindroidSegment>?
suspend fun getTrickplayData(
itemId: UUID,
width: Int,
index: Int,
): ByteArray?
suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray?
suspend fun postCapabilities()
suspend fun postPlaybackStart(itemId: UUID)
suspend fun postPlaybackStop(
itemId: UUID,
positionTicks: Long,
playedPercentage: Int,
)
suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int)
suspend fun postPlaybackProgress(
itemId: UUID,
positionTicks: Long,
isPaused: Boolean,
)
suspend fun postPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean)
suspend fun markAsFavorite(itemId: UUID)
@ -155,8 +136,8 @@ interface JellyfinRepository {
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
maxHeight: Int,
container: String,
maxHeight: Int,
): String
suspend fun getTranscodedVideoStream(
@ -175,4 +156,6 @@ interface JellyfinRepository {
): Response<PlaybackInfoResponse>
suspend fun stopEncodingProcess(playSessionId: String)
suspend fun getAccessToken(): String?
}

View file

@ -384,7 +384,7 @@ class JellyfinRepositoryImpl(
playSessionId: String?,
): String =
withContext(Dispatchers.IO) {
// val deviceId = getDeviceId()
val deviceId = getDeviceId()
try {
val url =
if (playSessionId != null) {
@ -393,7 +393,7 @@ class JellyfinRepositoryImpl(
static = true,
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
// deviceId = deviceId,
deviceId = deviceId,
context = EncodingContext.STREAMING,
)
} else {
@ -401,7 +401,7 @@ class JellyfinRepositoryImpl(
itemId,
static = true,
mediaSourceId = mediaSourceId,
// deviceId = deviceId,
deviceId = deviceId,
)
}
url
@ -752,8 +752,8 @@ class JellyfinRepositoryImpl(
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
maxHeight: Int,
container: String,
maxHeight: Int,
): String {
val url =
jellyfinApi.videosApi.getVideoStreamByContainerUrl(
@ -764,9 +764,9 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId,
videoBitRate = videoBitrate,
maxHeight = maxHeight,
audioBitRate = 128000,
videoCodec = "hevc",
audioCodec = "aac",
audioBitRate = 328000,
videoCodec = appPreferences.transcodeCodec,
audioCodec = "aac,ac3,eac3",
container = container,
startTimeTicks = 0,
copyTimestamps = true,
@ -782,7 +782,7 @@ class JellyfinRepositoryImpl(
playSessionId: String,
videoBitrate: Int,
): String {
val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto)
val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.Auto)
val url: String
try {
url =
@ -795,9 +795,9 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId,
videoBitRate = videoBitrate,
enableAdaptiveBitrateStreaming = false,
audioBitRate = 128000,
videoCodec = "hevc",
audioCodec = "aac",
audioBitRate = 328000,
videoCodec = appPreferences.transcodeCodec,
audioCodec = "aac,ac3,eac3",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -813,8 +813,8 @@ class JellyfinRepositoryImpl(
mediaSourceId = mediaSourceId,
playSessionId = playSessionId,
enableAdaptiveBitrateStreaming = true,
videoCodec = "hevc",
audioCodec = "aac",
videoCodec = appPreferences.transcodeCodec,
audioCodec = "aac,ac3,eac3",
startTimeTicks = 0,
copyTimestamps = true,
subtitleMethod = SubtitleDeliveryMethod.EXTERNAL,
@ -839,4 +839,8 @@ class JellyfinRepositoryImpl(
playSessionId = playSessionId,
)
}
override suspend fun getAccessToken(): String? {
return jellyfinApi.api.accessToken
}
}

View file

@ -336,8 +336,8 @@ class JellyfinRepositoryOfflineImpl(
mediaSourceId: String,
playSessionId: String,
videoBitrate: Int,
maxHeight: Int,
container: String,
maxHeight: Int,
): String {
TODO("Not yet implemented")
}
@ -364,4 +364,8 @@ class JellyfinRepositoryOfflineImpl(
override suspend fun stopEncodingProcess(playSessionId: String) {
TODO("Not yet implemented")
}
override suspend fun getAccessToken(): String? {
TODO("Not yet implemented")
}
}

View file

@ -38,6 +38,7 @@ libmpv = "0.3.0"
material = "1.12.0"
media3-ffmpeg-decoder = "1.3.1+2"
timber = "5.0.1"
markwon = "4.6.2"
[libraries]
aboutlibraries-core = { group = "com.mikepenz", name = "aboutlibraries-core", version.ref = "aboutlibraries" }
@ -99,6 +100,7 @@ material = { group = "com.google.android.material", name = "material", version.r
media3-ffmpeg-decoder = { group = "org.jellyfin.media3", name = "media3-ffmpeg-decoder", version.ref = "media3-ffmpeg-decoder" }
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
markwon = { group = "io.noties.markwon", name = "core", version.ref = "markwon" }
[plugins]
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="MissingTranslation" severity="ignore" />
</lint>

View file

@ -0,0 +1,20 @@
package com.nomadics9.ananas
import androidx.media3.common.MimeTypes
public fun setSubtitlesMimeTypes(codec: String): String {
return when (codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.TEXT_VTT
"ssa" -> MimeTypes.TEXT_SSA
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA
"srt" -> MimeTypes.APPLICATION_SUBRIP
"vtt" -> MimeTypes.TEXT_VTT
"ttml" -> MimeTypes.APPLICATION_TTML
"dfxp" -> MimeTypes.APPLICATION_TTML
"stl" -> MimeTypes.APPLICATION_TTML
"sbv" -> MimeTypes.APPLICATION_SUBRIP
else -> MimeTypes.TEXT_UNKNOWN
}
}

View file

@ -31,6 +31,7 @@ import com.nomadics9.ananas.models.VideoQuality
import com.nomadics9.ananas.mpv.MPVPlayer
import com.nomadics9.ananas.player.video.R
import com.nomadics9.ananas.repository.JellyfinRepository
import com.nomadics9.ananas.setSubtitlesMimeTypes
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@ -57,12 +58,11 @@ class PlayerActivityViewModel
private val application: Application,
private val jellyfinRepository: JellyfinRepository,
private val appPreferences: AppPreferences,
private val jellyfinApi: JellyfinApi,
private val savedStateHandle: SavedStateHandle,
) : ViewModel(),
Player.Listener {
val player: Player
private var originalHeight: Int = 0
private var originalResolution: Int? = null
private val _uiState =
MutableStateFlow(
@ -191,6 +191,21 @@ class PlayerActivityViewModel
).setSubtitleConfigurations(mediaSubtitles)
.build()
mediaItems.add(mediaItem)
player.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_READY) {
val videoSize = player.videoSize
val initialHeight = videoSize.height
val initialWidth = videoSize.width
originalResolution = initialHeight * initialWidth
Timber.d("Initial video size: $initialWidth x $initialHeight")
player.removeListener(this)
}
}
})
}
} catch (e: Exception) {
Timber.e(e)
@ -538,110 +553,76 @@ class PlayerActivityViewModel
val currentPosition = player.currentPosition
viewModelScope.launch {
val videoQuality = VideoQuality.fromString(quality)!!
try {
val deviceProfile =
jellyfinRepository.buildDeviceProfile(
VideoQuality.getBitrate(videoQuality),
"ts",
EncodingContext.STREAMING,
)
val playbackInfo =
jellyfinRepository.getPostedPlaybackInfo(
currentItem.itemId,
true,
deviceProfile,
VideoQuality.getBitrate(videoQuality),
)
val videoQuality = VideoQuality.fromString(quality)!!
val deviceProfile = jellyfinRepository.buildDeviceProfile(VideoQuality.getBitrate(videoQuality), "mkv", EncodingContext.STREAMING)
val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,VideoQuality.getBitrate(videoQuality))
val playSessionId = playbackInfo.content.playSessionId
if (playSessionId != null) {
jellyfinRepository.stopEncodingProcess(playSessionId)
}
val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true)
val externalSubtitles =
currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration
.Builder(externalSubtitle.uri)
// TODO: can maybe tidy the sub stuff up
val externalSubtitles = currentItem.externalSubtitles.map { externalSubtitle ->
MediaItem.SubtitleConfiguration.Builder(externalSubtitle.uri)
.setLabel(externalSubtitle.title.ifBlank { application.getString(R.string.external) })
.setLanguage(externalSubtitle.language.ifBlank { "Unknown" })
.setMimeType(externalSubtitle.mimeType)
.build()
}
val embeddedSubtitles =
mediaSources[currentMediaItemIndex]
.mediaStreams
val embeddedSubtitles = mediaSources[currentMediaItemIndex].mediaStreams
.filter { it.type == MediaStreamType.SUBTITLE && !it.isExternal && it.path != null }
.map { mediaStream ->
val test = mediaStream.codec
Timber.d("Deliver: %s", test)
var deliveryUrl = mediaStream.path
Timber.d("Deliverurl: %s", deliveryUrl)
// Not sure if still needed
if (mediaStream.codec == "webvtt") {
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")
}
MediaItem.SubtitleConfiguration
.Builder(Uri.parse(deliveryUrl))
.setMimeType(
when (mediaStream.codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.TEXT_VTT
"ssa" -> MimeTypes.TEXT_SSA
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA // ASS is a subtitle format that is essentially an extension of SSA
"srt" -> MimeTypes.APPLICATION_SUBRIP // SRT is another common name for SubRip
"vtt" -> MimeTypes.TEXT_VTT // VTT is a common extension for WebVTT
"ttml" -> MimeTypes.APPLICATION_TTML // TTML (Timed Text Markup Language)
"dfxp" -> MimeTypes.APPLICATION_TTML // DFXP is a profile of TTML
"stl" -> MimeTypes.APPLICATION_TTML // EBU STL (Subtitling Data Exchange Format)
"sbv" -> MimeTypes.APPLICATION_SUBRIP // YouTube's SBV format is similar to SubRip
else -> MimeTypes.TEXT_UNKNOWN
},
).setLanguage(mediaStream.language.ifBlank { "Unknown" })
deliveryUrl = deliveryUrl?.replace("Stream.srt", "Stream.vtt")}
MediaItem.SubtitleConfiguration.Builder(Uri.parse(deliveryUrl))
.setMimeType(setSubtitlesMimeTypes(mediaStream.codec))
.setLanguage(mediaStream.language.ifBlank { "Unknown" })
.setLabel("Embedded")
.build()
}.toMutableList()
}
.toMutableList()
val allSubtitles = embeddedSubtitles.apply { addAll(externalSubtitles) }
val url =
if (VideoQuality.getQualityString(videoQuality) == "Original") {
val allSubtitles =
if (VideoQuality.getIsOriginalQuality(videoQuality)) {
externalSubtitles
}else {
embeddedSubtitles.apply { addAll(externalSubtitles) }
}
val url = if (VideoQuality.getIsOriginalQuality(videoQuality)){
jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId)
} else {
val mediaSourceId = mediaSources[currentMediaItemIndex].id
val deviceId = jellyfinApi.api.deviceInfo.id
Timber.d("deviceid = %s", deviceId)
val url =
jellyfinRepository.getTranscodedVideoStream(
currentItem.itemId,
deviceId,
mediaSourceId,
playSessionId!!,
VideoQuality.getBitrate(videoQuality),
)
val deviceId = jellyfinRepository.getDeviceId()
val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, VideoQuality.getBitrate(videoQuality))
val uriBuilder = url.toUri().buildUpon()
val apiKey = jellyfinApi.api.accessToken
uriBuilder.appendQueryParameter("api_key", apiKey)
val apiKey = jellyfinRepository.getAccessToken()
uriBuilder.appendQueryParameter("api_key",apiKey )
val newUri = uriBuilder.build()
newUri.toString()
}
Timber.e("URI IS %s", url)
val mediaItemBuilder =
MediaItem
.Builder()
val mediaItemBuilder = MediaItem.Builder()
.setMediaId(currentItem.itemId.toString())
.setUri(url)
.setSubtitleConfigurations(allSubtitles)
.setMediaMetadata(
MediaMetadata
.Builder()
MediaMetadata.Builder()
.setTitle(currentItem.name)
.build(),
)
player.pause()
player.setMediaItem(mediaItemBuilder.build())
player.prepare()
@ -649,25 +630,21 @@ class PlayerActivityViewModel
playWhenReady = true
player.play()
val originalHeight =
mediaSources[currentMediaItemIndex]
.mediaStreams
.filter { it.type == MediaStreamType.VIDEO }
.map { mediaStream -> mediaStream.height }
.first() ?: 1080
// Store the original height
this@PlayerActivityViewModel.originalHeight = originalHeight
// isQualityChangeInProgress = true
//isQualityChangeInProgress = true
} catch (e: Exception) {
Timber.e(e)
}
}
}
fun getOriginalHeight(): Int = originalHeight
fun getOriginalResolution(): Int? {
return originalResolution
}
}
sealed interface PlayerEvents {
data object NavigateBack : PlayerEvents

View file

@ -18,6 +18,7 @@ import com.nomadics9.ananas.models.PlayerChapter
import com.nomadics9.ananas.models.PlayerItem
import com.nomadics9.ananas.models.TrickplayInfo
import com.nomadics9.ananas.repository.JellyfinRepository
import com.nomadics9.ananas.setSubtitlesMimeTypes
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
@ -136,6 +137,7 @@ class PlayerViewModel @Inject internal constructor(
} else {
mediaSources[mediaSourceIndex]
}
// Embedded Sub externally for offline playback
val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) {
mediaSource.mediaStreams
.filter { mediaStream ->
@ -146,13 +148,7 @@ class PlayerViewModel @Inject internal constructor(
mediaStream.title,
mediaStream.language,
Uri.parse(mediaStream.path!!),
when (mediaStream.codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.APPLICATION_SUBRIP
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA
else -> MimeTypes.TEXT_UNKNOWN
},
setSubtitlesMimeTypes(mediaStream.codec),
)
}
}else {
@ -165,13 +161,7 @@ class PlayerViewModel @Inject internal constructor(
mediaStream.title,
mediaStream.language,
Uri.parse(mediaStream.path!!),
when (mediaStream.codec) {
"subrip" -> MimeTypes.APPLICATION_SUBRIP
"webvtt" -> MimeTypes.APPLICATION_SUBRIP
"pgs" -> MimeTypes.APPLICATION_PGS
"ass" -> MimeTypes.TEXT_SSA
else -> MimeTypes.TEXT_UNKNOWN
},
setSubtitlesMimeTypes(mediaStream.codec)
)
}
}

View file

@ -121,6 +121,11 @@ constructor(
Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT.toString(),
)!!.toLongOrNull() ?: Constants.NETWORK_DEFAULT_SOCKET_TIMEOUT
val transcodeCodec get() = sharedPreferences.getString(
Constants.PREF_NETWORK_CODEC,
Constants.NETWORK_DEFAULT_CODEC,
)
// Cache
val imageCache get() = sharedPreferences.getBoolean(
Constants.PREF_IMAGE_CACHE,

View file

@ -43,6 +43,7 @@ object Constants {
const val PREF_NETWORK_REQUEST_TIMEOUT = "pref_network_request_timeout"
const val PREF_NETWORK_CONNECT_TIMEOUT = "pref_network_connect_timeout"
const val PREF_NETWORK_SOCKET_TIMEOUT = "pref_network_socket_timeout"
const val PREF_NETWORK_CODEC = "pref_network_codec"
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"
@ -63,6 +64,7 @@ object Constants {
const val NETWORK_DEFAULT_REQUEST_TIMEOUT = 30_000L
const val NETWORK_DEFAULT_CONNECT_TIMEOUT = 6_000L
const val NETWORK_DEFAULT_SOCKET_TIMEOUT = 10_000L
const val NETWORK_DEFAULT_CODEC = "h264"
// sorting
// This values must correspond to a SortString from [SortBy]

1
version Normal file
View file

@ -0,0 +1 @@
0.10.6