From ba03ef4e9f9848473bcd3be9373839d62bd92810 Mon Sep 17 00:00:00 2001 From: Tio Date: Fri, 28 Jun 2024 17:58:38 +0000 Subject: [PATCH 01/32] 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/ --- core/src/main/res/values-pt-rBR/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml index de56b64c..dc8fca6c 100644 --- a/core/src/main/res/values-pt-rBR/strings.xml +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -189,4 +189,5 @@ Abra o vídeo no modo maximizado por padrão Marcadores de capítulo Exibir marcadores de capítulo na barra de tempo + Esta coleção não contém nenhuma mídia \ No newline at end of file From 32c6d220357aa9859d4de0fdb2bd107d5b9b1e0c Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 28 Jun 2024 11:39:09 +0000 Subject: [PATCH 02/32] 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/ --- core/src/main/res/values-ar/strings.xml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/src/main/res/values-ar/strings.xml b/core/src/main/res/values-ar/strings.xml index a6b3daec..87a35e43 100644 --- a/core/src/main/res/values-ar/strings.xml +++ b/core/src/main/res/values-ar/strings.xml @@ -1,2 +1,20 @@ - \ No newline at end of file + + اصدار الخادم قديم: %1$s. الرجاء تحديث الخادم + ليس خادم جيلي فن:%1$s + اصدار الخادم غير مدعوم: %1$s. الرجاء تحديث الخادم + رد الخادم بطيء: %1$s + تسجيل دخول + اسم المستخدم او الكلمه السريه غير صحيحه + اختر الخادم + عنوان الخادم + اسم المستخدم + الكلمه السريه + اتصل + تسجيل دخول + حذف الخادم + اضافه خادم + الخادم غير موجود + عنوان الخادم فاضي + الخادم غير معرف بالid , يبدو انه هناك خلل في الخادم + \ No newline at end of file From 36891e7682849154bc443a4a620e2eeb3a9cbbee Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Sat, 29 Jun 2024 23:00:11 +0200 Subject: [PATCH 03/32] test: fix main flow test and update dependencies --- .../kotlin/dev/jdtech/jellyfin/MainActivityTest.kt | 6 +++--- gradle/libs.versions.toml | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/phone/src/androidTest/kotlin/dev/jdtech/jellyfin/MainActivityTest.kt b/app/phone/src/androidTest/kotlin/dev/jdtech/jellyfin/MainActivityTest.kt index 486b6759..831c2e12 100644 --- a/app/phone/src/androidTest/kotlin/dev/jdtech/jellyfin/MainActivityTest.kt +++ b/app/phone/src/androidTest/kotlin/dev/jdtech/jellyfin/MainActivityTest.kt @@ -76,9 +76,9 @@ class MainActivityTest { waitForElement(allOf(withText("Movies"), isDisplayed())) onView(withText("Movies")).perform(click()) - // Navigate to Battle of the Stars - waitForElement(allOf(withText("Battle of the Stars"), isDisplayed())) - onView(withText("Battle of the Stars")).perform(click()) + // Navigate to The Boy in the Plastic Bubble + waitForElement(allOf(withText("The Boy in the Plastic Bubble"), isDisplayed())) + onView(withText("The Boy in the Plastic Bubble")).perform(click()) // Play the movie waitForElement(allOf(withId(R.id.play_button), isEnabled())) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bb99237..03e10f30 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,11 +17,11 @@ androidx-preference = "1.2.1" androidx-recyclerview = "1.3.2" androidx-room = "2.6.1" androidx-swiperefreshlayout = "1.1.0" -androidx-test-core = "1.5.0" -androidx-test-expresso = "3.5.1" -androidx-test-junit = "1.1.5" -androidx-test-rules = "1.5.0" -androidx-test-runner = "1.5.2" +androidx-test-core = "1.6.1" +androidx-test-expresso = "3.6.1" +androidx-test-junit = "1.2.1" +androidx-test-rules = "1.6.1" +androidx-test-runner = "1.6.1" androidx-tv = "1.0.0-alpha10" androidx-tv-material3 = "1.0.0-beta01" androidx-work = "2.9.0" From 544e6432f529ee0a17503941c8c442cfcfbee43f Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Sun, 30 Jun 2024 18:47:03 +0200 Subject: [PATCH 04/32] chore(deps): update libmpv libmpv 0.2.0 -> 0.3.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03e10f30..e9c0888a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,7 +34,7 @@ kotlin = "2.0.0" kotlinx-serialization = "1.7.0" ksp = "2.0.0-1.0.22" ktlint = "12.1.1" -libmpv = "0.2.0" +libmpv = "0.3.0" material = "1.12.0" media3-ffmpeg-decoder = "1.3.1+2" timber = "5.0.1" From 44fe7dac3540da2b313ac3e9c2394164f94b2b2b Mon Sep 17 00:00:00 2001 From: adiskill Date: Sat, 29 Jun 2024 18:19:24 +0000 Subject: [PATCH 05/32] 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/ --- core/src/main/res/values-sk/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/res/values-sk/strings.xml b/core/src/main/res/values-sk/strings.xml index 4e6fee3e..a12626aa 100644 --- a/core/src/main/res/values-sk/strings.xml +++ b/core/src/main/res/values-sk/strings.xml @@ -190,4 +190,6 @@ Dlhé stlačenie na ľavej / pravej strane pre preskočenie kapitoly (prepíše gesto na 2x rýchlosť) Značky kapitol Zobraz značky kapitol na časovej osi + Trickplay + Zobrazenie náhľadu počas prechádzania časovej osi \ No newline at end of file From 2bfe4388eacb1d231c0e9b1d290131062a388748 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:38:31 +0200 Subject: [PATCH 06/32] chore(deps): update aboutlibraries to v11.2.2 (#785) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9c0888a..98a3b8c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -aboutlibraries = "11.2.1" +aboutlibraries = "11.2.2" android-desugar-jdk-libs = "2.0.4" android-plugin = "8.5.0" androidx-activity = "1.9.0" From 2dd65705af4c6b86ad8adcae99fcd5014ce6c243 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 12:39:35 +0200 Subject: [PATCH 07/32] 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> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98a3b8c0..8dc6cbe4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ compose-destinations = "1.10.2" jellyfin = "1.5.0-beta.4" junit = "4.13.2" kotlin = "2.0.0" -kotlinx-serialization = "1.7.0" +kotlinx-serialization = "1.7.1" ksp = "2.0.0-1.0.22" ktlint = "12.1.1" libmpv = "0.3.0" From 785db44744bad2b7de950902075844e45040de48 Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Sun, 7 Jul 2024 14:01:18 +0200 Subject: [PATCH 08/32] chore(deps): update androidx lifecyle to 2.8.3 lifecycle 2.8.2 -> 2.8.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8dc6cbe4..231e119f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ androidx-compose-material3 = "1.2.1" androidx-constraintlayout = "2.1.4" androidx-core = "1.13.1" androidx-hilt = "1.2.0" -androidx-lifecycle = "2.8.2" +androidx-lifecycle = "2.8.3" androidx-media3 = "1.3.1" androidx-navigation = "2.7.7" androidx-paging = "3.3.0" From 03023d8c9f9a0ded90178e7eecf5a141d27eae4c Mon Sep 17 00:00:00 2001 From: Jasper Date: Tue, 9 Jul 2024 18:18:48 +0000 Subject: [PATCH 09/32] 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/ --- core/src/main/res/values-nl/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml index dd664fcd..c96d29fb 100644 --- a/core/src/main/res/values-nl/strings.xml +++ b/core/src/main/res/values-nl/strings.xml @@ -189,4 +189,5 @@ Afspelen Aan favorieten toevoegen Van favorieten verwijderen + Deze collectie bevat geen media \ No newline at end of file From 0c94c3c7dce45046d373b8ee025326a9786e4725 Mon Sep 17 00:00:00 2001 From: Suyash Mahar Date: Fri, 12 Jul 2024 04:20:24 +0200 Subject: [PATCH 10/32] chore(translate): add (Hindi) --- core/src/main/res/values-hi/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 core/src/main/res/values-hi/strings.xml diff --git a/core/src/main/res/values-hi/strings.xml b/core/src/main/res/values-hi/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/core/src/main/res/values-hi/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From ea1163d25d0d3d57a239213a5575f47348f98afe Mon Sep 17 00:00:00 2001 From: Suyash Mahar Date: Fri, 12 Jul 2024 02:18:56 +0000 Subject: [PATCH 11/32] 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/ --- core/src/main/res/values-fr/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml index f12287a9..d39e8bb2 100644 --- a/core/src/main/res/values-fr/strings.xml +++ b/core/src/main/res/values-fr/strings.xml @@ -190,4 +190,5 @@ Regarder la bande annonce Marquer comme joué Retirer des favoris + Afficher les images de prévisualisation pendant le balayage \ No newline at end of file From e00156cd1c9bfe144e1a6145e3fcb1d0326c8cfe Mon Sep 17 00:00:00 2001 From: Suyash Mahar Date: Fri, 12 Jul 2024 02:43:03 +0000 Subject: [PATCH 12/32] 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/ --- core/src/main/res/values-hi/strings.xml | 117 +++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/core/src/main/res/values-hi/strings.xml b/core/src/main/res/values-hi/strings.xml index a6b3daec..a6121e3d 100644 --- a/core/src/main/res/values-hi/strings.xml +++ b/core/src/main/res/values-hi/strings.xml @@ -1,2 +1,117 @@ - \ No newline at end of file + + अगला + आपके पास कोई पसंदीदा नहीं है + जेलीफिन बैनर + सर्वर जोड़ें + जेलीफिन सर्वर नहीं है: %1$s + सर्वर की प्रतिक्रिया बहुत धीमी है: %1$s + सर्वर का पता खाली है + सर्वर नहीं मिला + लॉगिन + गलत उपयोगकर्ता का नाम या पासवर्ड + सर्वर चुनें + सर्वर का पता + उपयोगकर्ता का नाम + पासवर्ड + कनेक्ट करें + लॉगिन + सर्वर हटाएं + उपयोगकर्ता हटाएं + सर्वर का पता हटाएं + क्या आप सुनिश्चित हैं कि आप सर्वर का पता %1$s हटाना चाहते हैं + हटाएं + रद्द करें + मुख्य पृष्ठ + मेरा मीडिया + सेटिंग्स + डाउनलोड्स + सभी देखें + दोबारा प्रयास करें + शैलियाँ + निर्देशक + लेखक + कलाकार और क्रू + सीज़न + ट्रेलर देखें + पसंदीदा + देखे हुए एपिसोड का संकेतक + डाउनलोड संकेतक + %1$d. %2$s + %1$d-%2$d. %3$s + स%1$d:ए%2$d - %3$s + जारी रखें देखना + नवीनतम %1$s + लाइब्रेरीज़ + इस संग्रह में कोई मीडिया नहीं है + खोजें + भाषा + ऑडियो की पसंदीदा भाषा + उपशीर्षक की पसंदीदा भाषा + ऐप की भाषा + प्लेयर + डाउनलोड्स + दिखावट + उपकरण का नाम + उपकरण + कैश (cache) + कैश (cache) का साइज़ (MB) + विस्तार से देखें + विस्तार से देखें + ऐप के बारे में + गोपनीयता नीति + ऐप की जानकारी + फिल्में, शो, एपिसोड इत्यादि खोजें + एमपीवी (mpv) प्लेयर + रोमिंग के दौरान डाउनलोड करें + डाउनलोड + फिल्में + विवरण + टीवी शो + छिपाएं + क्रमबद्धता + साझा करें + इशारे + प्लेयर के इशारे + ज़ूम के इशारे + वीडियो को स्क्रीन में भरने के लिए पिंच करें + खोजने (सीक) का इशारा + चमक के स्तर को याद रखें + नाम + वर्ज़न चुने + [%1$s] %2$s (%3$s) + नेटवर्क + उपयोगकर्ता + असमर्थित सर्वर संस्करण: %1$s। कृपया अपने सर्वर को अपडेट करें। + थर्ड-पार्टी नेटिव जेलीफिन ऐप + सर्वर संस्करण पुराना है: %1$s। कृपया अपने सर्वर को अपडेट करें। + क्या आप सुनिश्चित हैं कि आप उपयोगकर्ता %1$s को हटाना चाहते हैं + स%1$d:ए%2$d-%3$d - %4$s + श्रृंखला पोस्टर + आपने कुछ भी डाउनलोड नहीं किया है + सर्वर के पास कोई आईडी नहीं है, सर्वर में कुछ गड़बड़ हो सकती है + प्लेयर आइटम्स तैयार करते समय त्रुटि हुई। + क्या आप सुनिश्चित हैं कि आप सर्वर %1$s को हटाना चाहते हैं + पसंदीदा + डेटा लोड करने में त्रुटि + देखा हुआ या न देखा हुआ चिह्नित करें + सर्वर्स + अज्ञात त्रुटि + मीडिया चलाएं + डिस्क पर इमेजेस कैश (cache) करें ताकि लोडिंग टाइम तेज हो। यह ऐप रीस्टार्ट के बाद प्रभावी होगा। + मोबाइल डेटा का उपयोग करके डाउनलोड करें + अध्यायों को छोड़ने के लिए बाएँ / दाएँ हिस्से पर लंबे समय तक दबाएँ (2x गति इशारे को ओवरराइड करता है) + %1$d मीन + कोई भी खोज परिणाम नहीं मिले + थीम + एपिसोड + कैश (cache) इमेजेस + जेलीफिन सर्वर से छवियों को संग्रहीत करने के लिए ऐप आपके डिस्क की इस एमबी का उपयोग करेगी। धीमे नेटवर्क पर बड़े मान फायदेमंद हो सकता हैं। + स्क्रीन के दाएं हिस्से पर ऊपर और नीचे स्वाइप करके वॉल्यूम बदलें और बाएं हिस्से पर स्वाइप करके चमक बदलें + उपशीर्षक + वीडियो चलाने के लिए प्रयोगात्मक एमपीवी (mpv) प्लेयर का उपयोग करें। एमपीवी में अधिक वीडियो, ऑडियो और सबटाइटल कोडेक्स के लिए समर्थन है। + %1$s पृष्ठभूमि + बंद करें + आगे या पीछे खोजने (सीक) के लिए क्षैतिज रूप से स्वाइप करें + %1$s पोस्टर + \ No newline at end of file From 1267f9809d9a6130b05225cdedff63a49929ef59 Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Sat, 13 Jul 2024 16:52:03 +0200 Subject: [PATCH 13/32] 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 --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 231e119f..84ce78bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] aboutlibraries = "11.2.2" android-desugar-jdk-libs = "2.0.4" -android-plugin = "8.5.0" +android-plugin = "8.5.1" androidx-activity = "1.9.0" androidx-appcompat = "1.7.0" androidx-compose-bom = "2024.06.00" @@ -22,13 +22,13 @@ androidx-test-expresso = "3.6.1" androidx-test-junit = "1.2.1" androidx-test-rules = "1.6.1" androidx-test-runner = "1.6.1" -androidx-tv = "1.0.0-alpha10" -androidx-tv-material3 = "1.0.0-beta01" +androidx-tv = "1.0.0-alpha11" +androidx-tv-material3 = "1.0.0-rc01" androidx-work = "2.9.0" coil = "2.6.0" hilt = "2.51.1" compose-destinations = "1.10.2" -jellyfin = "1.5.0-beta.4" +jellyfin = "1.5.0" junit = "4.13.2" kotlin = "2.0.0" kotlinx-serialization = "1.7.1" From 307ce957c2e02bfe1e2acca38d95dfa0a1fb0189 Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Sat, 13 Jul 2024 17:12:56 +0200 Subject: [PATCH 14/32] chore: target SDK 35 --- .../java/dev/jdtech/jellyfin/MainActivity.kt | 18 ++++++++++++++++++ buildSrc/src/main/kotlin/Versions.kt | 6 +++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt index 163bc80c..28e1ea16 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt @@ -4,6 +4,9 @@ import android.os.Bundle import android.view.View import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding import androidx.navigation.NavController import androidx.navigation.NavGraph import androidx.navigation.fragment.NavHostFragment @@ -45,6 +48,21 @@ class MainActivity : AppCompatActivity() { scheduleUserDataSync() applyTheme() setupActivity() + + // Temp fix insets because SDK 35 enables edge to edge by default. This will probably be removed once we move to compose + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> + val bars = insets.getInsets( + WindowInsetsCompat.Type.systemBars() + or WindowInsetsCompat.Type.displayCutout() + ) + v.updatePadding( + left = bars.left, + top = bars.top, + right = bars.right, + bottom = bars.bottom, + ) + WindowInsetsCompat.CONSUMED + } } @OptIn(NavigationUiSaveStateControl::class) diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 33ed80aa..2f3b28bc 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -4,10 +4,10 @@ object Versions { const val appCode = 25 const val appName = "0.14.2" - const val compileSdk = 34 - const val buildTools = "34.0.0" + const val compileSdk = 35 + const val buildTools = "35.0.0" const val minSdk = 28 - const val targetSdk = 34 + const val targetSdk = 35 val java = JavaVersion.VERSION_17 From 15c1ac959304fd8afd9e9916033a395970a55004 Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Mon, 15 Jul 2024 22:18:09 +0200 Subject: [PATCH 15/32] 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) --- app/tv/build.gradle.kts | 6 ++---- .../dev/jdtech/jellyfin/ui/AddServerScreen.kt | 2 +- .../java/dev/jdtech/jellyfin/ui/HomeScreen.kt | 12 ++++++------ .../dev/jdtech/jellyfin/ui/LibrariesScreen.kt | 10 +++++----- .../java/dev/jdtech/jellyfin/ui/LibraryScreen.kt | 12 ++++++------ .../java/dev/jdtech/jellyfin/ui/LoginScreen.kt | 4 ++-- .../java/dev/jdtech/jellyfin/ui/PlayerScreen.kt | 2 +- .../java/dev/jdtech/jellyfin/ui/SeasonScreen.kt | 6 +++--- .../dev/jdtech/jellyfin/ui/ServerSelectScreen.kt | 6 +++--- .../java/dev/jdtech/jellyfin/ui/SettingsScreen.kt | 14 +++++++------- .../dev/jdtech/jellyfin/ui/SettingsSubScreen.kt | 6 +++--- .../java/dev/jdtech/jellyfin/ui/ShowScreen.kt | 15 ++++++++------- .../dev/jdtech/jellyfin/ui/UserSelectScreen.kt | 6 +++--- .../ui/components/SettingsDetailsSelectCard.kt | 4 ++-- .../ui/dialogs/VideoPlayerTrackSelectorDialog.kt | 6 +++--- core/build.gradle.kts | 4 ++-- .../dev/jdtech/jellyfin/utils/ComposeUtils.kt | 2 +- gradle/libs.versions.toml | 13 ++++++------- 18 files changed, 64 insertions(+), 66 deletions(-) diff --git a/app/tv/build.gradle.kts b/app/tv/build.gradle.kts index 3324bee0..b4aa064e 100644 --- a/app/tv/build.gradle.kts +++ b/app/tv/build.gradle.kts @@ -81,15 +81,14 @@ ktlint { } dependencies { - val composeBom = platform(libs.androidx.compose.bom) - implementation(projects.core) implementation(projects.data) implementation(projects.preferences) implementation(projects.player.core) implementation(projects.player.video) implementation(libs.androidx.activity.compose) - implementation(composeBom) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) implementation(libs.androidx.core) @@ -99,7 +98,6 @@ dependencies { implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.session) implementation(libs.androidx.paging.compose) - implementation(libs.androidx.tv.foundation) implementation(libs.androidx.tv.material) implementation(libs.coil.compose) implementation(libs.coil.svg) diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/AddServerScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/AddServerScreen.kt index acdacdce..a59595b4 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/AddServerScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/AddServerScreen.kt @@ -115,7 +115,7 @@ private fun AddServerScreenLayout( }, singleLine = true, keyboardOptions = KeyboardOptions( - autoCorrect = false, + autoCorrectEnabled = false, keyboardType = KeyboardType.Uri, imeAction = ImeAction.Go, ), diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/HomeScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/HomeScreen.kt index 4515a414..2fbb892b 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/HomeScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/HomeScreen.kt @@ -6,6 +6,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -19,9 +22,6 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.TvLazyRow -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.ramcosta.composedestinations.annotation.Destination @@ -107,7 +107,7 @@ private fun HomeScreenLayout( } else -> Unit } - TvLazyColumn( + LazyColumn( contentPadding = PaddingValues(bottom = MaterialTheme.spacings.large), modifier = Modifier .fillMaxSize() @@ -122,7 +122,7 @@ private fun HomeScreenLayout( modifier = Modifier.padding(start = MaterialTheme.spacings.large), ) Spacer(modifier = Modifier.height(MaterialTheme.spacings.medium)) - TvLazyRow( + LazyRow( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), contentPadding = PaddingValues(horizontal = MaterialTheme.spacings.large), ) { @@ -145,7 +145,7 @@ private fun HomeScreenLayout( modifier = Modifier.padding(start = MaterialTheme.spacings.large), ) Spacer(modifier = Modifier.height(MaterialTheme.spacings.medium)) - TvLazyRow( + LazyRow( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), contentPadding = PaddingValues(horizontal = MaterialTheme.spacings.large), ) { diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibrariesScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibrariesScreen.kt index 07e042fc..d412a0c1 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibrariesScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibrariesScreen.kt @@ -2,6 +2,9 @@ package dev.jdtech.jellyfin.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -14,9 +17,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.grid.TvGridCells -import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid -import androidx.tv.foundation.lazy.grid.items import androidx.tv.material3.MaterialTheme import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator @@ -72,8 +72,8 @@ private fun LibrariesScreenLayout( val focusRequester = remember { FocusRequester() } - TvLazyVerticalGrid( - columns = TvGridCells.Fixed(3), + LazyVerticalGrid( + columns = GridCells.Fixed(3), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.large), verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.large), contentPadding = PaddingValues( diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibraryScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibraryScreen.kt index 6b18565f..4af57365 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibraryScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LibraryScreen.kt @@ -3,6 +3,9 @@ package dev.jdtech.jellyfin.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -15,9 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems -import androidx.tv.foundation.lazy.grid.TvGridCells -import androidx.tv.foundation.lazy.grid.TvGridItemSpan -import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.ramcosta.composedestinations.annotation.Destination @@ -86,8 +86,8 @@ private fun LibraryScreenLayout( is LibraryViewModel.UiState.Loading -> Text(text = "LOADING") is LibraryViewModel.UiState.Normal -> { val items = uiState.items.collectAsLazyPagingItems() - TvLazyVerticalGrid( - columns = TvGridCells.Fixed(5), + LazyVerticalGrid( + columns = GridCells.Fixed(5), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), contentPadding = PaddingValues(horizontal = MaterialTheme.spacings.default * 2, vertical = MaterialTheme.spacings.large), @@ -95,7 +95,7 @@ private fun LibraryScreenLayout( .fillMaxSize() .focusRequester(focusRequester), ) { - item(span = { TvGridItemSpan(this.maxLineSpan) }) { + item(span = { GridItemSpan(this.maxLineSpan) }) { Text( text = libraryName, style = MaterialTheme.typography.displayMedium, diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LoginScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LoginScreen.kt index 6cbc210c..e0dd01d9 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LoginScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/LoginScreen.kt @@ -152,7 +152,7 @@ private fun LoginScreenLayout( label = { Text(text = stringResource(id = CoreR.string.edit_text_username_hint)) }, singleLine = true, keyboardOptions = KeyboardOptions( - autoCorrect = false, + autoCorrectEnabled = false, keyboardType = KeyboardType.Text, imeAction = ImeAction.Next, ), @@ -175,7 +175,7 @@ private fun LoginScreenLayout( label = { Text(text = stringResource(id = CoreR.string.edit_text_password_hint)) }, singleLine = true, keyboardOptions = KeyboardOptions( - autoCorrect = false, + autoCorrectEnabled = false, keyboardType = KeyboardType.Password, imeAction = ImeAction.Go, ), diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/PlayerScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/PlayerScreen.kt index 92cb0aae..af5a8c43 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/PlayerScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/PlayerScreen.kt @@ -18,12 +18,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SeasonScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SeasonScreen.kt index e54895e7..21c64f70 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SeasonScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SeasonScreen.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -16,8 +18,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.ramcosta.composedestinations.annotation.Destination @@ -109,7 +109,7 @@ private fun SeasonScreenLayout( style = MaterialTheme.typography.headlineMedium, ) } - TvLazyColumn( + LazyColumn( contentPadding = PaddingValues( top = MaterialTheme.spacings.large, bottom = MaterialTheme.spacings.large, diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ServerSelectScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ServerSelectScreen.kt index 4d3cc63c..e11f04ac 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ServerSelectScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ServerSelectScreen.kt @@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -31,8 +33,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.list.TvLazyRow -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Border import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.Icon @@ -172,7 +172,7 @@ private fun ServerSelectScreenLayout( style = MaterialTheme.typography.bodyMedium, ) } else { - TvLazyRow( + LazyRow( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.large), contentPadding = PaddingValues(horizontal = MaterialTheme.spacings.default), modifier = Modifier.focusRequester(focusRequester), diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsScreen.kt index 9e5866c0..f65bc835 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsScreen.kt @@ -3,6 +3,10 @@ package dev.jdtech.jellyfin.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -15,10 +19,6 @@ import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.grid.TvGridCells -import androidx.tv.foundation.lazy.grid.TvGridItemSpan -import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid -import androidx.tv.foundation.lazy.grid.items import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.ramcosta.composedestinations.annotation.Destination @@ -88,8 +88,8 @@ private fun SettingsScreenLayout( when (uiState) { is SettingsViewModel.UiState.Normal -> { - TvLazyVerticalGrid( - columns = TvGridCells.Fixed(3), + LazyVerticalGrid( + columns = GridCells.Fixed(3), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), contentPadding = PaddingValues(horizontal = MaterialTheme.spacings.default * 2, vertical = MaterialTheme.spacings.large), @@ -97,7 +97,7 @@ private fun SettingsScreenLayout( .fillMaxSize() .focusRequester(focusRequester), ) { - item(span = { TvGridItemSpan(this.maxLineSpan) }) { + item(span = { GridItemSpan(this.maxLineSpan) }) { Text( text = stringResource(id = CoreR.string.title_settings), style = MaterialTheme.typography.displayMedium, diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt index eec271ef..aeef7df1 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/SettingsSubScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -23,8 +25,6 @@ import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.ramcosta.composedestinations.annotation.Destination @@ -131,7 +131,7 @@ private fun SettingsSubScreenLayout( Row( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.large), ) { - TvLazyColumn( + LazyColumn( verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), contentPadding = PaddingValues(vertical = MaterialTheme.spacings.large), modifier = Modifier diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt index 77fb1464..95c35bef 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt @@ -1,5 +1,6 @@ package dev.jdtech.jellyfin.ui +import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.view.KeyEvent @@ -16,6 +17,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -44,10 +49,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toSize import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.TvLazyRow -import androidx.tv.foundation.lazy.list.items -import androidx.tv.foundation.lazy.list.rememberTvLazyListState import androidx.tv.material3.Button import androidx.tv.material3.Icon import androidx.tv.material3.LocalContentColor @@ -136,7 +137,7 @@ private fun ShowScreenLayout( ) { val focusRequester = remember { FocusRequester() } - val listState = rememberTvLazyListState() + val listState = rememberLazyListState() val listSize = remember { mutableIntStateOf(2) } var currentIndex by remember { mutableIntStateOf(0) } @@ -179,7 +180,7 @@ private fun ShowScreenLayout( ), ) } - TvLazyColumn( + LazyColumn( state = listState, contentPadding = PaddingValues(top = 112.dp, bottom = MaterialTheme.spacings.large), verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.medium), @@ -364,7 +365,7 @@ private fun ShowScreenLayout( } } item { - TvLazyRow( + LazyRow( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), contentPadding = PaddingValues(horizontal = MaterialTheme.spacings.default * 2), ) { diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/UserSelectScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/UserSelectScreen.kt index ed1b6e92..4a0679e3 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/UserSelectScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/UserSelectScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -29,8 +31,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.tv.foundation.lazy.list.TvLazyRow -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Border import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.Icon @@ -146,7 +146,7 @@ private fun UserSelectScreenLayout( style = MaterialTheme.typography.bodyMedium, ) } else { - TvLazyRow( + LazyRow( horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.default), contentPadding = PaddingValues(MaterialTheme.spacings.default), modifier = Modifier.focusRequester(focusRequester), diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsDetailsSelectCard.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsDetailsSelectCard.kt index 74d2b9ba..efc6aa19 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsDetailsSelectCard.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/components/SettingsDetailsSelectCard.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -18,7 +19,6 @@ import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.material3.Border import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceScale @@ -56,7 +56,7 @@ fun SettingsDetailsCard( Text(text = stringResource(id = it), style = MaterialTheme.typography.bodyMedium) } Spacer(modifier = Modifier.height(MaterialTheme.spacings.default)) - TvLazyColumn( + LazyColumn( verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.medium - MaterialTheme.spacings.extraSmall), contentPadding = PaddingValues(vertical = MaterialTheme.spacings.extraSmall), ) { diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dialogs/VideoPlayerTrackSelectorDialog.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dialogs/VideoPlayerTrackSelectorDialog.kt index 94176948..9393adc9 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dialogs/VideoPlayerTrackSelectorDialog.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/dialogs/VideoPlayerTrackSelectorDialog.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -19,8 +21,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.media3.common.C -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Border import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceScale @@ -65,7 +65,7 @@ fun VideoPlayerTrackSelectorDialog( style = MaterialTheme.typography.headlineMedium, ) Spacer(modifier = Modifier.height(MaterialTheme.spacings.medium)) - TvLazyColumn( + LazyColumn( verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacings.medium - MaterialTheme.spacings.extraSmall), contentPadding = PaddingValues(vertical = MaterialTheme.spacings.extraSmall), ) { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 76bfe312..f38277b9 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -49,13 +49,13 @@ ktlint { } dependencies { - val composeBom = platform(libs.androidx.compose.bom) + // val composeBom = platform(libs.androidx.compose.bom) implementation(projects.data) implementation(projects.preferences) implementation(projects.player.core) implementation(libs.androidx.appcompat) - implementation(composeBom) + // implementation(composeBom) implementation(libs.androidx.compose.ui) implementation(libs.androidx.core) implementation(libs.androidx.hilt.work) diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/ComposeUtils.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/ComposeUtils.kt index 00da8ca2..79e85a5b 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/ComposeUtils.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/ComposeUtils.kt @@ -6,8 +6,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle import kotlinx.coroutines.flow.Flow diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 84ce78bb..5797bad9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ android-desugar-jdk-libs = "2.0.4" android-plugin = "8.5.1" androidx-activity = "1.9.0" androidx-appcompat = "1.7.0" -androidx-compose-bom = "2024.06.00" +androidx-compose = "1.7.0-beta05" androidx-compose-material3 = "1.2.1" androidx-constraintlayout = "2.1.4" androidx-core = "1.13.1" @@ -22,7 +22,6 @@ androidx-test-expresso = "3.6.1" androidx-test-junit = "1.2.1" androidx-test-rules = "1.6.1" androidx-test-runner = "1.6.1" -androidx-tv = "1.0.0-alpha11" androidx-tv-material3 = "1.0.0-rc01" androidx-work = "2.9.0" coil = "2.6.0" @@ -46,11 +45,12 @@ android-desugar-jdk = { group = "com.android.tools", name = "desugar_jdk_libs", androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "androidx-activity" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidx-compose" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidx-compose" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidx-compose" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidx-compose" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "androidx-constraintlayout" } androidx-core = { group = "androidx.core", name = "core", version.ref = "androidx-core" } androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidx-hilt" } @@ -79,7 +79,6 @@ androidx-test-expresso = { group = "androidx.test.espresso", name = "espresso-co androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-junit" } androidx-test-rules = { group = "androidx.test" , name = "rules", version.ref = "androidx-test-rules" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidx-test-runner" } -androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "androidx-tv" } androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "androidx-tv-material3" } androidx-work = { group = "androidx.work", name = "work-runtime", version.ref = "androidx-work" } androidx-work-testing = { group = "androidx.work", name = "work-testing", version.ref = "androidx-work" } From 48d8b18bae32207bf72d53a399ee47d2311069df Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Mon, 15 Jul 2024 23:20:42 +0200 Subject: [PATCH 16/32] lint: run ktlintFormat --- app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt | 2 +- app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt index 28e1ea16..8353e750 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/MainActivity.kt @@ -53,7 +53,7 @@ class MainActivity : AppCompatActivity() { ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, insets -> val bars = insets.getInsets( WindowInsetsCompat.Type.systemBars() - or WindowInsetsCompat.Type.displayCutout() + or WindowInsetsCompat.Type.displayCutout(), ) v.updatePadding( left = bars.left, diff --git a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt index 95c35bef..7da7de6c 100644 --- a/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt +++ b/app/tv/src/main/java/dev/jdtech/jellyfin/ui/ShowScreen.kt @@ -1,6 +1,5 @@ package dev.jdtech.jellyfin.ui -import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.view.KeyEvent From eabe73813616628a790d615e787e7f68b3a0dc39 Mon Sep 17 00:00:00 2001 From: xxzp3 Date: Mon, 15 Jul 2024 09:50:24 +0000 Subject: [PATCH 17/32] 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/ --- core/src/main/res/values-da/strings.xml | 124 ++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/core/src/main/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml index f9900900..4df9aa44 100644 --- a/core/src/main/res/values-da/strings.xml +++ b/core/src/main/res/values-da/strings.xml @@ -1,4 +1,128 @@ Ekstern + Jellyfin banner + Tilføj server + Ikke en Jellyfin-server: %1$s + Server er for langsom til at svare: %1$s + Server ikke fundet + Tom server-adresse + Login + Forkert brugernavn eller adgangskode + Vælg server + Server-adresse + Brugernavn + Adgangskode + Forbind + Login + Fjern server + Er du sikker på, at du vil fjerne serveren %1$s + Fjern bruger + Er du sikker på, at du vil fjerne brugeren %1$s + Fjern server-adresse + Er du sikker på, at du vil fjerne server-adressen %1$s + Fjern + Annuller + Hjem + Favoritter + Indstillinger + Downloads + Se alle + Fejl i indlæsning af data + Prøv igen + Genrer + Sæsoner + Se traileren + Favorit + Næste + Biblioteker + Sprog + Enhed + Tema + Om + Privatlivspolitik + App info + mpv player + Film + TV-serier + Episoder + Skjul + Sorter efter + Luk + Del + %1$d min + Vælg version + Faldende + Brug Material You-dynamiske farver (kun tilgængeligt på Android 12+) + Undertekster + Dynamiske farver + Følg system + Picture-in-picture + Tilføj + Fejl under download + Gå online + Vælg lagerlokation + Lagerlokation er utilgængelig + Intern + %1$s (%2$d MB ledig) + Stop download + Ingen brugere fundet + Ingen servere fundet + Marker som ikke-afspillet + Marker som afspillet + Søg film, serier, episoder… + Se detaljer + Vis kapitelmarkører på tidslinjen + Størrelse + Vis Ekstra Info + Viser detaljeret information om Lyd, Video og Undertekster + Ingen forbindelse til Jellyfin-server, slå Offline Mode til for at se offline + Er du sikker på, at du vil annullere downloadningen? + Du har ingen favoritter + Du har ikke noget downloadet + Søg + Ingen søgeresultater + Foretrukket sprog for lyd + Foretrukket sprog for undertekster + Servere + Afspiller + Downloads + Udseende + Enhedsnavn + Cache + Cache billeder + Cache billeder på disk for at forbedre loading-tiden. Tager effekt efter app-genstart. + Cache-størrelse (MB) + Se detaljer + Ukendt fejl + Download med mobildata + Download ved roaming + Brug den eksperimentelle mpv-afspiller til at afspille videoer. mpv understøtter flere codecs til video, lyd og undertekster. + Download + Detaljer + Swipe op og ned i højre side af skærmen for at ændre lydstyrke, og i venstre side for at ændre lysstyrke + Husk lysstyrkeniveau + Titel + Dato tilføjet + Dato afspillet + Stigende + Lyst + Mørkt + Netværk + Brugere + Tilføj bruger + Kapitelmarkører + Adresser + Tilføj adresse + Tilføj server-adresse + Video + Lyd + Undertekster + Forbereder download + Annuller download + Vælg bruger + Afspil + Se trailer + Tilføj til favoritter + Fjern fra favoritter \ No newline at end of file From 45d4b88738fb1452b45107b061ad2340fcb36e9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:08:26 +0200 Subject: [PATCH 18/32] chore(deps): update dependency gradle to v8.9 (#789) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/wrapper/gradle-wrapper.jar | Bin 43453 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 5 ++++- gradlew.bat | 2 ++ 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3d4ba8a0da8d277868979cfbc8ad796..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 8703 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 Ojj7^59?Sdh$^Qpp*~F>< delta 8662 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4413138..09523c0e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index b740cf13..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e4..9b42019c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From db79b506293511ade4cdecfd89f41f2688bd7953 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:20:48 +0200 Subject: [PATCH 19/32] 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> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5797bad9..df61213c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ jellyfin = "1.5.0" junit = "4.13.2" kotlin = "2.0.0" kotlinx-serialization = "1.7.1" -ksp = "2.0.0-1.0.22" +ksp = "2.0.0-1.0.23" ktlint = "12.1.1" libmpv = "0.3.0" material = "1.12.0" From ccc6788a02c9151917d500a06d17f4df67f8669e Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 02:20:55 +0300 Subject: [PATCH 20/32] feat: Transcoding stream in player selection /code: prep repo for next commit transcoding downloads --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 22 +++ .../src/main/res/layout/exo_main_controls.xml | 18 ++ core/src/main/res/drawable/ic_quality.xml | 35 ++++ .../jellyfin/repository/JellyfinRepository.kt | 20 +- .../repository/JellyfinRepositoryImpl.kt | 176 +++++++++++++++++- .../JellyfinRepositoryOfflineImpl.kt | 56 +++++- .../viewmodels/PlayerActivityViewModel.kt | 141 ++++++++++++++ .../jellyfin/viewmodels/PlayerViewModel.kt | 25 ++- 8 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 core/src/main/res/drawable/ic_quality.xml diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index e21c79b3..891170e8 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -33,6 +33,7 @@ import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerControlView import androidx.media3.ui.PlayerView import androidx.navigation.navArgs +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment @@ -82,6 +83,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 @@ -342,6 +347,23 @@ class PlayerActivity : BasePlayerActivity() { } catch (_: IllegalArgumentException) { } } + private fun showQualitySelectionDialog() { + val height = viewModel.getOriginalHeight() // TODO: rewrite getting height stuff I don't like that its only update after changing quality + val qualities = when (height) { + 0 -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") + in 1001..1999 -> arrayOf("Auto", "Original (1080p) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") + in 2000..3000 -> arrayOf("Auto", "Original (4K) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") + else -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") + } + MaterialAlertDialogBuilder(this) + .setTitle("Select Video Quality") + .setItems(qualities) { _, which -> + val selectedQuality = qualities[which] + viewModel.changeVideoQuality(selectedQuality) + } + .show() + } + override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration, diff --git a/app/phone/src/main/res/layout/exo_main_controls.xml b/app/phone/src/main/res/layout/exo_main_controls.xml index 00431e70..b136be35 100644 --- a/app/phone/src/main/res/layout/exo_main_controls.xml +++ b/app/phone/src/main/res/layout/exo_main_controls.xml @@ -73,6 +73,24 @@ android:layout_height="0dp" android:layout_weight="1" /> + + + + + + + + + + diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index e2f117a3..2b4380c0 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -11,9 +11,13 @@ import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.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.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 @@ -81,7 +85,7 @@ interface JellyfinRepository { suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List - suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String + suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String suspend fun getIntroTimestamps(itemId: UUID): Intro? @@ -112,4 +116,18 @@ interface JellyfinRepository { suspend fun getDownloads(): List fun getUserId(): UUID + + suspend fun getDeviceId(): String + + suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair + + suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile + + suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String + + suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String + + suspend fun getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response + + suspend fun stopEncodingProcess(playSessionId: String) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 995619d2..ae178c7b 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -28,23 +28,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.dynamicHlsApi 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.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 @@ -322,13 +334,14 @@ class JellyfinRepositoryImpl( sources } - override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String = + override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String = withContext(Dispatchers.IO) { try { jellyfinApi.videosApi.getVideoStreamUrl( itemId, static = true, mediaSourceId = mediaSourceId, + playSessionId = playSessionId ) } catch (e: Exception) { Timber.e(e) @@ -536,4 +549,165 @@ class JellyfinRepositoryImpl( override fun getUserId(): UUID { return jellyfinApi.userId!! } + + + override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair { + return when (transcodeResolution) { + 1080 -> 8000000 to 384000 // Adjusted for personal can be other values + 720 -> 2000000 to 384000 // 720p + 480 -> 1000000 to 384000 // 480p + 360 -> 800000 to 128000 // 360p + else -> 12000000 to 384000 // its adaptive but setting max here + } + } + + 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 { + 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, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String { + val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl( + itemId, + static = false, + deviceId = deviceId, + mediaSourceId = mediaSourceId, + playSessionId = playSessionId, + videoBitRate = videoBitrate, + audioBitRate = 384000, + videoCodec = "hevc", + audioCodec = "aac,ac3,eac3", + container = container, + startTimeTicks = 0, + copyTimestamps = true, + subtitleMethod = SubtitleDeliveryMethod.EXTERNAL + ) + return url + } + + override suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String { + val isAuto = videoBitrate == 12000000 + val url = if (!isAuto) { + jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( + itemId, + static = false, + deviceId = deviceId, + mediaSourceId = mediaSourceId, + playSessionId = playSessionId, + videoBitRate = videoBitrate, + enableAdaptiveBitrateStreaming = false, + audioBitRate = 384000, //could also be passed with audioBitrate but i preferred not as its not much data anyways + videoCodec = "hevc,h264", + audioCodec = "aac,ac3,eac3", + startTimeTicks = 0, + copyTimestamps = true, + subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, + context = EncodingContext.STREAMING, + segmentContainer = "ts", + transcodeReasons = "ContainerBitrateExceedsLimit", + ) + } else { + jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( + itemId, + static = false, + deviceId = deviceId, + mediaSourceId = mediaSourceId, + playSessionId = playSessionId, + enableAdaptiveBitrateStreaming = true, + videoCodec = "hevc", + audioCodec = "aac,ac3,eac3", + startTimeTicks = 0, + copyTimestamps = true, + subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, + context = EncodingContext.STREAMING, + segmentContainer = "ts", + transcodeReasons = "ContainerBitrateExceedsLimit", + ) + } + return url + } + + + override suspend fun getDeviceId(): String { + val devices = jellyfinApi.devicesApi.getDevices(getUserId()) + return devices.content.items?.firstOrNull()?.id!! + } + + override suspend fun stopEncodingProcess(playSessionId: String) { + val deviceId = getDeviceId() + jellyfinApi.api.hlsSegmentApi.stopEncodingProcess( + deviceId = deviceId, + playSessionId = playSessionId + ) + } + } + + diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 0a78ec47..6901c09d 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -23,9 +23,13 @@ import dev.jdtech.jellyfin.models.toIntro 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 @@ -173,7 +177,7 @@ class JellyfinRepositoryOfflineImpl( database.getSources(itemId).map { it.toFindroidSource(database) } } - override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String): String { + override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String { TODO("Not yet implemented") } @@ -285,4 +289,54 @@ class JellyfinRepositoryOfflineImpl( override fun getUserId(): UUID { return jellyfinApi.userId!! } + + override suspend fun getDeviceId(): String { + TODO("Not yet implemented") + } + + override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair { + 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, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: Int, + container: String + ): String { + TODO("Not yet implemented") + } + + override suspend fun getTranscodedVideoStream( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: Int + ): String { + TODO("Not yet implemented") + } + + override suspend fun getPostedPlaybackInfo( + itemId: UUID, + enableDirectStream: Boolean, + deviceProfile: DeviceProfile, + maxBitrate: Int + ): Response { + TODO("Not yet implemented") + } + + override suspend fun stopEncodingProcess(playSessionId: String) { + TODO("Not yet implemented") + } } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 37b1ed42..e804df6e 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -3,8 +3,10 @@ package dev.jdtech.jellyfin.viewmodels import android.app.Application import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.net.Uri import android.os.Handler import android.os.Looper +import androidx.core.net.toUri import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -12,6 +14,7 @@ import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata +import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionParameters @@ -20,6 +23,7 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.AppPreferences +import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem @@ -38,6 +42,8 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.jellyfin.sdk.model.api.EncodingContext +import org.jellyfin.sdk.model.api.MediaStreamType import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -49,10 +55,12 @@ 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 { val player: Player + private var originalHeight: Int = 0 private val _uiState = MutableStateFlow( UiState( @@ -455,8 +463,141 @@ constructor( super.onIsPlayingChanged(isPlaying) eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying)) } + + private fun getTranscodeResolutions(preferredQuality: String): Int { + return when (preferredQuality) { + "1080p" -> 1080 // TODO: 1080p this logic is based on 1080p being original + "720p - 2Mbps" -> 720 + "480p - 1Mbps" -> 480 + "360p - 800kbps" -> 360 + "Auto" -> 1 + else -> 1080 //default to Original + } + } + + fun changeVideoQuality(quality: String) { + val mediaId = player.currentMediaItem?.mediaId ?: return + val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return + val currentPosition = player.currentPosition + + viewModelScope.launch { + try { + val transcodingResolution = getTranscodeResolutions(quality) + val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate( + transcodingResolution + ) + val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "mkv", EncodingContext.STREAMING) + val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,videoBitRate) + val playSessionId = playbackInfo.content.playSessionId + if (playSessionId != null) { + jellyfinRepository.stopEncodingProcess(playSessionId) + } + val mediaSources = jellyfinRepository.getMediaSources(currentItem.itemId, true) + + // 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 + .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) + 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 + "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 + } + ) + .setLanguage(mediaStream.language.ifBlank { "Unknown" }) + .setLabel("Embedded") + .build() + } + .toMutableList() + + + val allSubtitles = + if (transcodingResolution == 1080) { + externalSubtitles + }else { + embeddedSubtitles.apply { addAll(externalSubtitles) } + } + + val url = if (transcodingResolution == 1080){ + jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) + } else { + val mediaSourceId = mediaSources[currentMediaItemIndex].id + val deviceId = jellyfinRepository.getDeviceId() + val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, videoBitRate) + val uriBuilder = url.toUri().buildUpon() + val apiKey = jellyfinApi.api.accessToken // TODO: add in repo + uriBuilder.appendQueryParameter("api_key",apiKey ) + val newUri = uriBuilder.build() + newUri.toString() + } + + + + Timber.e("URI IS %s", url) + val mediaItemBuilder = MediaItem.Builder() + .setMediaId(currentItem.itemId.toString()) + .setUri(url) + .setSubtitleConfigurations(allSubtitles) + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle(currentItem.name) + .build(), + ) + + + player.pause() + player.setMediaItem(mediaItemBuilder.build()) + player.prepare() + player.seekTo(currentPosition) + 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 + } catch (e: Exception) { + Timber.e(e) + } + } + } + + fun getOriginalHeight(): Int { + return originalHeight + } } + sealed interface PlayerEvents { data object NavigateBack : PlayerEvents data class IsPlayingChanged(val isPlaying: Boolean) : PlayerEvents diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 9b3f76ff..50a0fc1c 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -136,7 +136,28 @@ class PlayerViewModel @Inject internal constructor( } else { mediaSources[mediaSourceIndex] } - val externalSubtitles = mediaSource.mediaStreams + // Embedded Sub externally for offline prep next commit + val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) { + mediaSource.mediaStreams + .filter { mediaStream -> + mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.path.isNullOrBlank() + } + .map { mediaStream -> + ExternalSubtitle( + 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 + }, + ) + } + }else { + mediaSource.mediaStreams .filter { mediaStream -> mediaStream.isExternal && mediaStream.type == MediaStreamType.SUBTITLE && !mediaStream.path.isNullOrBlank() } @@ -148,11 +169,13 @@ class PlayerViewModel @Inject internal constructor( when (mediaStream.codec) { "subrip" -> MimeTypes.APPLICATION_SUBRIP "webvtt" -> MimeTypes.APPLICATION_SUBRIP + "pgs" -> MimeTypes.APPLICATION_PGS "ass" -> MimeTypes.TEXT_SSA else -> MimeTypes.TEXT_UNKNOWN }, ) } + } val trickplayInfo = when (this) { is FindroidSources -> { this.trickplayInfo?.get(mediaSource.id)?.let { From 062781a43dabc0eea9fc724dcaa384d3b37990df Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 03:44:43 +0300 Subject: [PATCH 21/32] feat: Download transcoded media --- .../fragments/EpisodeBottomSheetFragment.kt | 153 +++++++++++------- .../jellyfin/fragments/MovieFragment.kt | 153 +++++++++++------- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 148 ++++++++++++++--- core/src/main/res/values/string_arrays.xml | 12 ++ .../res/xml/fragment_settings_downloads.xml | 12 ++ .../dev/jdtech/jellyfin/AppPreferences.kt | 13 ++ .../java/dev/jdtech/jellyfin/Constants.kt | 2 + 7 files changed, 357 insertions(+), 136 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 17c61caf..925f70f3 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -157,70 +157,80 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } binding.itemActions.downloadButton.setOnClickListener { - if (viewModel.item.isDownloaded()) { - viewModel.deleteEpisode() - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - } else if (viewModel.item.isDownloading()) { - createCancelDialog() - } else { - binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent) - binding.itemActions.progressDownload.isIndeterminate = true - binding.itemActions.progressDownload.isVisible = true - if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { - val storageDialog = getStorageSelectionDialog( - requireContext(), - onItemSelected = { storageIndex -> - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex, storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@getStorageSelectionDialog - } - createDownloadPreparingDialog() - viewModel.download(storageIndex = storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - storageDialog.show() - return@setOnClickListener - } - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@setOnClickListener - } - createDownloadPreparingDialog() - viewModel.download() - } + 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 { + download() + } + } + + private fun download(){ + binding.itemActions.downloadButton.setIconResource(AndroidR.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { + val storageDialog = getStorageSelectionDialog( + requireContext(), + onItemSelected = { storageIndex -> + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex, storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return@getStorageSelectionDialog + } + createDownloadPreparingDialog() + viewModel.download(storageIndex = storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + storageDialog.show() + return + } + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return + } + createDownloadPreparingDialog() + viewModel.download() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { dialog?.let { val sheet = it as BottomSheetDialog @@ -402,6 +412,31 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { dialog.show() } + private fun createPickQualityDialog() { + val qualityEntries = resources.getStringArray(CoreR.array.quality_entries) + val qualityValues = resources.getStringArray(CoreR.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() + download() + } + builder.setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + private fun navigateToPlayerActivity( playerItems: Array, ) { diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt index ed6b8894..b5aded7c 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt @@ -192,65 +192,7 @@ class MovieFragment : Fragment() { } binding.itemActions.downloadButton.setOnClickListener { - if (viewModel.item.isDownloaded()) { - viewModel.deleteItem() - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - } else if (viewModel.item.isDownloading()) { - createCancelDialog() - } else { - binding.itemActions.downloadButton.setIconResource(android.R.color.transparent) - binding.itemActions.progressDownload.isIndeterminate = true - binding.itemActions.progressDownload.isVisible = true - if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { - val storageDialog = getStorageSelectionDialog( - requireContext(), - onItemSelected = { storageIndex -> - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex, storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@getStorageSelectionDialog - } - createDownloadPreparingDialog() - viewModel.download(storageIndex = storageIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - storageDialog.show() - return@setOnClickListener - } - if (viewModel.item.sources.size > 1) { - val dialog = getVideoVersionDialog( - requireContext(), - viewModel.item, - onItemSelected = { sourceIndex -> - createDownloadPreparingDialog() - viewModel.download(sourceIndex) - }, - onCancel = { - binding.itemActions.progressDownload.isVisible = false - binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) - }, - ) - dialog.show() - return@setOnClickListener - } - createDownloadPreparingDialog() - viewModel.download() - } + handleDownload() } binding.peopleRecyclerView.adapter = PersonListAdapter { person -> @@ -258,6 +200,74 @@ class MovieFragment : Fragment() { } } + 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 { + download() + } + } + + private fun download() { + binding.itemActions.downloadButton.setIconResource(android.R.color.transparent) + binding.itemActions.progressDownload.isIndeterminate = true + binding.itemActions.progressDownload.isVisible = true + if (requireContext().getExternalFilesDirs(null).filterNotNull().size > 1) { + val storageDialog = getStorageSelectionDialog( + requireContext(), + onItemSelected = { storageIndex -> + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex, storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return@getStorageSelectionDialog + } + createDownloadPreparingDialog() + viewModel.download(storageIndex = storageIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + storageDialog.show() + return + } + if (viewModel.item.sources.size > 1) { + val dialog = getVideoVersionDialog( + requireContext(), + viewModel.item, + onItemSelected = { sourceIndex -> + createDownloadPreparingDialog() + viewModel.download(sourceIndex) + }, + onCancel = { + binding.itemActions.progressDownload.isVisible = false + binding.itemActions.downloadButton.setIconResource(CoreR.drawable.ic_download) + }, + ) + dialog.show() + return + } + createDownloadPreparingDialog() + viewModel.download() + } + override fun onResume() { super.onResume() @@ -495,6 +505,31 @@ class MovieFragment : Fragment() { dialog.show() } + private fun createPickQualityDialog() { + val qualityEntries = resources.getStringArray(CoreR.array.quality_entries) + val qualityValues = resources.getStringArray(CoreR.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 + download() + dialog.dismiss() + } + builder.setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + val dialog = builder.create() + dialog.show() + } + private fun navigateToPlayerActivity( playerItems: Array, ) { diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 9b0d2090..d04f9110 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -26,6 +26,8 @@ import dev.jdtech.jellyfin.models.toFindroidTrickplayInfoDto import dev.jdtech.jellyfin.models.toFindroidUserDataDto import dev.jdtech.jellyfin.models.toIntroDto import dev.jdtech.jellyfin.repository.JellyfinRepository +import org.jellyfin.sdk.model.api.EncodingContext +import org.jellyfin.sdk.model.api.MediaStreamType import java.io.File import java.util.UUID import kotlin.Exception @@ -82,15 +84,29 @@ class DownloaderImpl( if (intro != null) { database.insertIntro(intro.toIntroDto(item.id)) } - val request = DownloadManager.Request(source.path.toUri()) - .setTitle(item.name) - .setAllowedOverMetered(appPreferences.downloadOverMobileData) - .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setDestinationUri(path) - val downloadId = downloadManager.enqueue(request) - database.setSourceDownloadId(source.id, downloadId) - return Pair(downloadId, null) + if (appPreferences.downloadQuality != "Original") { + 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()) + .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 -> { @@ -111,15 +127,29 @@ class DownloaderImpl( if (intro != null) { database.insertIntro(intro.toIntroDto(item.id)) } - val request = DownloadManager.Request(source.path.toUri()) - .setTitle(item.name) - .setAllowedOverMetered(appPreferences.downloadOverMobileData) - .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setDestinationUri(path) - val downloadId = downloadManager.enqueue(request) - database.setSourceDownloadId(source.id, downloadId) - return Pair(downloadId, null) + if (appPreferences.downloadQuality != "Original") { + 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()) + .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) @@ -230,6 +260,45 @@ class DownloaderImpl( } } + private fun downloadEmbeddedMediaStreams( + item: FindroidItem, + source: FindroidSource, + storageIndex: Int = 0 + ) { + val storageLocation = context.getExternalFilesDirs(null)[storageIndex] + val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null } + for (mediaStream in subtitleStreams) { + var deliveryUrl = mediaStream.path!! + if (mediaStream.codec == "webvtt") { + deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt") + } + 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 request = DownloadManager.Request(Uri.parse(deliveryUrl)) + .setTitle(mediaStream.title) + .setAllowedOverMetered(appPreferences.downloadOverMobileData) + .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) + .setDestinationUri(streamPath) + + val downloadId = downloadManager.enqueue(request) + database.setMediaStreamDownloadId(id, downloadId) + } + } + + private suspend fun downloadTrickplayData( itemId: UUID, sourceId: String, @@ -263,4 +332,47 @@ 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 + } + + 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 deviceId = jellyfinRepository.getDeviceId() + val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, deviceId, mediaSourceId, playSessionId, maxBitrate, "ts") + + val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality) + transcodeUri + } catch (e: Exception) { + null + } + } + + // TODO: I believe building upon the uri is not necessary anymore all is handled in the sdk api + 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") + .build() + } } diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index 6e92f5c0..d198af5a 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -25,4 +25,16 @@ audiotrack opensles + + Original + 720p - 2Mbps + 480p - 1Mbps + 360p - 800Kbps + + + Original + 720p + 480p + 360p + \ No newline at end of file diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index 358972ee..9ebeb356 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -9,4 +9,16 @@ android:defaultValue="false" app:key="pref_downloads_roaming" app:title="@string/download_roaming" /> + + + \ No newline at end of file diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt index eb7e9dca..c88d2d9d 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt @@ -123,6 +123,19 @@ 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( diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt index cca99608..852dac19 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt @@ -42,6 +42,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" From 633ee6b8c42f731240285eff243aa3a9022cccee Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 05:01:09 +0300 Subject: [PATCH 22/32] lint: klint standard --- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 283 +++++--- .../repository/JellyfinRepositoryImpl.kt | 661 ++++++++++-------- .../JellyfinRepositoryOfflineImpl.kt | 130 ++-- 3 files changed, 636 insertions(+), 438 deletions(-) diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index d04f9110..0a463d73 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -30,7 +30,6 @@ import org.jellyfin.sdk.model.api.EncodingContext import org.jellyfin.sdk.model.api.MediaStreamType import java.io.File import java.util.UUID -import kotlin.Exception import kotlin.math.ceil import dev.jdtech.jellyfin.core.R as CoreR @@ -48,13 +47,15 @@ class DownloaderImpl( storageIndex: Int, ): Pair { try { - val source = jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } + val source = + jellyfinRepository.getMediaSources(item.id, true).first { it.id == sourceId } val intro = jellyfinRepository.getIntroTimestamps(item.id) - val trickplayInfo = if (item is FindroidSources) { - item.trickplayInfo?.get(sourceId) - } else { - null - } + val trickplayInfo = + if (item is FindroidSources) { + item.trickplayInfo?.get(sourceId) + } else { + null + } val storageLocation = context.getExternalFilesDirs(null)[storageIndex] if (storageLocation == null || Environment.getExternalStorageState(storageLocation) != Environment.MEDIA_MOUNTED) { return Pair(-1, UiText.StringResource(CoreR.string.storage_unavailable)) @@ -85,24 +86,29 @@ class DownloaderImpl( database.insertIntro(intro.toIntroDto(item.id)) } if (appPreferences.downloadQuality != "Original") { - 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) + 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()) - .setTitle(item.name) - .setAllowedOverMetered(appPreferences.downloadOverMobileData) - .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setDestinationUri(path) + } else { + val request = + DownloadManager + .Request(source.path.toUri()) + .setTitle(item.name) + .setAllowedOverMetered(appPreferences.downloadOverMobileData) + .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + .setDestinationUri(path) val downloadId = downloadManager.enqueue(request) database.setSourceDownloadId(source.id, downloadId) return Pair(downloadId, null) @@ -111,7 +117,8 @@ class DownloaderImpl( is FindroidEpisode -> { database.insertShow( - jellyfinRepository.getShow(item.seriesId) + jellyfinRepository + .getShow(item.seriesId) .toFindroidShowDto(appPreferences.currentServer!!), ) database.insertSeason( @@ -128,24 +135,29 @@ class DownloaderImpl( database.insertIntro(intro.toIntroDto(item.id)) } if (appPreferences.downloadQuality != "Original") { - 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) + 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()) - .setTitle(item.name) - .setAllowedOverMetered(appPreferences.downloadOverMobileData) - .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setDestinationUri(path) + } else { + val request = + DownloadManager + .Request(source.path.toUri()) + .setTitle(item.name) + .setAllowedOverMetered(appPreferences.downloadOverMobileData) + .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + .setDestinationUri(path) val downloadId = downloadManager.enqueue(request) database.setSourceDownloadId(source.id, downloadId) return Pair(downloadId, null) @@ -157,24 +169,41 @@ class DownloaderImpl( try { val source = jellyfinRepository.getMediaSources(item.id).first { it.id == sourceId } deleteItem(item, source) - } catch (_: Exception) {} + } catch (_: Exception) { + } - return Pair(-1, if (e.message != null) UiText.DynamicString(e.message!!) else UiText.StringResource(CoreR.string.unknown_error)) + return Pair( + -1, + if (e.message != null) { + UiText.DynamicString(e.message!!) + } else { + UiText.StringResource( + CoreR.string.unknown_error, + ) + }, + ) } } - override suspend fun cancelDownload(item: FindroidItem, source: FindroidSource) { + override suspend fun cancelDownload( + item: FindroidItem, + source: FindroidSource, + ) { if (source.downloadId != null) { downloadManager.remove(source.downloadId!!) } deleteItem(item, source) } - override suspend fun deleteItem(item: FindroidItem, source: FindroidSource) { + override suspend fun deleteItem( + item: FindroidItem, + source: FindroidSource, + ) { when (item) { is FindroidMovie -> { database.deleteMovie(item.id) } + is FindroidEpisode -> { database.deleteEpisode(item.id) val remainingEpisodes = database.getEpisodesBySeasonId(item.seasonId) @@ -212,23 +241,29 @@ class DownloaderImpl( if (downloadId == null) { return Pair(downloadStatus, progress) } - val query = DownloadManager.Query() - .setFilterById(downloadId) + val query = + DownloadManager + .Query() + .setFilterById(downloadId) val cursor = downloadManager.query(query) if (cursor.moveToFirst()) { - downloadStatus = cursor.getInt( - cursor.getColumnIndexOrThrow( - DownloadManager.COLUMN_STATUS, - ), - ) + downloadStatus = + cursor.getInt( + cursor.getColumnIndexOrThrow( + DownloadManager.COLUMN_STATUS, + ), + ) when (downloadStatus) { DownloadManager.STATUS_RUNNING -> { - val totalBytes = 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() } } + DownloadManager.STATUS_SUCCESSFUL -> { progress = 100 } @@ -247,14 +282,28 @@ class DownloaderImpl( val storageLocation = context.getExternalFilesDirs(null)[storageIndex] for (mediaStream in source.mediaStreams.filter { it.isExternal }) { val id = UUID.randomUUID() - val streamPath = Uri.fromFile(File(storageLocation, "downloads/${item.id}.${source.id}.$id.download")) - database.insertMediaStream(mediaStream.toFindroidMediaStreamDto(id, source.id, streamPath.path.orEmpty())) - val request = DownloadManager.Request(Uri.parse(mediaStream.path)) - .setTitle(mediaStream.title) - .setAllowedOverMetered(appPreferences.downloadOverMobileData) - .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) - .setDestinationUri(streamPath) + val 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) + .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) + .setDestinationUri(streamPath) val downloadId = downloadManager.enqueue(request) database.setMediaStreamDownloadId(id, downloadId) } @@ -263,57 +312,66 @@ class DownloaderImpl( private fun downloadEmbeddedMediaStreams( item: FindroidItem, source: FindroidSource, - storageIndex: Int = 0 + storageIndex: Int = 0, ) { val storageLocation = context.getExternalFilesDirs(null)[storageIndex] - val subtitleStreams = source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null } + val subtitleStreams = + source.mediaStreams.filter { !it.isExternal && it.type == MediaStreamType.SUBTITLE && it.path != null } for (mediaStream in subtitleStreams) { var deliveryUrl = mediaStream.path!! if (mediaStream.codec == "webvtt") { deliveryUrl = deliveryUrl.replace("Stream.srt", "Stream.vtt") } val id = UUID.randomUUID() - val streamPath = Uri.fromFile( - File( - storageLocation, - "downloads/${item.id}.${source.id}.$id.download" + val streamPath = + Uri.fromFile( + File( + storageLocation, + "downloads/${item.id}.${source.id}.$id.download", + ), ) - ) database.insertMediaStream( mediaStream.toFindroidMediaStreamDto( id, source.id, - streamPath.path.orEmpty() - ) + streamPath.path.orEmpty(), + ), ) - val request = DownloadManager.Request(Uri.parse(deliveryUrl)) - .setTitle(mediaStream.title) - .setAllowedOverMetered(appPreferences.downloadOverMobileData) - .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) - .setDestinationUri(streamPath) + val request = + DownloadManager + .Request(Uri.parse(deliveryUrl)) + .setTitle(mediaStream.title) + .setAllowedOverMetered(appPreferences.downloadOverMobileData) + .setAllowedOverRoaming(appPreferences.downloadWhenRoaming) + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN) + .setDestinationUri(streamPath) val downloadId = downloadManager.enqueue(request) database.setMediaStreamDownloadId(id, downloadId) } } - private suspend fun downloadTrickplayData( itemId: UUID, sourceId: String, trickplayInfo: FindroidTrickplayInfo, ) { - val maxIndex = ceil(trickplayInfo.thumbnailCount.toDouble().div(trickplayInfo.tileWidth * trickplayInfo.tileHeight)).toInt() + val maxIndex = + ceil( + trickplayInfo.thumbnailCount + .toDouble() + .div(trickplayInfo.tileWidth * trickplayInfo.tileHeight), + ).toInt() val byteArrays = mutableListOf() for (i in 0..maxIndex) { - jellyfinRepository.getTrickplayData( - itemId, - trickplayInfo.width, - i, - )?.let { byteArray -> - byteArrays.add(byteArray) - } + jellyfinRepository + .getTrickplayData( + itemId, + trickplayInfo.width, + i, + )?.let { byteArray -> + byteArrays.add(byteArray) + } } saveTrickplayData(itemId, sourceId, trickplayInfo, byteArrays) } @@ -333,22 +391,38 @@ class DownloaderImpl( } } - private suspend fun getTranscodedUrl(itemId: UUID, quality: String): Uri? { - val maxBitrate = when (quality) { - "720p" -> 2000000 // 2 Mbps - "480p" -> 1000000 // 1 Mbps - "360p" -> 800000 // 800Kbps - else -> 2000000 - } + 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 + } return try { - - val deviceProfile = jellyfinRepository.buildDeviceProfile(maxBitrate,"mkv", EncodingContext.STATIC) - val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(itemId,false,deviceProfile,maxBitrate) - val mediaSourceId = playbackInfo.content.mediaSources.firstOrNull()?.id!! + val deviceProfile = + jellyfinRepository.buildDeviceProfile(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 deviceId = jellyfinRepository.getDeviceId() - val downloadUrl = jellyfinRepository.getVideoStreambyContainerUrl(itemId, deviceId, mediaSourceId, playSessionId, maxBitrate, "ts") + val downloadUrl = + jellyfinRepository.getVideoStreambyContainerUrl( + itemId, + deviceId, + mediaSourceId, + playSessionId, + maxBitrate, + "ts", + ) val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality) transcodeUri @@ -361,15 +435,18 @@ class DownloaderImpl( private fun buildTranscodeUri( transcodingUrl: String, maxBitrate: Int, - quality: String + quality: String, ): Uri { - val resolution = when (quality) { - "720p" -> "720" - "480p" -> "480" - "360p" -> "360" - else -> "720" - } - return Uri.parse(transcodingUrl).buildUpon() + 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") diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index ae178c7b..c8d6d66d 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -68,55 +68,70 @@ class JellyfinRepositoryImpl( private val database: ServerDatabaseDao, private val appPreferences: AppPreferences, ) : JellyfinRepository { - override suspend fun getPublicSystemInfo(): PublicSystemInfo = withContext(Dispatchers.IO) { - jellyfinApi.systemApi.getPublicSystemInfo().content - } + override suspend fun getPublicSystemInfo(): PublicSystemInfo = + withContext(Dispatchers.IO) { + jellyfinApi.systemApi.getPublicSystemInfo().content + } - override suspend fun getUserViews(): List = withContext(Dispatchers.IO) { - jellyfinApi.viewsApi.getUserViews(jellyfinApi.userId!!).content.items.orEmpty() - } + override suspend fun getUserViews(): List = + withContext(Dispatchers.IO) { + jellyfinApi.viewsApi + .getUserViews(jellyfinApi.userId!!) + .content.items + .orEmpty() + } - override suspend fun getItem(itemId: UUID): BaseItemDto = withContext(Dispatchers.IO) { - jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content - } + override suspend fun getItem(itemId: UUID): BaseItemDto = + withContext(Dispatchers.IO) { + jellyfinApi.userLibraryApi.getItem(itemId, jellyfinApi.userId!!).content + } override suspend fun getEpisode(itemId: UUID): FindroidEpisode = withContext(Dispatchers.IO) { - jellyfinApi.userLibraryApi.getItem( - itemId, - jellyfinApi.userId!!, - ).content.toFindroidEpisode(this@JellyfinRepositoryImpl, database)!! + jellyfinApi.userLibraryApi + .getItem( + itemId, + jellyfinApi.userId!!, + ).content + .toFindroidEpisode(this@JellyfinRepositoryImpl, database)!! } override suspend fun getMovie(itemId: UUID): FindroidMovie = withContext(Dispatchers.IO) { - jellyfinApi.userLibraryApi.getItem( - itemId, - jellyfinApi.userId!!, - ).content.toFindroidMovie(this@JellyfinRepositoryImpl, database) + jellyfinApi.userLibraryApi + .getItem( + itemId, + jellyfinApi.userId!!, + ).content + .toFindroidMovie(this@JellyfinRepositoryImpl, database) } override suspend fun getShow(itemId: UUID): FindroidShow = withContext(Dispatchers.IO) { - jellyfinApi.userLibraryApi.getItem( - itemId, - jellyfinApi.userId!!, - ).content.toFindroidShow(this@JellyfinRepositoryImpl) + jellyfinApi.userLibraryApi + .getItem( + itemId, + jellyfinApi.userId!!, + ).content + .toFindroidShow(this@JellyfinRepositoryImpl) } override suspend fun getSeason(itemId: UUID): FindroidSeason = withContext(Dispatchers.IO) { - jellyfinApi.userLibraryApi.getItem( - itemId, - jellyfinApi.userId!!, - ).content.toFindroidSeason(this@JellyfinRepositoryImpl) + jellyfinApi.userLibraryApi + .getItem( + itemId, + jellyfinApi.userId!!, + ).content + .toFindroidSeason(this@JellyfinRepositoryImpl) } override suspend fun getLibraries(): List = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getItems( - jellyfinApi.userId!!, - ).content.items + jellyfinApi.itemsApi + .getItems( + jellyfinApi.userId!!, + ).content.items .orEmpty() .mapNotNull { it.toFindroidCollection(this@JellyfinRepositoryImpl) } } @@ -131,16 +146,17 @@ class JellyfinRepositoryImpl( limit: Int?, ): List = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getItems( - jellyfinApi.userId!!, - parentId = parentId, - includeItemTypes = includeTypes, - recursive = recursive, - sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)), - sortOrder = listOf(sortOrder), - startIndex = startIndex, - limit = limit, - ).content.items + jellyfinApi.itemsApi + .getItems( + jellyfinApi.userId!!, + parentId = parentId, + includeItemTypes = includeTypes, + recursive = recursive, + sortBy = listOf(ItemSortBy.fromName(sortBy.sortString)), + sortOrder = listOf(sortOrder), + startIndex = startIndex, + limit = limit, + ).content.items .orEmpty() .mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) } } @@ -151,13 +167,14 @@ class JellyfinRepositoryImpl( recursive: Boolean, sortBy: SortBy, sortOrder: SortOrder, - ): Flow> { - return Pager( - config = PagingConfig( - pageSize = 10, - maxSize = 100, - enablePlaceholders = false, - ), + ): Flow> = + Pager( + config = + PagingConfig( + pageSize = 10, + maxSize = 100, + enablePlaceholders = false, + ), pagingSourceFactory = { ItemsPagingSource( this, @@ -169,87 +186,102 @@ class JellyfinRepositoryImpl( ) }, ).flow - } override suspend fun getPersonItems( personIds: List, includeTypes: List?, recursive: Boolean, - ): List = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getItems( - jellyfinApi.userId!!, - personIds = personIds, - includeItemTypes = includeTypes, - recursive = recursive, - ).content.items - .orEmpty() - .mapNotNull { - it.toFindroidItem(this@JellyfinRepositoryImpl, database) - } - } + ): List = + withContext(Dispatchers.IO) { + jellyfinApi.itemsApi + .getItems( + jellyfinApi.userId!!, + personIds = personIds, + includeItemTypes = includeTypes, + recursive = recursive, + ).content.items + .orEmpty() + .mapNotNull { + it.toFindroidItem(this@JellyfinRepositoryImpl, database) + } + } override suspend fun getFavoriteItems(): List = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getItems( - jellyfinApi.userId!!, - filters = listOf(ItemFilter.IS_FAVORITE), - includeItemTypes = listOf( - BaseItemKind.MOVIE, - BaseItemKind.SERIES, - BaseItemKind.EPISODE, - ), - recursive = true, - ).content.items + jellyfinApi.itemsApi + .getItems( + jellyfinApi.userId!!, + filters = listOf(ItemFilter.IS_FAVORITE), + includeItemTypes = + listOf( + BaseItemKind.MOVIE, + BaseItemKind.SERIES, + BaseItemKind.EPISODE, + ), + recursive = true, + ).content.items .orEmpty() .mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) } } override suspend fun getSearchItems(searchQuery: String): List = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getItems( - jellyfinApi.userId!!, - searchTerm = searchQuery, - includeItemTypes = listOf( - BaseItemKind.MOVIE, - BaseItemKind.SERIES, - BaseItemKind.EPISODE, - ), - recursive = true, - ).content.items + jellyfinApi.itemsApi + .getItems( + jellyfinApi.userId!!, + searchTerm = searchQuery, + includeItemTypes = + listOf( + BaseItemKind.MOVIE, + BaseItemKind.SERIES, + BaseItemKind.EPISODE, + ), + recursive = true, + ).content.items .orEmpty() .mapNotNull { it.toFindroidItem(this@JellyfinRepositoryImpl, database) } } override suspend fun getResumeItems(): List { - val items = withContext(Dispatchers.IO) { - jellyfinApi.itemsApi.getResumeItems( - jellyfinApi.userId!!, - limit = 12, - includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE), - ).content.items.orEmpty() - } + val items = + withContext(Dispatchers.IO) { + jellyfinApi.itemsApi + .getResumeItems( + jellyfinApi.userId!!, + limit = 12, + includeItemTypes = listOf(BaseItemKind.MOVIE, BaseItemKind.EPISODE), + ).content.items + .orEmpty() + } return items.mapNotNull { it.toFindroidItem(this, database) } } override suspend fun getLatestMedia(parentId: UUID): List { - val items = withContext(Dispatchers.IO) { - jellyfinApi.userLibraryApi.getLatestMedia( - jellyfinApi.userId!!, - parentId = parentId, - limit = 16, - ).content - } + val items = + withContext(Dispatchers.IO) { + jellyfinApi.userLibraryApi + .getLatestMedia( + jellyfinApi.userId!!, + parentId = parentId, + limit = 16, + ).content + } return items.mapNotNull { it.toFindroidItem(this, database) } } - override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List = + override suspend fun getSeasons( + seriesId: UUID, + offline: Boolean, + ): List = withContext(Dispatchers.IO) { if (!offline) { - jellyfinApi.showsApi.getSeasons(seriesId, jellyfinApi.userId!!).content.items + jellyfinApi.showsApi + .getSeasons(seriesId, jellyfinApi.userId!!) + .content.items .orEmpty() .map { it.toFindroidSeason(this@JellyfinRepositoryImpl) } } else { @@ -259,12 +291,13 @@ class JellyfinRepositoryImpl( override suspend fun getNextUp(seriesId: UUID?): List = withContext(Dispatchers.IO) { - jellyfinApi.showsApi.getNextUp( - jellyfinApi.userId!!, - limit = 24, - seriesId = seriesId, - enableResumable = false, - ).content.items + jellyfinApi.showsApi + .getNextUp( + jellyfinApi.userId!!, + limit = 24, + seriesId = seriesId, + enableResumable = false, + ).content.items .orEmpty() .mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl) } } @@ -279,14 +312,15 @@ class JellyfinRepositoryImpl( ): List = withContext(Dispatchers.IO) { if (!offline) { - jellyfinApi.showsApi.getEpisodes( - seriesId, - jellyfinApi.userId!!, - seasonId = seasonId, - fields = fields, - startItemId = startItemId, - limit = limit, - ).content.items + jellyfinApi.showsApi + .getEpisodes( + seriesId, + jellyfinApi.userId!!, + seasonId = seasonId, + fields = fields, + startItemId = startItemId, + limit = limit, + ).content.items .orEmpty() .mapNotNull { it.toFindroidEpisode(this@JellyfinRepositoryImpl, database) } } else { @@ -294,39 +328,47 @@ class JellyfinRepositoryImpl( } } - override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List = + override suspend fun getMediaSources( + itemId: UUID, + includePath: Boolean, + ): List = withContext(Dispatchers.IO) { val sources = mutableListOf() sources.addAll( - jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( - itemId, - PlaybackInfoDto( - userId = jellyfinApi.userId!!, - deviceProfile = DeviceProfile( - name = "Direct play all", - maxStaticBitrate = 1_000_000_000, - maxStreamingBitrate = 1_000_000_000, - codecProfiles = emptyList(), - containerProfiles = emptyList(), - directPlayProfiles = listOf( - DirectPlayProfile(type = DlnaProfileType.VIDEO), - DirectPlayProfile(type = DlnaProfileType.AUDIO), - ), - transcodingProfiles = emptyList(), - subtitleProfiles = listOf( - SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL), - SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL), - ), - ), - maxStreamingBitrate = 1_000_000_000, - ), - ).content.mediaSources.map { - it.toFindroidSource( - this@JellyfinRepositoryImpl, + jellyfinApi.mediaInfoApi + .getPostedPlaybackInfo( itemId, - includePath, - ) - }, + PlaybackInfoDto( + userId = jellyfinApi.userId!!, + deviceProfile = + DeviceProfile( + name = "Direct play all", + maxStaticBitrate = 1_000_000_000, + maxStreamingBitrate = 1_000_000_000, + codecProfiles = emptyList(), + containerProfiles = emptyList(), + directPlayProfiles = + listOf( + DirectPlayProfile(type = DlnaProfileType.VIDEO), + DirectPlayProfile(type = DlnaProfileType.AUDIO), + ), + transcodingProfiles = emptyList(), + subtitleProfiles = + listOf( + SubtitleProfile("srt", SubtitleDeliveryMethod.EXTERNAL), + SubtitleProfile("ass", SubtitleDeliveryMethod.EXTERNAL), + ), + ), + maxStreamingBitrate = 1_000_000_000, + ), + ).content.mediaSources + .map { + it.toFindroidSource( + this@JellyfinRepositoryImpl, + itemId, + includePath, + ) + }, ) sources.addAll( database.getSources(itemId).map { it.toFindroidSource(database) }, @@ -334,14 +376,18 @@ class JellyfinRepositoryImpl( sources } - override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String = + override suspend fun getStreamUrl( + itemId: UUID, + mediaSourceId: String, + playSessionId: String?, + ): String = withContext(Dispatchers.IO) { try { jellyfinApi.videosApi.getVideoStreamUrl( itemId, static = true, mediaSourceId = mediaSourceId, - playSessionId = playSessionId + playSessionId = playSessionId, ) } catch (e: Exception) { Timber.e(e) @@ -362,16 +408,21 @@ class JellyfinRepositoryImpl( pathParameters["itemId"] = itemId try { - return@withContext jellyfinApi.api.get( - "/Episode/{itemId}/IntroTimestamps/v1", - pathParameters, - ).content + return@withContext jellyfinApi.api + .get( + "/Episode/{itemId}/IntroTimestamps/v1", + pathParameters, + ).content } catch (e: Exception) { return@withContext null } } - override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? = + override suspend fun getTrickplayData( + itemId: UUID, + width: Int, + index: Int, + ): ByteArray? = withContext(Dispatchers.IO) { try { try { @@ -379,9 +430,13 @@ class JellyfinRepositoryImpl( if (sources != null) { return@withContext File(sources.first(), index.toString()).readBytes() } - } catch (_: Exception) { } + } catch (_: Exception) { + } - return@withContext jellyfinApi.trickplayApi.getTrickplayTileImage(itemId, width, index).content.toByteArray() + return@withContext jellyfinApi.trickplayApi + .getTrickplayTileImage(itemId, width, index) + .content + .toByteArray() } catch (e: Exception) { return@withContext null } @@ -392,21 +447,22 @@ class JellyfinRepositoryImpl( withContext(Dispatchers.IO) { jellyfinApi.sessionApi.postCapabilities( playableMediaTypes = listOf(MediaType.VIDEO), - supportedCommands = listOf( - GeneralCommandType.VOLUME_UP, - GeneralCommandType.VOLUME_DOWN, - GeneralCommandType.TOGGLE_MUTE, - GeneralCommandType.SET_AUDIO_STREAM_INDEX, - GeneralCommandType.SET_SUBTITLE_STREAM_INDEX, - GeneralCommandType.MUTE, - GeneralCommandType.UNMUTE, - GeneralCommandType.SET_VOLUME, - GeneralCommandType.DISPLAY_MESSAGE, - GeneralCommandType.PLAY, - GeneralCommandType.PLAY_STATE, - GeneralCommandType.PLAY_NEXT, - GeneralCommandType.PLAY_MEDIA_SOURCE, - ), + supportedCommands = + listOf( + GeneralCommandType.VOLUME_UP, + GeneralCommandType.VOLUME_DOWN, + GeneralCommandType.TOGGLE_MUTE, + GeneralCommandType.SET_AUDIO_STREAM_INDEX, + GeneralCommandType.SET_SUBTITLE_STREAM_INDEX, + GeneralCommandType.MUTE, + GeneralCommandType.UNMUTE, + GeneralCommandType.SET_VOLUME, + GeneralCommandType.DISPLAY_MESSAGE, + GeneralCommandType.PLAY, + GeneralCommandType.PLAY_STATE, + GeneralCommandType.PLAY_NEXT, + GeneralCommandType.PLAY_MEDIA_SOURCE, + ), supportsMediaControl = true, ) } @@ -528,186 +584,215 @@ class JellyfinRepositoryImpl( } } - override suspend fun getUserConfiguration(): UserConfiguration = withContext(Dispatchers.IO) { - jellyfinApi.userApi.getCurrentUser().content.configuration!! - } + override suspend fun getUserConfiguration(): UserConfiguration = + withContext(Dispatchers.IO) { + jellyfinApi.userApi + .getCurrentUser() + .content.configuration!! + } override suspend fun getDownloads(): List = withContext(Dispatchers.IO) { val items = mutableListOf() items.addAll( - database.getMoviesByServerId(appPreferences.currentServer!!) + database + .getMoviesByServerId(appPreferences.currentServer!!) .map { it.toFindroidMovie(database, jellyfinApi.userId!!) }, ) items.addAll( - database.getShowsByServerId(appPreferences.currentServer!!) + database + .getShowsByServerId(appPreferences.currentServer!!) .map { it.toFindroidShow(database, jellyfinApi.userId!!) }, ) items } - override fun getUserId(): UUID { - return jellyfinApi.userId!! - } + override fun getUserId(): UUID = jellyfinApi.userId!! - - override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair { - return when (transcodeResolution) { + override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair = + when (transcodeResolution) { 1080 -> 8000000 to 384000 // Adjusted for personal can be other values 720 -> 2000000 to 384000 // 720p 480 -> 1000000 to 384000 // 480p - 360 -> 800000 to 128000 // 360p + 360 -> 800000 to 128000 // 360p else -> 12000000 to 384000 // its adaptive but setting max here } - } - override suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile { - val deviceProfile = ClientCapabilitiesDto( - supportedCommands = emptyList(), - playableMediaTypes = emptyList(), - supportsMediaControl = true, - supportsPersistentIdentifier = true, - deviceProfile = DeviceProfile( - name = "AnanasUser", - id = getUserId().toString(), - maxStaticBitrate = maxBitrate, - maxStreamingBitrate = maxBitrate, - codecProfiles = emptyList(), - containerProfiles = listOf(), - directPlayProfiles = listOf( - DirectPlayProfile(type = DlnaProfileType.VIDEO), - DirectPlayProfile(type = DlnaProfileType.AUDIO), - ), - transcodingProfiles = listOf( - TranscodingProfile( - container = container, - context = context, - protocol = MediaStreamProtocol.HLS, - audioCodec = "aac,ac3,eac3", - videoCodec = "hevc,h264", - type = DlnaProfileType.VIDEO, - conditions = listOf( - ProfileCondition( - condition = ProfileConditionType.LESS_THAN_EQUAL, - property = ProfileConditionValue.VIDEO_BITRATE, - value = "8000000", - isRequired = true, - ) - ), - copyTimestamps = true, - enableSubtitlesInManifest = true, - transcodeSeekInfo = TranscodeSeekInfo.AUTO, + override suspend fun buildDeviceProfile( + maxBitrate: Int, + container: String, + context: EncodingContext, + ): DeviceProfile { + val deviceProfile = + ClientCapabilitiesDto( + supportedCommands = emptyList(), + playableMediaTypes = 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), + ), ), - ), - 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 { - val playbackInfo = jellyfinApi.mediaInfoApi.getPostedPlaybackInfo( - itemId = itemId, - PlaybackInfoDto( - userId = jellyfinApi.userId!!, - enableTranscoding = true, - enableDirectPlay = false, - enableDirectStream = enableDirectStream, - autoOpenLiveStream = true, - deviceProfile = deviceProfile, - allowAudioStreamCopy = true, - allowVideoStreamCopy = true, - maxStreamingBitrate = maxBitrate, + override suspend fun getPostedPlaybackInfo( + itemId: UUID, + enableDirectStream: Boolean, + deviceProfile: DeviceProfile, + maxBitrate: Int, + ): Response { + 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, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String { - val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl( - itemId, - static = false, - deviceId = deviceId, - mediaSourceId = mediaSourceId, - playSessionId = playSessionId, - videoBitRate = videoBitrate, - audioBitRate = 384000, - videoCodec = "hevc", - audioCodec = "aac,ac3,eac3", - container = container, - startTimeTicks = 0, - copyTimestamps = true, - subtitleMethod = SubtitleDeliveryMethod.EXTERNAL - ) - return url - } - - override suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String { - val isAuto = videoBitrate == 12000000 - val url = if (!isAuto) { - jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( + override suspend fun getVideoStreambyContainerUrl( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: Int, + container: String, + ): String { + val url = + jellyfinApi.videosApi.getVideoStreamByContainerUrl( itemId, static = false, deviceId = deviceId, mediaSourceId = mediaSourceId, playSessionId = playSessionId, videoBitRate = videoBitrate, - enableAdaptiveBitrateStreaming = false, - audioBitRate = 384000, //could also be passed with audioBitrate but i preferred not as its not much data anyways - videoCodec = "hevc,h264", - audioCodec = "aac,ac3,eac3", - startTimeTicks = 0, - copyTimestamps = true, - subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, - context = EncodingContext.STREAMING, - segmentContainer = "ts", - transcodeReasons = "ContainerBitrateExceedsLimit", - ) - } else { - jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( - itemId, - static = false, - deviceId = deviceId, - mediaSourceId = mediaSourceId, - playSessionId = playSessionId, - enableAdaptiveBitrateStreaming = true, + audioBitRate = 384000, videoCodec = "hevc", audioCodec = "aac,ac3,eac3", + container = container, startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, - context = EncodingContext.STREAMING, - segmentContainer = "ts", - transcodeReasons = "ContainerBitrateExceedsLimit", ) - } return url } + override suspend fun getTranscodedVideoStream( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: Int, + ): String { + val isAuto = videoBitrate == 12000000 + val url = + if (!isAuto) { + jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( + itemId, + static = false, + deviceId = deviceId, + mediaSourceId = mediaSourceId, + playSessionId = playSessionId, + videoBitRate = videoBitrate, + enableAdaptiveBitrateStreaming = false, + audioBitRate = 384000, // could also be passed with audioBitrate but i preferred not as its not much data anyways + videoCodec = "hevc,h264", + audioCodec = "aac,ac3,eac3", + startTimeTicks = 0, + copyTimestamps = true, + subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, + context = EncodingContext.STREAMING, + segmentContainer = "ts", + transcodeReasons = "ContainerBitrateExceedsLimit", + ) + } else { + jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( + itemId, + static = false, + deviceId = deviceId, + mediaSourceId = mediaSourceId, + playSessionId = playSessionId, + enableAdaptiveBitrateStreaming = true, + videoCodec = "hevc", + audioCodec = "aac,ac3,eac3", + startTimeTicks = 0, + copyTimestamps = true, + subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, + context = EncodingContext.STREAMING, + segmentContainer = "ts", + transcodeReasons = "ContainerBitrateExceedsLimit", + ) + } + return url + } override suspend fun getDeviceId(): String { val devices = jellyfinApi.devicesApi.getDevices(getUserId()) - return devices.content.items?.firstOrNull()?.id!! + return devices.content.items + ?.firstOrNull() + ?.id!! } override suspend fun stopEncodingProcess(playSessionId: String) { val deviceId = getDeviceId() jellyfinApi.api.hlsSegmentApi.stopEncodingProcess( deviceId = deviceId, - playSessionId = playSessionId + playSessionId = playSessionId, ) } - } - - diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 6901c09d..9658541f 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -42,14 +42,9 @@ class JellyfinRepositoryOfflineImpl( private val database: ServerDatabaseDao, private val appPreferences: AppPreferences, ) : JellyfinRepository { + override suspend fun getPublicSystemInfo(): PublicSystemInfo = throw Exception("System info not available in offline mode") - override suspend fun getPublicSystemInfo(): PublicSystemInfo { - throw Exception("System info not available in offline mode") - } - - override suspend fun getUserViews(): List { - return emptyList() - } + override suspend fun getUserViews(): List = emptyList() override suspend fun getItem(itemId: UUID): BaseItemDto { TODO("Not yet implemented") @@ -113,38 +108,69 @@ class JellyfinRepositoryOfflineImpl( TODO("Not yet implemented") } - override suspend fun getSearchItems(searchQuery: String): List { - return withContext(Dispatchers.IO) { - val movies = database.searchMovies(appPreferences.currentServer!!, searchQuery).map { it.toFindroidMovie(database, jellyfinApi.userId!!) } - val shows = database.searchShows(appPreferences.currentServer!!, searchQuery).map { it.toFindroidShow(database, jellyfinApi.userId!!) } - val episodes = database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) } + override suspend fun getSearchItems(searchQuery: String): List = + withContext(Dispatchers.IO) { + val movies = + database.searchMovies(appPreferences.currentServer!!, searchQuery).map { + it.toFindroidMovie( + database, + @Suppress("ktlint:standard:max-line-length") + jellyfinApi.userId!!, + ) + } + val shows = + database + .searchShows( + appPreferences.currentServer!!, + searchQuery, + ).map { it.toFindroidShow(database, jellyfinApi.userId!!) } + val episodes = + database.searchEpisodes(appPreferences.currentServer!!, searchQuery).map { + it.toFindroidEpisode(database, jellyfinApi.userId!!) + } movies + shows + episodes } - } - override suspend fun getResumeItems(): List { - return withContext(Dispatchers.IO) { - val movies = database.getMoviesByServerId(appPreferences.currentServer!!).map { it.toFindroidMovie(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 } - val episodes = database.getEpisodesByServerId(appPreferences.currentServer!!).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) }.filter { it.playbackPositionTicks > 0 } + override suspend fun getResumeItems(): List = + withContext(Dispatchers.IO) { + val movies = + database + .getMoviesByServerId( + appPreferences.currentServer!!, + ).map { it.toFindroidMovie(database, jellyfinApi.userId!!) } + .filter { + it.playbackPositionTicks > + 0 + } + val episodes = + database + .getEpisodesByServerId( + appPreferences.currentServer!!, + ).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) } + .filter { + it.playbackPositionTicks > + 0 + } movies + episodes } - } - override suspend fun getLatestMedia(parentId: UUID): List { - return emptyList() - } + override suspend fun getLatestMedia(parentId: UUID): List = emptyList() - override suspend fun getSeasons(seriesId: UUID, offline: Boolean): List = + override suspend fun getSeasons( + seriesId: UUID, + offline: Boolean, + ): List = withContext(Dispatchers.IO) { database.getSeasonsByShowId(seriesId).map { it.toFindroidSeason(database, jellyfinApi.userId!!) } } - override suspend fun getNextUp(seriesId: UUID?): List { - return withContext(Dispatchers.IO) { + override suspend fun getNextUp(seriesId: UUID?): List = + withContext(Dispatchers.IO) { val result = mutableListOf() - val shows = database.getShowsByServerId(appPreferences.currentServer!!).filter { - if (seriesId != null) it.id == seriesId else true - } + val shows = + database.getShowsByServerId(appPreferences.currentServer!!).filter { + if (seriesId != null) it.id == seriesId else true + } for (show in shows) { val episodes = database.getEpisodesByShowId(show.id).map { it.toFindroidEpisode(database, jellyfinApi.userId!!) } val indexOfLastPlayed = episodes.indexOfLast { it.played } @@ -156,7 +182,6 @@ class JellyfinRepositoryOfflineImpl( } result.filter { it.playbackPositionTicks == 0L } } - } override suspend fun getEpisodes( seriesId: UUID, @@ -172,12 +197,19 @@ class JellyfinRepositoryOfflineImpl( items } - override suspend fun getMediaSources(itemId: UUID, includePath: Boolean): List = + override suspend fun getMediaSources( + itemId: UUID, + includePath: Boolean, + ): List = withContext(Dispatchers.IO) { database.getSources(itemId).map { it.toFindroidSource(database) } } - override suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String?): String { + override suspend fun getStreamUrl( + itemId: UUID, + mediaSourceId: String, + playSessionId: String?, + ): String { TODO("Not yet implemented") } @@ -186,7 +218,11 @@ class JellyfinRepositoryOfflineImpl( database.getIntro(itemId)?.toIntro() } - override suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? = + override suspend fun getTrickplayData( + itemId: UUID, + width: Int, + index: Int, + ): ByteArray? = withContext(Dispatchers.IO) { try { val sources = File(context.filesDir, "trickplay/$itemId").listFiles() ?: return@withContext null @@ -200,7 +236,11 @@ class JellyfinRepositoryOfflineImpl( override suspend fun postPlaybackStart(itemId: UUID) {} - override suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int) { + override suspend fun postPlaybackStop( + itemId: UUID, + positionTicks: Long, + playedPercentage: Int, + ) { withContext(Dispatchers.IO) { when { playedPercentage < 10 -> { @@ -260,35 +300,31 @@ class JellyfinRepositoryOfflineImpl( } } - override fun getBaseUrl(): String { - return "" - } + override fun getBaseUrl(): String = "" override suspend fun updateDeviceName(name: String) { TODO("Not yet implemented") } - override suspend fun getUserConfiguration(): UserConfiguration? { - return null - } + override suspend fun getUserConfiguration(): UserConfiguration? = null override suspend fun getDownloads(): List = withContext(Dispatchers.IO) { val items = mutableListOf() items.addAll( - database.getMoviesByServerId(appPreferences.currentServer!!) + database + .getMoviesByServerId(appPreferences.currentServer!!) .map { it.toFindroidMovie(database, jellyfinApi.userId!!) }, ) items.addAll( - database.getShowsByServerId(appPreferences.currentServer!!) + database + .getShowsByServerId(appPreferences.currentServer!!) .map { it.toFindroidShow(database, jellyfinApi.userId!!) }, ) items } - override fun getUserId(): UUID { - return jellyfinApi.userId!! - } + override fun getUserId(): UUID = jellyfinApi.userId!! override suspend fun getDeviceId(): String { TODO("Not yet implemented") @@ -301,7 +337,7 @@ class JellyfinRepositoryOfflineImpl( override suspend fun buildDeviceProfile( maxBitrate: Int, container: String, - context: EncodingContext + context: EncodingContext, ): DeviceProfile { TODO("Not yet implemented") } @@ -312,7 +348,7 @@ class JellyfinRepositoryOfflineImpl( mediaSourceId: String, playSessionId: String, videoBitrate: Int, - container: String + container: String, ): String { TODO("Not yet implemented") } @@ -322,7 +358,7 @@ class JellyfinRepositoryOfflineImpl( deviceId: String, mediaSourceId: String, playSessionId: String, - videoBitrate: Int + videoBitrate: Int, ): String { TODO("Not yet implemented") } @@ -331,7 +367,7 @@ class JellyfinRepositoryOfflineImpl( itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile, - maxBitrate: Int + maxBitrate: Int, ): Response { TODO("Not yet implemented") } From 4baa7bc0463e0f46e879ca33e2ef4a916ed52c9d Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 05:10:32 +0300 Subject: [PATCH 23/32] lint: klint standard --- .../jellyfin/repository/JellyfinRepository.kt | 68 ++++++++++++++++--- .../repository/JellyfinRepositoryImpl.kt | 1 + 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt index 2b4380c0..7c1d6725 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepository.kt @@ -29,7 +29,9 @@ interface JellyfinRepository { suspend fun getUserViews(): List suspend fun getItem(itemId: UUID): BaseItemDto + suspend fun getEpisode(itemId: UUID): FindroidEpisode + suspend fun getMovie(itemId: UUID): FindroidMovie suspend fun getShow(itemId: UUID): FindroidShow @@ -70,7 +72,10 @@ interface JellyfinRepository { suspend fun getLatestMedia(parentId: UUID): List - suspend fun getSeasons(seriesId: UUID, offline: Boolean = false): List + suspend fun getSeasons( + seriesId: UUID, + offline: Boolean = false, + ): List suspend fun getNextUp(seriesId: UUID? = null): List @@ -83,21 +88,40 @@ interface JellyfinRepository { offline: Boolean = false, ): List - suspend fun getMediaSources(itemId: UUID, includePath: Boolean = false): List + suspend fun getMediaSources( + itemId: UUID, + includePath: Boolean = false, + ): List - suspend fun getStreamUrl(itemId: UUID, mediaSourceId: String, playSessionId: String? = null): String + suspend fun getStreamUrl( + itemId: UUID, + mediaSourceId: String, + playSessionId: String? = null, + ): String suspend fun getIntroTimestamps(itemId: UUID): Intro? - suspend fun getTrickplayData(itemId: UUID, width: Int, index: Int): ByteArray? + suspend fun getTrickplayData( + itemId: UUID, + width: Int, + index: Int, + ): ByteArray? suspend fun postCapabilities() suspend fun postPlaybackStart(itemId: UUID) - suspend fun postPlaybackStop(itemId: UUID, positionTicks: Long, playedPercentage: Int) + suspend fun postPlaybackStop( + itemId: UUID, + positionTicks: Long, + playedPercentage: Int, + ) - suspend fun postPlaybackProgress(itemId: UUID, positionTicks: Long, isPaused: Boolean) + suspend fun postPlaybackProgress( + itemId: UUID, + positionTicks: Long, + isPaused: Boolean, + ) suspend fun markAsFavorite(itemId: UUID) @@ -121,13 +145,37 @@ interface JellyfinRepository { suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair - suspend fun buildDeviceProfile(maxBitrate: Int, container: String, context: EncodingContext): DeviceProfile + suspend fun buildDeviceProfile( + maxBitrate: Int, + container: String, + context: EncodingContext, + ): DeviceProfile - suspend fun getVideoStreambyContainerUrl(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int, container: String): String + suspend fun getVideoStreambyContainerUrl( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: + @Suppress("ktlint:standard:max-line-length") + Int, + container: String, + ): String - suspend fun getTranscodedVideoStream(itemId: UUID, deviceId: String, mediaSourceId: String, playSessionId: String, videoBitrate: Int): String + suspend fun getTranscodedVideoStream( + itemId: UUID, + deviceId: String, + mediaSourceId: String, + playSessionId: String, + videoBitrate: Int, + ): String - suspend fun getPostedPlaybackInfo(itemId: UUID, enableDirectStream: Boolean, deviceProfile: DeviceProfile ,maxBitrate: Int): Response + suspend fun getPostedPlaybackInfo( + itemId: UUID, + enableDirectStream: Boolean, + deviceProfile: DeviceProfile, + maxBitrate: Int, + ): Response suspend fun stopEncodingProcess(playSessionId: String) } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index c8d6d66d..5e4e1a6e 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -710,6 +710,7 @@ class JellyfinRepositoryImpl( deviceId: String, mediaSourceId: String, playSessionId: String, + @Suppress("ktlint:standard:max-line-length") videoBitrate: Int, container: String, ): String { From ba580f8769e18e27ee89081312c844babc8a3367 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Fri, 19 Jul 2024 05:27:17 +0300 Subject: [PATCH 24/32] lint: fix --- .../dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 5e4e1a6e..c8d6d66d 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -710,7 +710,6 @@ class JellyfinRepositoryImpl( deviceId: String, mediaSourceId: String, playSessionId: String, - @Suppress("ktlint:standard:max-line-length") videoBitrate: Int, container: String, ): String { From 6dded2e72641868731d4716e6b35e906496b9799 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 08:36:23 +0300 Subject: [PATCH 25/32] bugfixes: deviceId / code: New Enum VideoQuality --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 49 ++++++++++++++----- .../fragments/EpisodeBottomSheetFragment.kt | 4 +- .../jellyfin/fragments/MovieFragment.kt | 4 +- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 42 +++------------- core/src/main/res/values/string_arrays.xml | 22 ++++++++- .../res/xml/fragment_settings_downloads.xml | 4 +- .../jdtech/jellyfin/models/VideoQuality.kt | 25 ++++++++++ .../jellyfin/repository/JellyfinRepository.kt | 7 +-- .../repository/JellyfinRepositoryImpl.kt | 41 ++++++---------- .../JellyfinRepositoryOfflineImpl.kt | 5 +- .../viewmodels/PlayerActivityViewModel.kt | 27 +++------- 11 files changed, 121 insertions(+), 109 deletions(-) create mode 100644 data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 891170e8..1b4eb03f 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -45,6 +45,7 @@ import dev.jdtech.jellyfin.viewmodels.PlayerEvents import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +import dev.jdtech.jellyfin.core.R as CoreR var isControlsLocked: Boolean = false @@ -348,20 +349,44 @@ class PlayerActivity : BasePlayerActivity() { } private fun showQualitySelectionDialog() { - val height = viewModel.getOriginalHeight() // TODO: rewrite getting height stuff I don't like that its only update after changing quality - val qualities = when (height) { - 0 -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") - in 1001..1999 -> arrayOf("Auto", "Original (1080p) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") - in 2000..3000 -> arrayOf("Auto", "Original (4K) - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") - else -> arrayOf("Auto", "Original - Max", "720p - 2Mbps", "480p - 1Mbps", "360p - 800kbps") - } + val height = viewModel.getOriginalHeight() + val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() + val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() + + // Map entries to values + val qualityMap = qualityEntries.zip(qualityValues).toMap() + + val qualities: List = + when (height) { + 0 -> qualityEntries + in 1001..1999 -> + listOf( + qualityEntries[0], + "${qualityEntries[1]} (1080p)", + qualityEntries[2], + qualityEntries[3], + qualityEntries[4], + qualityEntries[5], + ) + in 2000..3000 -> + listOf( + qualityEntries[0], + "${qualityEntries[1]} (4K)", + qualityEntries[2], + qualityEntries[3], + qualityEntries[4], + qualityEntries[5], + ) + else -> qualityEntries + } MaterialAlertDialogBuilder(this) .setTitle("Select Video Quality") - .setItems(qualities) { _, which -> - val selectedQuality = qualities[which] - viewModel.changeVideoQuality(selectedQuality) - } - .show() + .setItems(qualities.toTypedArray()) { _, which -> + val selectedQualityEntry = qualities[which] + val selectedQualityValue = + qualityMap.entries.find { it.key.contains(selectedQualityEntry.split(" ")[0]) }?.value ?: selectedQualityEntry + viewModel.changeVideoQuality(selectedQualityValue) + }.show() } override fun onPictureInPictureModeChanged( diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 925f70f3..83d13407 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -413,8 +413,8 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { } 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 diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt index b5aded7c..a70d4456 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt @@ -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 diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 0a463d73..34ae76f0 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -16,6 +16,7 @@ import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.FindroidSources import dev.jdtech.jellyfin.models.FindroidTrickplayInfo import dev.jdtech.jellyfin.models.UiText +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.models.toFindroidEpisodeDto import dev.jdtech.jellyfin.models.toFindroidMediaStreamDto import dev.jdtech.jellyfin.models.toFindroidMovieDto @@ -395,19 +396,12 @@ class DownloaderImpl( itemId: UUID, quality: String, ): Uri? { - val maxBitrate = - when (quality) { - "720p" -> 2000000 // 2 Mbps - "480p" -> 1000000 // 1 Mbps - "360p" -> 800000 // 800Kbps - else -> 2000000 - } - + val videoQuality = VideoQuality.fromString(quality)!! return try { val deviceProfile = - jellyfinRepository.buildDeviceProfile(maxBitrate, "mkv", EncodingContext.STATIC) + jellyfinRepository.buildDeviceProfile(VideoQuality.getBitrate(videoQuality), "mkv", EncodingContext.STATIC) val playbackInfo = - jellyfinRepository.getPostedPlaybackInfo(itemId, false, deviceProfile, maxBitrate) + jellyfinRepository.getPostedPlaybackInfo(itemId, false, deviceProfile, VideoQuality.getBitrate(videoQuality)) val mediaSourceId = playbackInfo.content.mediaSources .firstOrNull() @@ -420,36 +414,14 @@ class DownloaderImpl( deviceId, mediaSourceId, playSessionId, - maxBitrate, + VideoQuality.getBitrate(videoQuality), "ts", + VideoQuality.getQualityInt(videoQuality) ) - val transcodeUri = buildTranscodeUri(downloadUrl, maxBitrate, quality) - transcodeUri + downloadUrl.toUri() } catch (e: Exception) { null } } - - // TODO: I believe building upon the uri is not necessary anymore all is handled in the sdk api - 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") - .build() - } } diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index d198af5a..b5dd2095 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -26,13 +26,31 @@ opensles + Auto Original - 720p - 2Mbps - 480p - 1Mbps + 1080p - 8Mbps + 720p - 3Mbps + 480p - 1.5Mbps 360p - 800Kbps + Auto Original + 1080p + 720p + 480p + 360p + + + Original + 1080p - 8Mbps + 720p - 3Mbps + 480p - 1.5Mbps + 360p - 800Kbps + + + Original + 1080p 720p 480p 360p diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index 9ebeb356..c88d3b81 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -14,8 +14,8 @@ android:key="pref_downloads_quality" android:title="Download Quality" android:defaultValue="Original" - android:entries="@array/quality_entries" - android:entryValues="@array/quality_values" + android:entries="@array/download_quality_entries" + android:entryValues="@array/download_quality_values" android:summary="%s" /> - suspend fun buildDeviceProfile( maxBitrate: Int, container: String, @@ -156,10 +154,9 @@ interface JellyfinRepository { deviceId: String, mediaSourceId: String, playSessionId: String, - videoBitrate: - @Suppress("ktlint:standard:max-line-length") - Int, + videoBitrate: Int, container: String, + maxHeight: Int, ): String suspend fun getTranscodedVideoStream( diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index c8d6d66d..40d86bc6 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -16,6 +16,7 @@ import dev.jdtech.jellyfin.models.FindroidShow import dev.jdtech.jellyfin.models.FindroidSource import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.SortBy +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.models.toFindroidCollection import dev.jdtech.jellyfin.models.toFindroidEpisode import dev.jdtech.jellyfin.models.toFindroidItem @@ -609,15 +610,6 @@ class JellyfinRepositoryImpl( override fun getUserId(): UUID = jellyfinApi.userId!! - override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair = - when (transcodeResolution) { - 1080 -> 8000000 to 384000 // Adjusted for personal can be other values - 720 -> 2000000 to 384000 // 720p - 480 -> 1000000 to 384000 // 480p - 360 -> 800000 to 128000 // 360p - else -> 12000000 to 384000 // its adaptive but setting max here - } - override suspend fun buildDeviceProfile( maxBitrate: Int, container: String, @@ -631,7 +623,7 @@ class JellyfinRepositoryImpl( supportsPersistentIdentifier = true, deviceProfile = DeviceProfile( - name = "AnanasUser", + name = "FindroidUser", id = getUserId().toString(), maxStaticBitrate = maxBitrate, maxStreamingBitrate = maxBitrate, @@ -648,8 +640,8 @@ class JellyfinRepositoryImpl( container = container, context = context, protocol = MediaStreamProtocol.HLS, - audioCodec = "aac,ac3,eac3", - videoCodec = "hevc,h264", + audioCodec = "aac", + videoCodec = "h264", type = DlnaProfileType.VIDEO, conditions = listOf( @@ -712,6 +704,7 @@ class JellyfinRepositoryImpl( playSessionId: String, videoBitrate: Int, container: String, + maxHeight: Int, ): String { val url = jellyfinApi.videosApi.getVideoStreamByContainerUrl( @@ -721,10 +714,11 @@ class JellyfinRepositoryImpl( mediaSourceId = mediaSourceId, playSessionId = playSessionId, videoBitRate = videoBitrate, - audioBitRate = 384000, - videoCodec = "hevc", - audioCodec = "aac,ac3,eac3", + audioBitRate = 128000, + videoCodec = "h264", + audioCodec = "aac", container = container, + maxHeight = maxHeight, startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -739,7 +733,7 @@ class JellyfinRepositoryImpl( playSessionId: String, videoBitrate: Int, ): String { - val isAuto = videoBitrate == 12000000 + val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto) val url = if (!isAuto) { jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( @@ -750,9 +744,9 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, videoBitRate = videoBitrate, enableAdaptiveBitrateStreaming = false, - audioBitRate = 384000, // could also be passed with audioBitrate but i preferred not as its not much data anyways - videoCodec = "hevc,h264", - audioCodec = "aac,ac3,eac3", + audioBitRate = 128000, + videoCodec = "h264", + audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -768,8 +762,8 @@ class JellyfinRepositoryImpl( mediaSourceId = mediaSourceId, playSessionId = playSessionId, enableAdaptiveBitrateStreaming = true, - videoCodec = "hevc", - audioCodec = "aac,ac3,eac3", + videoCodec = "h264", + audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, subtitleMethod = SubtitleDeliveryMethod.EXTERNAL, @@ -782,10 +776,7 @@ class JellyfinRepositoryImpl( } override suspend fun getDeviceId(): String { - val devices = jellyfinApi.devicesApi.getDevices(getUserId()) - return devices.content.items - ?.firstOrNull() - ?.id!! + return jellyfinApi.api.deviceInfo.id } override suspend fun stopEncodingProcess(playSessionId: String) { diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index 9658541f..dcc4a39e 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -330,10 +330,6 @@ class JellyfinRepositoryOfflineImpl( TODO("Not yet implemented") } - override suspend fun getVideoTranscodeBitRate(transcodeResolution: Int): Pair { - TODO("Not yet implemented") - } - override suspend fun buildDeviceProfile( maxBitrate: Int, container: String, @@ -349,6 +345,7 @@ class JellyfinRepositoryOfflineImpl( playSessionId: String, videoBitrate: Int, container: String, + maxHeight: Int, ): String { TODO("Not yet implemented") } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index e804df6e..3ca1771f 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -28,6 +28,7 @@ import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.Trickplay +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.player.video.R import dev.jdtech.jellyfin.repository.JellyfinRepository @@ -464,17 +465,6 @@ constructor( eventsChannel.trySend(PlayerEvents.IsPlayingChanged(isPlaying)) } - private fun getTranscodeResolutions(preferredQuality: String): Int { - return when (preferredQuality) { - "1080p" -> 1080 // TODO: 1080p this logic is based on 1080p being original - "720p - 2Mbps" -> 720 - "480p - 1Mbps" -> 480 - "360p - 800kbps" -> 360 - "Auto" -> 1 - else -> 1080 //default to Original - } - } - fun changeVideoQuality(quality: String) { val mediaId = player.currentMediaItem?.mediaId ?: return val currentItem = items.firstOrNull { it.itemId.toString() == mediaId } ?: return @@ -482,12 +472,9 @@ constructor( viewModelScope.launch { try { - val transcodingResolution = getTranscodeResolutions(quality) - val (videoBitRate, audioBitRate) = jellyfinRepository.getVideoTranscodeBitRate( - transcodingResolution - ) - val deviceProfile = jellyfinRepository.buildDeviceProfile(videoBitRate, "mkv", EncodingContext.STREAMING) - val playbackInfo = jellyfinRepository.getPostedPlaybackInfo(currentItem.itemId,true,deviceProfile,videoBitRate) + 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) @@ -537,18 +524,18 @@ constructor( val allSubtitles = - if (transcodingResolution == 1080) { + if (VideoQuality.getQualityString(videoQuality) == "Original") { externalSubtitles }else { embeddedSubtitles.apply { addAll(externalSubtitles) } } - val url = if (transcodingResolution == 1080){ + val url = if (VideoQuality.getQualityString(videoQuality) == "Original"){ jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) } else { val mediaSourceId = mediaSources[currentMediaItemIndex].id val deviceId = jellyfinRepository.getDeviceId() - val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, videoBitRate) + val url = jellyfinRepository.getTranscodedVideoStream(currentItem.itemId, deviceId ,mediaSourceId, playSessionId!!, VideoQuality.getBitrate(videoQuality)) val uriBuilder = url.toUri().buildUpon() val apiKey = jellyfinApi.api.accessToken // TODO: add in repo uriBuilder.appendQueryParameter("api_key",apiKey ) From 0ace01f5f820391aa61df56073662154f78c7177 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 08:40:23 +0300 Subject: [PATCH 26/32] klint --- core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 34ae76f0..5b608a91 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -416,7 +416,7 @@ class DownloaderImpl( playSessionId, VideoQuality.getBitrate(videoQuality), "ts", - VideoQuality.getQualityInt(videoQuality) + VideoQuality.getQualityInt(videoQuality), ) downloadUrl.toUri() From 7adcc50d750d82e08104987e97665ab9c65c85f9 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 22:28:38 +0300 Subject: [PATCH 27/32] rework: Enum --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 49 +++++++------------ .../jdtech/jellyfin/utils/DownloaderImpl.kt | 2 +- .../jdtech/jellyfin/models/VideoQuality.kt | 34 +++++++------ .../repository/JellyfinRepositoryImpl.kt | 2 +- .../viewmodels/PlayerActivityViewModel.kt | 4 +- 5 files changed, 41 insertions(+), 50 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 1b4eb03f..122bd8bf 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -1,5 +1,6 @@ package dev.jdtech.jellyfin +import android.annotation.SuppressLint import android.app.AppOpsManager import android.app.PictureInPictureParams import android.content.Context @@ -38,6 +39,7 @@ import dagger.hilt.android.AndroidEntryPoint import dev.jdtech.jellyfin.databinding.ActivityPlayerBinding import dev.jdtech.jellyfin.dialogs.SpeedSelectionDialogFragment import dev.jdtech.jellyfin.dialogs.TrackSelectionDialogFragment +import dev.jdtech.jellyfin.models.VideoQuality import dev.jdtech.jellyfin.utils.PlayerGestureHelper import dev.jdtech.jellyfin.utils.PreviewScrubListener import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel @@ -288,6 +290,7 @@ class PlayerActivity : BasePlayerActivity() { viewModel.initializePlayer(args.items) } + @SuppressLint("MissingSuperCall") override fun onUserLeaveHint() { super.onUserLeaveHint() if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && @@ -348,45 +351,29 @@ class PlayerActivity : BasePlayerActivity() { } catch (_: IllegalArgumentException) { } } + private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val height = viewModel.getOriginalHeight() + val originalHeight = viewModel.getOriginalHeight() // TODO: Rework getting originalHeight 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 - originalHeight) } - val qualities: List = - 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 + .setSingleChoiceItems(qualities.toTypedArray(), selectedIndex) { dialog, which -> + selectedIndex = which + val selectedQualityValue = qualityValues[which] viewModel.changeVideoQuality(selectedQualityValue) - }.show() + dialog.dismiss() + } + .show() } override fun onPictureInPictureModeChanged( diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 5b608a91..93453796 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -416,7 +416,7 @@ class DownloaderImpl( playSessionId, VideoQuality.getBitrate(videoQuality), "ts", - VideoQuality.getQualityInt(videoQuality), + VideoQuality.getHeight(videoQuality), ) downloadUrl.toUri() diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt index 46b0f07b..5df83ece 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt @@ -2,24 +2,28 @@ package dev.jdtech.jellyfin.models enum class VideoQuality( val bitrate: Int, - val qualityString: String, - val qualityInt: Int, + val height: Int, + val width: Int, + val original: Boolean, ) { - PAuto(10000000, "Auto", 1080), - POriginal(1000000000, "Original", 1080), - P1080(8000000, "1080p", 1080), - P720(3000000, "720p", 720), - P480(1500000, "480p", 480), - P360(800000, "360p", 360), - ; + Auto(10000000, 1080, 1920, false), + Original(1000000000, 1080, 1920, true), + 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" + 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 getOriginal(quality: VideoQuality): Boolean = quality.original } } \ No newline at end of file diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index 40d86bc6..e16b9fc5 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -733,7 +733,7 @@ class JellyfinRepositoryImpl( playSessionId: String, videoBitrate: Int, ): String { - val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.PAuto) + val isAuto = videoBitrate == VideoQuality.getBitrate(VideoQuality.Auto) val url = if (!isAuto) { jellyfinApi.api.dynamicHlsApi.getMasterHlsVideoPlaylistUrl( diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 3ca1771f..897e84c2 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -524,13 +524,13 @@ constructor( val allSubtitles = - if (VideoQuality.getQualityString(videoQuality) == "Original") { + if (VideoQuality.getOriginal(videoQuality)) { externalSubtitles }else { embeddedSubtitles.apply { addAll(externalSubtitles) } } - val url = if (VideoQuality.getQualityString(videoQuality) == "Original"){ + val url = if (VideoQuality.getOriginal(videoQuality)){ jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) } else { val mediaSourceId = mediaSources[currentMediaItemIndex].id From c79342523be289a34d0bfd1a143e3c3abf3ecd88 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 23:12:12 +0300 Subject: [PATCH 28/32] refactor: strings & naming standard for icon --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 2 +- .../src/main/res/layout/exo_main_controls.xml | 2 +- .../jdtech/jellyfin/utils/DownloaderImpl.kt | 2 +- .../{ic_quality.xml => ic_monitor_play.xml} | 0 core/src/main/res/values/string_arrays.xml | 22 +++++++++---------- core/src/main/res/values/strings.xml | 9 ++++++++ .../res/xml/fragment_settings_downloads.xml | 6 ++--- 7 files changed, 26 insertions(+), 17 deletions(-) rename core/src/main/res/drawable/{ic_quality.xml => ic_monitor_play.xml} (100%) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 122bd8bf..a7c51e02 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -366,7 +366,7 @@ class PlayerActivity : BasePlayerActivity() { qualities[1] = "${qualities[1]} (${closestQuality})" } MaterialAlertDialogBuilder(this) - .setTitle("Select Video Quality") + .setTitle(CoreR.string.select_quality) .setSingleChoiceItems(qualities.toTypedArray(), selectedIndex) { dialog, which -> selectedIndex = which val selectedQualityValue = qualityValues[which] diff --git a/app/phone/src/main/res/layout/exo_main_controls.xml b/app/phone/src/main/res/layout/exo_main_controls.xml index b136be35..940f0f97 100644 --- a/app/phone/src/main/res/layout/exo_main_controls.xml +++ b/app/phone/src/main/res/layout/exo_main_controls.xml @@ -81,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" /> diff --git a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt index 93453796..1586f020 100644 --- a/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt +++ b/core/src/main/java/dev/jdtech/jellyfin/utils/DownloaderImpl.kt @@ -86,7 +86,7 @@ class DownloaderImpl( if (intro != null) { database.insertIntro(intro.toIntroDto(item.id)) } - if (appPreferences.downloadQuality != "Original") { + if (appPreferences.downloadQuality != VideoQuality.Original.toString()) { downloadEmbeddedMediaStreams(item, source, storageIndex) val transcodingUrl = getTranscodedUrl(item.id, appPreferences.downloadQuality!!) diff --git a/core/src/main/res/drawable/ic_quality.xml b/core/src/main/res/drawable/ic_monitor_play.xml similarity index 100% rename from core/src/main/res/drawable/ic_quality.xml rename to core/src/main/res/drawable/ic_monitor_play.xml diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index b5dd2095..472228cf 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -26,12 +26,12 @@ opensles - Auto - Original - 1080p - 8Mbps - 720p - 3Mbps - 480p - 1.5Mbps - 360p - 800Kbps + @string/quality_auto + @string/quality_original + @string/quality_1080p + @string/quality_720p + @string/quality_480p + @string/quality_360p Auto @@ -42,11 +42,11 @@ 360p - Original - 1080p - 8Mbps - 720p - 3Mbps - 480p - 1.5Mbps - 360p - 800Kbps + @string/quality_original + @string/quality_1080p + @string/quality_720p + @string/quality_480p + @string/quality_360p Original diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index b00c4f89..7716be38 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -192,4 +192,13 @@ Unmark as played Add to favorites Remove from favorites + Default to selected download quality + Download Quality + Select Video Quality + Auto + Original + 1080p - 8Mbps + 720p - 3Mbps + 480p - 1.5Mbps + 360p - 0.8Mbps diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index c88d3b81..f0d0aa53 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -12,13 +12,13 @@ + app:summary="@string/quality_default" /> \ No newline at end of file From 21ae815223643a8dae0fa28cdf2e4ca00486b07a Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sat, 20 Jul 2024 23:55:34 +0300 Subject: [PATCH 29/32] rework: getting original resolution for quality selection dialog --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 4 +-- .../jdtech/jellyfin/models/VideoQuality.kt | 2 ++ .../viewmodels/PlayerActivityViewModel.kt | 28 +++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index a7c51e02..00f53e82 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -353,14 +353,14 @@ class PlayerActivity : BasePlayerActivity() { private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val originalHeight = viewModel.getOriginalHeight() // TODO: Rework getting originalHeight + val originalResolution = viewModel.getoriginalResolution() // TODO: Rework getting originalResolution val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() val qualities = qualityEntries.toMutableList() val closestQuality = VideoQuality.entries .filter { it != VideoQuality.Auto && it != VideoQuality.Original } - .minByOrNull { kotlin.math.abs(it.height - originalHeight) } + .minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution!!) } if (closestQuality != null) { qualities[1] = "${qualities[1]} (${closestQuality})" diff --git a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt index 5df83ece..bcd87448 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/models/VideoQuality.kt @@ -8,6 +8,7 @@ enum class VideoQuality( ) { 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), @@ -16,6 +17,7 @@ enum class VideoQuality( override fun toString(): String = when (this) { Auto -> "Auto" Original -> "Original" + P3840 -> "4K" else -> "${height}p" } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index 897e84c2..ed808c49 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -18,6 +18,7 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionParameters +import androidx.media3.common.VideoSize import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector @@ -61,7 +62,7 @@ constructor( private val savedStateHandle: SavedStateHandle, ) : ViewModel(), Player.Listener { val player: Player - private var originalHeight: Int = 0 + private var originalResolution: Int? = null private val _uiState = MutableStateFlow( UiState( @@ -179,6 +180,22 @@ constructor( .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) @@ -564,13 +581,8 @@ constructor( 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 } catch (e: Exception) { @@ -579,8 +591,8 @@ constructor( } } - fun getOriginalHeight(): Int { - return originalHeight + fun getoriginalResolution(): Int? { + return originalResolution } } From 8482df97334301057081a735491b00bd8d84e1c5 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sun, 21 Jul 2024 00:42:35 +0300 Subject: [PATCH 30/32] feat: choice of codec in network settings / bugfix: nullsafe fix --- .../src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt | 4 ++-- core/src/main/res/values/string_arrays.xml | 4 ++++ core/src/main/res/values/strings.xml | 1 + core/src/main/res/xml/fragment_settings_network.xml | 7 +++++++ .../jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt | 8 ++++---- .../src/main/java/dev/jdtech/jellyfin/AppPreferences.kt | 5 +++++ .../src/main/java/dev/jdtech/jellyfin/Constants.kt | 2 ++ 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index 00f53e82..b7b4fb7c 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -353,14 +353,14 @@ class PlayerActivity : BasePlayerActivity() { private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val originalResolution = viewModel.getoriginalResolution() // TODO: Rework getting originalResolution + val originalResolution = viewModel.getoriginalResolution() ?: 0// TODO: Rework getting originalResolution val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() val qualities = qualityEntries.toMutableList() val closestQuality = VideoQuality.entries .filter { it != VideoQuality.Auto && it != VideoQuality.Original } - .minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution!!) } + .minByOrNull { kotlin.math.abs(it.height*it.width - originalResolution) } if (closestQuality != null) { qualities[1] = "${qualities[1]} (${closestQuality})" diff --git a/core/src/main/res/values/string_arrays.xml b/core/src/main/res/values/string_arrays.xml index 472228cf..36b6658f 100644 --- a/core/src/main/res/values/string_arrays.xml +++ b/core/src/main/res/values/string_arrays.xml @@ -55,4 +55,8 @@ 480p 360p + + h264 + hevc + \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 7716be38..5bc5be72 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -139,6 +139,7 @@ Request timeout (ms) Connect timeout (ms) Socket timeout (ms) + Transcoding Codec Users Add user Hardware decoding diff --git a/core/src/main/res/xml/fragment_settings_network.xml b/core/src/main/res/xml/fragment_settings_network.xml index 5e5bd8a2..fd966364 100644 --- a/core/src/main/res/xml/fragment_settings_network.xml +++ b/core/src/main/res/xml/fragment_settings_network.xml @@ -15,4 +15,11 @@ app:key="pref_network_socket_timeout" app:title="@string/settings_socket_timeout" app:useSimpleSummaryProvider="true" /> + \ No newline at end of file diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index e16b9fc5..a0847349 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -641,7 +641,7 @@ class JellyfinRepositoryImpl( context = context, protocol = MediaStreamProtocol.HLS, audioCodec = "aac", - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec!!, type = DlnaProfileType.VIDEO, conditions = listOf( @@ -715,7 +715,7 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, videoBitRate = videoBitrate, audioBitRate = 128000, - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec, audioCodec = "aac", container = container, maxHeight = maxHeight, @@ -745,7 +745,7 @@ class JellyfinRepositoryImpl( videoBitRate = videoBitrate, enableAdaptiveBitrateStreaming = false, audioBitRate = 128000, - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec, audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, @@ -762,7 +762,7 @@ class JellyfinRepositoryImpl( mediaSourceId = mediaSourceId, playSessionId = playSessionId, enableAdaptiveBitrateStreaming = true, - videoCodec = "h264", + videoCodec = appPreferences.transcodeCodec, audioCodec = "aac", startTimeTicks = 0, copyTimestamps = true, diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt index c88d2d9d..957f9feb 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/AppPreferences.kt @@ -103,6 +103,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, diff --git a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt index 852dac19..4f555422 100644 --- a/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt +++ b/preferences/src/main/java/dev/jdtech/jellyfin/Constants.kt @@ -40,6 +40,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" @@ -60,6 +61,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] From d70253140dee07b6c3e786f13953c0499b71c3f6 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sun, 21 Jul 2024 00:43:12 +0300 Subject: [PATCH 31/32] refactor: string --- core/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 5bc5be72..2f65247c 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -139,7 +139,7 @@ Request timeout (ms) Connect timeout (ms) Socket timeout (ms) - Transcoding Codec + Transcoding codec Users Add user Hardware decoding From 5609f7368d34ed6a7067c29d35dfdcc3eca03f00 Mon Sep 17 00:00:00 2001 From: nomadics9 Date: Sun, 21 Jul 2024 01:49:30 +0300 Subject: [PATCH 32/32] code: cleanup --- .../dev/jdtech/jellyfin/PlayerActivity.kt | 2 +- .../fragments/EpisodeBottomSheetFragment.kt | 6 ++-- .../jellyfin/fragments/MovieFragment.kt | 6 ++-- .../res/xml/fragment_settings_downloads.xml | 1 - .../jdtech/jellyfin/models/VideoQuality.kt | 4 +-- .../jellyfin/repository/JellyfinRepository.kt | 2 ++ .../repository/JellyfinRepositoryImpl.kt | 4 +++ .../JellyfinRepositoryOfflineImpl.kt | 4 +++ .../java/dev/jdtech/jellyfin/SubtitleUtils.kt | 20 +++++++++++ .../viewmodels/PlayerActivityViewModel.kt | 33 ++++--------------- .../jellyfin/viewmodels/PlayerViewModel.kt | 20 +++-------- 11 files changed, 51 insertions(+), 51 deletions(-) create mode 100644 player/video/src/main/java/dev/jdtech/jellyfin/SubtitleUtils.kt diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt index b7b4fb7c..d9f20770 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/PlayerActivity.kt @@ -353,7 +353,7 @@ class PlayerActivity : BasePlayerActivity() { private var selectedIndex = 1 // Default to "Original" (index 1) private fun showQualitySelectionDialog() { - val originalResolution = viewModel.getoriginalResolution() ?: 0// TODO: Rework getting originalResolution + val originalResolution = viewModel.getOriginalResolution() ?: 0 val qualityEntries = resources.getStringArray(CoreR.array.quality_entries).toList() val qualityValues = resources.getStringArray(CoreR.array.quality_values).toList() diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt index 83d13407..a2245399 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/EpisodeBottomSheetFragment.kt @@ -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 @@ -428,7 +428,7 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() { builder.setPositiveButton("Download") { dialog, _ -> appPreferences.downloadQuality = selectedQuality dialog.dismiss() - download() + startDownload() } builder.setNegativeButton("Cancel") { dialog, _ -> dialog.dismiss() diff --git a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt index a70d4456..abfa1f94 100644 --- a/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt +++ b/app/phone/src/main/java/dev/jdtech/jellyfin/fragments/MovieFragment.kt @@ -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 @@ -520,7 +520,7 @@ class MovieFragment : Fragment() { } builder.setPositiveButton("Download") { dialog, _ -> appPreferences.downloadQuality = selectedQuality - download() + startDownload() dialog.dismiss() } builder.setNegativeButton("Cancel") { dialog, _ -> diff --git a/core/src/main/res/xml/fragment_settings_downloads.xml b/core/src/main/res/xml/fragment_settings_downloads.xml index f0d0aa53..295a08a9 100644 --- a/core/src/main/res/xml/fragment_settings_downloads.xml +++ b/core/src/main/res/xml/fragment_settings_downloads.xml @@ -9,7 +9,6 @@ android:defaultValue="false" app:key="pref_downloads_roaming" app:title="@string/download_roaming" /> - suspend fun stopEncodingProcess(playSessionId: String) + + suspend fun getAccessToken(): String? } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt index a0847349..43c65951 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryImpl.kt @@ -786,4 +786,8 @@ class JellyfinRepositoryImpl( playSessionId = playSessionId, ) } + + override suspend fun getAccessToken(): String? { + return jellyfinApi.api.accessToken + } } diff --git a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt index dcc4a39e..2bc0a7db 100644 --- a/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt +++ b/data/src/main/java/dev/jdtech/jellyfin/repository/JellyfinRepositoryOfflineImpl.kt @@ -372,4 +372,8 @@ class JellyfinRepositoryOfflineImpl( override suspend fun stopEncodingProcess(playSessionId: String) { TODO("Not yet implemented") } + + override suspend fun getAccessToken(): String? { + TODO("Not yet implemented") + } } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/SubtitleUtils.kt b/player/video/src/main/java/dev/jdtech/jellyfin/SubtitleUtils.kt new file mode 100644 index 00000000..1403ac2c --- /dev/null +++ b/player/video/src/main/java/dev/jdtech/jellyfin/SubtitleUtils.kt @@ -0,0 +1,20 @@ +package dev.jdtech.jellyfin + +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 + } +} diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt index ed808c49..ec9ab034 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerActivityViewModel.kt @@ -14,22 +14,20 @@ import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.TrackSelectionOverride import androidx.media3.common.TrackSelectionParameters -import androidx.media3.common.VideoSize import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import dagger.hilt.android.lifecycle.HiltViewModel import dev.jdtech.jellyfin.AppPreferences -import dev.jdtech.jellyfin.api.JellyfinApi import dev.jdtech.jellyfin.models.Intro import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.Trickplay import dev.jdtech.jellyfin.models.VideoQuality +import dev.jdtech.jellyfin.setSubtitlesMimeTypes import dev.jdtech.jellyfin.mpv.MPVPlayer import dev.jdtech.jellyfin.player.video.R import dev.jdtech.jellyfin.repository.JellyfinRepository @@ -57,7 +55,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 { @@ -510,29 +507,13 @@ constructor( 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 - "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 - } - ) + .setMimeType(setSubtitlesMimeTypes(mediaStream.codec)) .setLanguage(mediaStream.language.ifBlank { "Unknown" }) .setLabel("Embedded") .build() @@ -541,20 +522,20 @@ constructor( val allSubtitles = - if (VideoQuality.getOriginal(videoQuality)) { + if (VideoQuality.getIsOriginalQuality(videoQuality)) { externalSubtitles }else { embeddedSubtitles.apply { addAll(externalSubtitles) } } - val url = if (VideoQuality.getOriginal(videoQuality)){ + val url = if (VideoQuality.getIsOriginalQuality(videoQuality)){ jellyfinRepository.getStreamUrl(currentItem.itemId, currentItem.mediaSourceId, playSessionId) } else { val mediaSourceId = mediaSources[currentMediaItemIndex].id 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 // TODO: add in repo + val apiKey = jellyfinRepository.getAccessToken() uriBuilder.appendQueryParameter("api_key",apiKey ) val newUri = uriBuilder.build() newUri.toString() @@ -591,7 +572,7 @@ constructor( } } - fun getoriginalResolution(): Int? { + fun getOriginalResolution(): Int? { return originalResolution } } diff --git a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt index 50a0fc1c..317367d0 100644 --- a/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt +++ b/player/video/src/main/java/dev/jdtech/jellyfin/viewmodels/PlayerViewModel.kt @@ -18,6 +18,7 @@ import dev.jdtech.jellyfin.models.PlayerChapter import dev.jdtech.jellyfin.models.PlayerItem import dev.jdtech.jellyfin.models.TrickplayInfo import dev.jdtech.jellyfin.repository.JellyfinRepository +import dev.jdtech.jellyfin.setSubtitlesMimeTypes import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -126,6 +127,7 @@ class PlayerViewModel @Inject internal constructor( .map { episode -> episode.toPlayerItem(mediaSourceIndex, playbackPosition) } } + private suspend fun FindroidItem.toPlayerItem( mediaSourceIndex: Int?, playbackPosition: Long, @@ -136,7 +138,7 @@ class PlayerViewModel @Inject internal constructor( } else { mediaSources[mediaSourceIndex] } - // Embedded Sub externally for offline prep next commit + // Embedded Sub externally for offline playback val externalSubtitles = if (mediaSource.type.toString() == "LOCAL" ) { mediaSource.mediaStreams .filter { mediaStream -> @@ -147,13 +149,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 { @@ -166,13 +162,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) ) } }