From 718335d7334522fa208406ea911f7701e22fe376 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Fri, 26 Dec 2025 20:23:15 +0100 Subject: [PATCH 01/30] fix(player): Fix scaleX being NaN on minimize to background app switch This aims to fix the following Exception which might occour when watching a live stream and switching the app while 'minimize to backgorund' is enabled: java.lang.IllegalArgumentException: Cannot set 'scaleX' to Float.NaN at android.view.View.sanitizeFloatPropertyValue(View.java:17479) at android.view.View.sanitizeFloatPropertyValue(View.java:17453) at android.view.View.setScaleX(View.java:16822) at org.schabi.newpipe.views.ExpandableSurfaceView.onLayout(ExpandableSurfaceView.java:71) scaleX is set in onMeasure() in which width could be 0 in theory and this leading to a division by zero of a float which results in an assignment of Float.NaN. --- .../java/org/schabi/newpipe/views/ExpandableSurfaceView.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java index 175c81e46..36380b650 100644 --- a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java +++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java @@ -35,12 +35,12 @@ public class ExpandableSurfaceView extends SurfaceView { && resizeMode != RESIZE_MODE_FIT && verticalVideo ? maxHeight : baseHeight; - if (height == 0) { + if (width == 0 || height == 0) { return; } final float viewAspectRatio = width / ((float) height); - final float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; + final float aspectDeformation = (videoAspectRatio / viewAspectRatio) - 1; scaleX = 1.0f; scaleY = 1.0f; From 7101aecc98aa80291c231e2c7a9425aef60cece4 Mon Sep 17 00:00:00 2001 From: TobiGr Date: Fri, 26 Dec 2025 23:49:30 +0100 Subject: [PATCH 02/30] Try to prevent invalid aspectRatio of SurfaceView If the video's hieght is 0, the aspectRatio is set to Float.NaN which can cause further issues. Do not assign invalid values for the aspectRatio. --- .../java/org/schabi/newpipe/views/ExpandableSurfaceView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java index 36380b650..7452fff09 100644 --- a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java +++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java @@ -100,7 +100,7 @@ public class ExpandableSurfaceView extends SurfaceView { } public void setAspectRatio(final float aspectRatio) { - if (videoAspectRatio == aspectRatio) { + if (videoAspectRatio == aspectRatio || aspectRatio == 0 || !Float.isFinite(aspectRatio)) { return; } From 465979e677beb5bdba7c2c0bd5d1d3f9f24b3cec Mon Sep 17 00:00:00 2001 From: tobigr Date: Mon, 29 Dec 2025 16:36:43 +0100 Subject: [PATCH 03/30] Do not change the aspectRation if the renderer is disabled --- .../java/org/schabi/newpipe/player/ui/VideoPlayerUi.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java index 7157d6af2..b68d3d94d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -1554,6 +1554,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa @Override public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { super.onVideoSizeChanged(videoSize); + // Starting with ExoPlayer 2.19.0, the VideoSize will report a width and height of 0 + // if the renderer is disabled. In that case, we skip updating the aspect ratio. + if (videoSize.width == 0 || videoSize.height == 0) { + return; + } binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height); } //endregion From c9339a5a0323a263a4228d1944c961a629a4937a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 28 Dec 2025 10:10:48 +0100 Subject: [PATCH 04/30] Translated using Weblate (French) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (764 of 764 strings) Translated using Weblate (Punjabi) Currently translated at 97.2% (743 of 764 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (87 of 87 strings) Added translation using Weblate (Romany) Translated using Weblate (Korean) Currently translated at 97.7% (85 of 87 strings) Translated using Weblate (Swedish) Currently translated at 100.0% (87 of 87 strings) Co-authored-by: Hosted Weblate Co-authored-by: Madalin Co-authored-by: Mona Lisa Co-authored-by: NormalRandomPeople Co-authored-by: TobiGr Co-authored-by: whistlingwoods <72640314+whistlingwoods@users.noreply.github.com> Co-authored-by: Максим Горпиніч Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ko/ Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/ Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/ Translation: NewPipe/Metadata --- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 6 ++++++ app/src/main/res/values-rom/strings.xml | 3 +++ fastlane/metadata/android/ko/changelogs/1006.txt | 16 ++++++++++++++++ fastlane/metadata/android/sv/changelogs/1006.txt | 16 ++++++++++++++++ fastlane/metadata/android/uk/changelogs/1006.txt | 16 ++++++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/values-rom/strings.xml create mode 100644 fastlane/metadata/android/ko/changelogs/1006.txt create mode 100644 fastlane/metadata/android/sv/changelogs/1006.txt create mode 100644 fastlane/metadata/android/uk/changelogs/1006.txt diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e587a301d..eb28d0e21 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -862,7 +862,7 @@ Compte fermé\n\n%1$s fournit la raison suivante : %2$s Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par l\'expiration de l\'URL de streaming ou une interdiction d\'IP Erreur HTTP %1$s reçue du serveur pendant la lecture - Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par une interdiction d\'IP ou des problèmes de désobfuscation d\'URL de streaming. + Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par un bannissement d\'IP ou des problèmes de désobfuscation de l\'URL de streaming %1$s a refusé de fournir des données et a demandé un identifiant pour confirmer que le demandeur n\'est pas un robot.\n\nVotre adresse IP a peut-être été temporairement bannie par %1$s. Vous pouvez patienter un peu ou changer d\'adresse IP (par exemple en activant/désactivant un VPN, ou en passant du Wi-Fi aux données mobiles). Ce contenu n\'est pas disponible pour le pays actuellement sélectionné.\n\nModifiez votre sélection dans « Paramètres > Contenu > Pays par défaut ». diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8b5969c0c..7029dda05 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -828,4 +828,10 @@ ਅਜੇ ਤੱਕ ਕੋਈ ਫੀਡ ਗਰੁੱਪ ਨਹੀਂ ਬਣਾਇਆ ਗਿਆ ਚੈਨਲ ਗਰੁੱਪ ਪੰਨਾ ਪਸੰਦ + + + ਫ਼ਾਈਲ ਮਿਟਾਓ + ਐਂਟਰੀ ਮਿਟਾਓ + ਖ਼ਾਤਾ ਬੰਦ ਕੀਤਾ ਗਿਆ\n\n%1$s ਇਹ ਕਾਰਨ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ: %2$s + ਐਂਟਰੀ ਮਿਟਾ ਦਿੱਤੀ ਗਈ diff --git a/app/src/main/res/values-rom/strings.xml b/app/src/main/res/values-rom/strings.xml new file mode 100644 index 000000000..55344e519 --- /dev/null +++ b/app/src/main/res/values-rom/strings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/fastlane/metadata/android/ko/changelogs/1006.txt b/fastlane/metadata/android/ko/changelogs/1006.txt new file mode 100644 index 000000000..1729e9827 --- /dev/null +++ b/fastlane/metadata/android/ko/changelogs/1006.txt @@ -0,0 +1,16 @@ +# 개선됨 +타임스탬프를 클릭할 때 현재 플레이어를 유지합니다. +가능한 경우 보류 중인 다운로드 미션을 복구하세요. +파일 삭제 없이 다운로드를 삭제하는 옵션을 추가하세요. +오버레이 권한: Android > R에 대한 설명 대화 상자 표시 +사운드클라우드 링크 열기 지원 +많은 작은 개선과 최적화 + +# 고정 +7 이하의 안드로이드 버전에 대한 짧은 숫자 형식을 수정하세요. +고스트 알림 수정 +SRT 자막 파일 수정 +고정된 수많은 충돌 사고 + +# 개발 +내부 코드 현대화 diff --git a/fastlane/metadata/android/sv/changelogs/1006.txt b/fastlane/metadata/android/sv/changelogs/1006.txt new file mode 100644 index 000000000..90825e8a8 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/1006.txt @@ -0,0 +1,16 @@ +# Förbättrat +Behåll aktuell spelare när du klickar på tidsstämplar +Försök att återställa väntande nedladdningsuppdrag när det är möjligt +Lägg till alternativ för att ta bort en nedladdning utan att också ta bort filen +Överläggsbehörighet: visa förklarande dialogruta för Android > R +Stöd för att öppna on.soundcloud-länkar +Många små förbättringar och optimeringar + +# Åtgärdat +Åtgärdade formatering av korta antal för Android-versioner under 7 +Åtgärdade Ghost Notifications +Åtgärdade för SRT-undertextfiler +Åtgärdade massor av krascher + +# Utveckling +Intern kodmodernisering diff --git a/fastlane/metadata/android/uk/changelogs/1006.txt b/fastlane/metadata/android/uk/changelogs/1006.txt new file mode 100644 index 000000000..6534e9145 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/1006.txt @@ -0,0 +1,16 @@ +# Покращено +Зберігати поточний програвач при натисканні на часові позначки +Намагатися відновлювати місії, що очікують завантаження, коли це можливо +Додано опцію видалення завантаження без одночасного видалення файлу +Дозвіл на накладання: відображення пояснювального діалогового вікна для Android > R +Підтримка відкриття посилання на .soundcloud +Багато дрібних покращень та оптимізацій + +# Виправлено +Виправлено форматування короткого лічильника для версій Android нижче 7 +Виправлено сповіщення-примари +Виправлення для файлів субтитрів SRT +Виправлено безліч збоїв + +# Розробка +Модернізація внутрішнього коду From 0a65c862a36510c8234277e71a7cfd38757ac6ed Mon Sep 17 00:00:00 2001 From: tobigr Date: Tue, 30 Dec 2025 09:16:58 +0100 Subject: [PATCH 05/30] Remove empty translations --- app/src/main/res/values-br/strings.xml | 1 - app/src/main/res/values-pa/strings.xml | 2 -- 2 files changed, 3 deletions(-) diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index a0a7744f6..dfff3c2cc 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -117,5 +117,4 @@ Lenn ar video, pad: Titouroù: Video - diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 7029dda05..eb6ee58a2 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -828,8 +828,6 @@ ਅਜੇ ਤੱਕ ਕੋਈ ਫੀਡ ਗਰੁੱਪ ਨਹੀਂ ਬਣਾਇਆ ਗਿਆ ਚੈਨਲ ਗਰੁੱਪ ਪੰਨਾ ਪਸੰਦ - - ਫ਼ਾਈਲ ਮਿਟਾਓ ਐਂਟਰੀ ਮਿਟਾਓ ਖ਼ਾਤਾ ਬੰਦ ਕੀਤਾ ਗਿਆ\n\n%1$s ਇਹ ਕਾਰਨ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ: %2$s From f6085d004497af807101944623605d8338344747 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Tue, 30 Dec 2025 13:30:37 +0200 Subject: [PATCH 06/30] Replace findPreference(getString(resId) with its null safe shortcut --- .../BackupRestoreSettingsFragment.java | 3 +-- .../settings/BasePreferenceFragment.java | 4 ++-- .../settings/DebugSettingsFragment.java | 21 +++++++------------ .../settings/MainSettingsFragment.java | 4 ++-- .../settings/NotificationsSettingsFragment.kt | 7 +++---- .../settings/UpdateSettingsFragment.java | 4 ++-- .../settings/VideoAudioSettingsFragment.java | 15 +++++++------ 7 files changed, 24 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index 2900dee90..baaa93e44 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -98,10 +98,9 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { return true; }); - final Preference resetSettings = findPreference(getString(R.string.reset_settings)); + final Preference resetSettings = requirePreference(R.string.reset_settings); // Resets all settings by deleting shared preference and restarting the app // A dialogue will pop up to confirm if user intends to reset all settings - assert resetSettings != null; resetSettings.setOnPreferenceClickListener(preference -> { // Show Alert Dialogue final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index 619579f3a..21cba3daa 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -48,8 +48,8 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { } @NonNull - public final Preference requirePreference(@StringRes final int resId) { - final Preference preference = findPreference(getString(resId)); + public final T requirePreference(@StringRes final int resId) { + final T preference = findPreference(getString(resId)); Objects.requireNonNull(preference); return preference; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index d78ade49d..82f2f5bb6 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -22,27 +22,20 @@ public class DebugSettingsFragment extends BasePreferenceFragment { addPreferencesFromResourceRegistry(); final Preference allowHeapDumpingPreference = - findPreference(getString(R.string.allow_heap_dumping_key)); + requirePreference(R.string.allow_heap_dumping_key); final Preference showMemoryLeaksPreference = - findPreference(getString(R.string.show_memory_leaks_key)); + requirePreference(R.string.show_memory_leaks_key); final Preference showImageIndicatorsPreference = - findPreference(getString(R.string.show_image_indicators_key)); + requirePreference(R.string.show_image_indicators_key); final Preference checkNewStreamsPreference = - findPreference(getString(R.string.check_new_streams_key)); + requirePreference(R.string.check_new_streams_key); final Preference crashTheAppPreference = - findPreference(getString(R.string.crash_the_app_key)); + requirePreference(R.string.crash_the_app_key); final Preference showErrorSnackbarPreference = - findPreference(getString(R.string.show_error_snackbar_key)); + requirePreference(R.string.show_error_snackbar_key); final Preference createErrorNotificationPreference = - findPreference(getString(R.string.create_error_notification_key)); + requirePreference(R.string.create_error_notification_key); - assert allowHeapDumpingPreference != null; - assert showMemoryLeaksPreference != null; - assert showImageIndicatorsPreference != null; - assert checkNewStreamsPreference != null; - assert crashTheAppPreference != null; - assert showErrorSnackbarPreference != null; - assert createErrorNotificationPreference != null; final Optional optBVLeakCanary = getBVDLeakCanary(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 32e33d55b..cb3de39a0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -25,7 +25,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { // Check if the app is updatable if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) { getPreferenceScreen().removePreference( - findPreference(getString(R.string.update_pref_screen_key))); + requirePreference(R.string.update_pref_screen_key)); defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); } @@ -33,7 +33,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { // Hide debug preferences in RELEASE build variant if (!DEBUG) { getPreferenceScreen().removePreference( - findPreference(getString(R.string.debug_pref_screen_key))); + requirePreference(R.string.debug_pref_screen_key)); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt index 2d3344c09..d6b0a84da 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt @@ -29,8 +29,7 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.notifications_settings) - streamsNotificationsPreference = - findPreference(getString(R.string.enable_streams_notifications)) + streamsNotificationsPreference = requirePreference(R.string.enable_streams_notifications) // main check is done in onResume, but also do it here to prevent flickering updateEnabledState(NotificationHelper.areNotificationsEnabledOnDevice(requireContext())) @@ -125,8 +124,8 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen private fun updateSubscriptions(subscriptions: List) { val notified = subscriptions.count { it.notificationMode != NotificationMode.DISABLED } - val preference = findPreference(getString(R.string.streams_notifications_channels_key)) - preference?.apply { summary = "$notified/${subscriptions.size}" } + val preference = requirePreference(R.string.streams_notifications_channels_key) + preference.summary = "$notified/${subscriptions.size}" } private fun onError(e: Throwable) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index b8d0aa556..8923972b0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -34,9 +34,9 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResourceRegistry(); - findPreference(getString(R.string.update_app_key)) + requirePreference(R.string.update_app_key) .setOnPreferenceChangeListener(updatePreferenceChange); - findPreference(getString(R.string.manual_update_key)) + requirePreference(R.string.manual_update_key) .setOnPreferenceClickListener(manualUpdateClick); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java index a1f563724..c5c4c480c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -90,12 +90,12 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { showHigherResolutions); // get resolution preferences - final ListPreference defaultResolution = findPreference( - getString(R.string.default_resolution_key)); - final ListPreference defaultPopupResolution = findPreference( - getString(R.string.default_popup_resolution_key)); - final ListPreference mobileDataResolution = findPreference( - getString(R.string.limit_mobile_data_usage_key)); + final ListPreference defaultResolution = requirePreference( + R.string.default_resolution_key); + final ListPreference defaultPopupResolution = requirePreference( + R.string.default_popup_resolution_key); + final ListPreference mobileDataResolution = requirePreference( + R.string.limit_mobile_data_usage_key); // update resolution preferences with new resolutions, entries & values for each defaultResolution.setEntries(resolutionListDescriptions.toArray(new String[0])); @@ -161,8 +161,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { } } - final ListPreference durations = findPreference( - getString(R.string.seek_duration_key)); + final ListPreference durations = requirePreference(R.string.seek_duration_key); durations.setEntryValues(displayedDurationValues.toArray(new CharSequence[0])); durations.setEntries(displayedDescriptionValues.toArray(new CharSequence[0])); final int selectedDuration = Integer.parseInt(durations.getValue()); From 7806a708c26b41a3386ecb06ecd5ec6a178ce53d Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Wed, 31 Dec 2025 05:23:14 +0200 Subject: [PATCH 07/30] Correct typo in playlist db sql query Solve #12855 --- .../schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt index 8e0b80c3b..36a80bc91 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt @@ -28,7 +28,7 @@ interface PlaylistRemoteDAO : BasicDAO { @Query("SELECT * FROM remote_playlists WHERE uid = :playlistId") fun getPlaylist(playlistId: Long): Flowable - @Query("SELECT * FROM remote_playlists WHERE url = :url AND uid = :serviceId") + @Query("SELECT * FROM remote_playlists WHERE url = :url AND service_id = :serviceId") fun getPlaylist(serviceId: Long, url: String?): Flowable> @get:Query("SELECT * FROM remote_playlists ORDER BY display_index") From 6c238fafbe8907f536cf2a24e83d0f725c1bf358 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Wed, 31 Dec 2025 17:05:12 +0800 Subject: [PATCH 08/30] fixup! Convert newpipe/util/KioskTranslator.java to kotlin Copyright and license header are not doc-comments. Move them to the top of the file. Also re-write it to follow the SPDX formatting as we have followed in the recent Kotlin conversion. Additionally, use a better name for the context parameter. Signed-off-by: Aayush Gupta --- .../schabi/newpipe/util/KioskTranslator.kt | 61 +++++++------------ 1 file changed, 22 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt index 39167e969..1f86f5db7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt @@ -1,50 +1,33 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.util import android.content.Context import org.schabi.newpipe.R -/** - * Created by Christian Schabesberger on 28.09.17. - * KioskTranslator.java is part of NewPipe. - * - * - * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * - * - * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * - * - * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see //www.gnu.org/licenses/>. - * - */ object KioskTranslator { @JvmStatic - fun getTranslatedKioskName(kioskId: String, c: Context): String { + fun getTranslatedKioskName(kioskId: String, context: Context): String { return when (kioskId) { - "Trending" -> c.getString(R.string.trending) - "Top 50" -> c.getString(R.string.top_50) - "New & hot" -> c.getString(R.string.new_and_hot) - "Local" -> c.getString(R.string.local) - "Recently added" -> c.getString(R.string.recently_added) - "Most liked" -> c.getString(R.string.most_liked) - "conferences" -> c.getString(R.string.conferences) - "recent" -> c.getString(R.string.recent) - "live" -> c.getString(R.string.duration_live) - "Featured" -> c.getString(R.string.featured) - "Radio" -> c.getString(R.string.radio) - "trending_gaming" -> c.getString(R.string.trending_gaming) - "trending_music" -> c.getString(R.string.trending_music) - "trending_movies_and_shows" -> c.getString(R.string.trending_movies) - "trending_podcasts_episodes" -> c.getString(R.string.trending_podcasts) + "Trending" -> context.getString(R.string.trending) + "Top 50" -> context.getString(R.string.top_50) + "New & hot" -> context.getString(R.string.new_and_hot) + "Local" -> context.getString(R.string.local) + "Recently added" -> context.getString(R.string.recently_added) + "Most liked" -> context.getString(R.string.most_liked) + "conferences" -> context.getString(R.string.conferences) + "recent" -> context.getString(R.string.recent) + "live" -> context.getString(R.string.duration_live) + "Featured" -> context.getString(R.string.featured) + "Radio" -> context.getString(R.string.radio) + "trending_gaming" -> context.getString(R.string.trending_gaming) + "trending_music" -> context.getString(R.string.trending_music) + "trending_movies_and_shows" -> context.getString(R.string.trending_movies) + "trending_podcasts_episodes" -> context.getString(R.string.trending_podcasts) else -> kioskId } } From b4f526c2a5f5cceeea001d1eea653d9aeb3af7c0 Mon Sep 17 00:00:00 2001 From: Aayush Gupta Date: Thu, 1 Jan 2026 16:51:56 +0800 Subject: [PATCH 09/30] workflows: Update actions versions Signed-off-by: Aayush Gupta --- .github/workflows/build-release-apk.yml | 6 +++--- .github/workflows/ci.yml | 20 ++++++++++---------- .github/workflows/image-minimizer.yml | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 0fad8e169..b558d90dd 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -7,11 +7,11 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: 'master' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' @@ -32,7 +32,7 @@ jobs: mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk" - name: "Upload APK" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app path: app/build/outputs/apk/release/*.apk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a184dd83d..d42c5a0b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,8 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: actions/checkout@v6 + - uses: gradle/actions/wrapper-validation@v4 - name: create and checkout branch # push events already checked out the branch @@ -48,7 +48,7 @@ jobs: run: git checkout -B "$BRANCH" - name: set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" @@ -58,7 +58,7 @@ jobs: run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint - name: Upload APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app path: app/build/outputs/apk/debug/*.apk @@ -80,7 +80,7 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Enable KVM run: | @@ -89,7 +89,7 @@ jobs: sudo udevadm trigger --name-match=kvm - name: set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" @@ -104,7 +104,7 @@ jobs: script: ./gradlew connectedCheck --stacktrace - name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: failure() with: name: android-test-report-api${{ matrix.api-level }} @@ -118,19 +118,19 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" cache: 'gradle' - name: Cache SonarCloud packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar diff --git a/.github/workflows/image-minimizer.yml b/.github/workflows/image-minimizer.yml index d9241c33b..264a0ac6c 100644 --- a/.github/workflows/image-minimizer.yml +++ b/.github/workflows/image-minimizer.yml @@ -17,9 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 16 @@ -27,7 +27,7 @@ jobs: run: npm i probe-image-size@7.2.3 --ignore-scripts - name: Minimize simple images - uses: actions/github-script@v7 + uses: actions/github-script@v8 timeout-minutes: 3 with: script: | From 74cf302bd6c2f706a257be9ef5764c8057d39cf2 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Wed, 31 Dec 2025 08:37:11 +0200 Subject: [PATCH 10/30] Convert newpipe/local/playlist/RemotePlaylistManager to kotlin --- .../local/playlist/RemotePlaylistManager.java | 69 ------------------- .../local/playlist/RemotePlaylistManager.kt | 61 ++++++++++++++++ 2 files changed, 61 insertions(+), 69 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java create mode 100644 app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.kt diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java deleted file mode 100644 index 08b203a7e..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.schabi.newpipe.local.playlist; - -import org.schabi.newpipe.database.AppDatabase; -import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; - -import java.util.List; - -import io.reactivex.rxjava3.core.Completable; -import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.schedulers.Schedulers; - -public class RemotePlaylistManager { - - private final AppDatabase database; - private final PlaylistRemoteDAO playlistRemoteTable; - - public RemotePlaylistManager(final AppDatabase db) { - database = db; - playlistRemoteTable = db.playlistRemoteDAO(); - } - - public Flowable> getPlaylists() { - return playlistRemoteTable.getPlaylists().subscribeOn(Schedulers.io()); - } - - public Flowable getPlaylist(final long playlistId) { - return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io()); - } - - public Flowable> getPlaylist(final PlaylistInfo info) { - return playlistRemoteTable.getPlaylist(info.getServiceId(), info.getUrl()) - .subscribeOn(Schedulers.io()); - } - - public Single deletePlaylist(final long playlistId) { - return Single.fromCallable(() -> playlistRemoteTable.deletePlaylist(playlistId)) - .subscribeOn(Schedulers.io()); - } - - public Completable updatePlaylists(final List updateItems, - final List deletedItems) { - return Completable.fromRunnable(() -> database.runInTransaction(() -> { - for (final Long uid: deletedItems) { - playlistRemoteTable.deletePlaylist(uid); - } - for (final PlaylistRemoteEntity item: updateItems) { - playlistRemoteTable.upsert(item); - } - })).subscribeOn(Schedulers.io()); - } - - public Single onBookmark(final PlaylistInfo playlistInfo) { - return Single.fromCallable(() -> { - final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); - return playlistRemoteTable.upsert(playlist); - }).subscribeOn(Schedulers.io()); - } - - public Single onUpdate(final long playlistId, final PlaylistInfo playlistInfo) { - return Single.fromCallable(() -> { - final PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo); - playlist.setUid(playlistId); - return playlistRemoteTable.update(playlist); - }).subscribeOn(Schedulers.io()); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.kt b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.kt new file mode 100644 index 000000000..6961b6bb4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/RemotePlaylistManager.kt @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.local.playlist + +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.schabi.newpipe.database.AppDatabase +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity +import org.schabi.newpipe.extractor.playlist.PlaylistInfo + +class RemotePlaylistManager(private val database: AppDatabase) { + private val playlistRemoteTable = database.playlistRemoteDAO() + + val playlists: Flowable> + get() = playlistRemoteTable.playlists.subscribeOn(Schedulers.io()) + + fun getPlaylist(playlistId: Long): Flowable { + return playlistRemoteTable.getPlaylist(playlistId).subscribeOn(Schedulers.io()) + } + + fun getPlaylist(info: PlaylistInfo): Flowable> { + return playlistRemoteTable.getPlaylist(info.serviceId.toLong(), info.url) + .subscribeOn(Schedulers.io()) + } + + fun deletePlaylist(playlistId: Long): Single { + return Single.fromCallable { playlistRemoteTable.deletePlaylist(playlistId) } + .subscribeOn(Schedulers.io()) + } + + fun updatePlaylists( + updateItems: List, + deletedItems: List + ): Completable { + return Completable.fromRunnable { + database.runInTransaction { + deletedItems.forEach { playlistRemoteTable.deletePlaylist(it) } + updateItems.forEach { playlistRemoteTable.upsert(it) } + } + }.subscribeOn(Schedulers.io()) + } + + fun onBookmark(playlistInfo: PlaylistInfo): Single { + return Single.fromCallable { + val playlist = PlaylistRemoteEntity(playlistInfo) + playlistRemoteTable.upsert(playlist) + }.subscribeOn(Schedulers.io()) + } + + fun onUpdate(playlistId: Long, playlistInfo: PlaylistInfo): Single { + return Single.fromCallable { + val playlist = PlaylistRemoteEntity(playlistInfo).apply { uid = playlistId } + playlistRemoteTable.update(playlist) + }.subscribeOn(Schedulers.io()) + } +} From 3398b4cdc939bcab92459452d4b0a67b1d819182 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Mon, 29 Dec 2025 11:59:31 +0200 Subject: [PATCH 11/30] Convert newpipe/fragments/list/search/Suggestion{Item,ListAdapter} to kotlin --- .../fragments/list/search/SuggestionItem.java | 32 ------- .../fragments/list/search/SuggestionItem.kt | 19 ++++ .../list/search/SuggestionListAdapter.java | 94 ------------------- .../list/search/SuggestionListAdapter.kt | 74 +++++++++++++++ 4 files changed, 93 insertions(+), 126 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.kt diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java deleted file mode 100644 index 83f68dbb5..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.schabi.newpipe.fragments.list.search; - -import androidx.annotation.NonNull; - -public class SuggestionItem { - final boolean fromHistory; - public final String query; - - public SuggestionItem(final boolean fromHistory, final String query) { - this.fromHistory = fromHistory; - this.query = query; - } - - @Override - public boolean equals(final Object o) { - if (o instanceof SuggestionItem) { - return query.equals(((SuggestionItem) o).query); - } - return false; - } - - @Override - public int hashCode() { - return query.hashCode(); - } - - @NonNull - @Override - public String toString() { - return "[" + fromHistory + "→" + query + "]"; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.kt new file mode 100644 index 000000000..1317f9acb --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.fragments.list.search + +class SuggestionItem(@JvmField val fromHistory: Boolean, @JvmField val query: String) { + override fun equals(other: Any?): Boolean { + if (other is SuggestionItem) { + return query == other.query + } + return false + } + + override fun hashCode() = query.hashCode() + + override fun toString() = "[$fromHistory→$query]" +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java deleted file mode 100644 index 6a330be0f..000000000 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.schabi.newpipe.fragments.list.search; - -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding; - -public class SuggestionListAdapter - extends ListAdapter { - private OnSuggestionItemSelected listener; - - public SuggestionListAdapter() { - super(new SuggestionItemCallback()); - } - - public void setListener(final OnSuggestionItemSelected listener) { - this.listener = listener; - } - - @NonNull - @Override - public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent, - final int viewType) { - return new SuggestionItemHolder(ItemSearchSuggestionBinding - .inflate(LayoutInflater.from(parent.getContext()), parent, false)); - } - - @Override - public void onBindViewHolder(final SuggestionItemHolder holder, final int position) { - final SuggestionItem currentItem = getItem(position); - holder.updateFrom(currentItem); - holder.itemBinding.suggestionSearch.setOnClickListener(v -> { - if (listener != null) { - listener.onSuggestionItemSelected(currentItem); - } - }); - holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> { - if (listener != null) { - listener.onSuggestionItemLongClick(currentItem); - } - return true; - }); - holder.itemBinding.suggestionInsert.setOnClickListener(v -> { - if (listener != null) { - listener.onSuggestionItemInserted(currentItem); - } - }); - } - - public interface OnSuggestionItemSelected { - void onSuggestionItemSelected(SuggestionItem item); - - void onSuggestionItemInserted(SuggestionItem item); - - void onSuggestionItemLongClick(SuggestionItem item); - } - - public static final class SuggestionItemHolder extends RecyclerView.ViewHolder { - private final ItemSearchSuggestionBinding itemBinding; - - private SuggestionItemHolder(final ItemSearchSuggestionBinding binding) { - super(binding.getRoot()); - this.itemBinding = binding; - } - - private void updateFrom(final SuggestionItem item) { - itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history - : R.drawable.ic_search); - itemBinding.itemSuggestionQuery.setText(item.query); - } - } - - private static final class SuggestionItemCallback - extends DiffUtil.ItemCallback { - @Override - public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem, - @NonNull final SuggestionItem newItem) { - return oldItem.fromHistory == newItem.fromHistory - && oldItem.query.equals(newItem.query); - } - - @Override - public boolean areContentsTheSame(@NonNull final SuggestionItem oldItem, - @NonNull final SuggestionItem newItem) { - return true; // items' contents never change; the list of items themselves does - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.kt b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.kt new file mode 100644 index 000000000..4eb4c1574 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.fragments.list.search + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.schabi.newpipe.R +import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding +import org.schabi.newpipe.fragments.list.search.SuggestionListAdapter.SuggestionItemHolder + +class SuggestionListAdapter : + ListAdapter(SuggestionItemCallback()) { + + var listener: OnSuggestionItemSelected? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SuggestionItemHolder { + return SuggestionItemHolder( + ItemSearchSuggestionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + + override fun onBindViewHolder(holder: SuggestionItemHolder, position: Int) { + val currentItem = getItem(position) + holder.updateFrom(currentItem) + holder.binding.suggestionSearch.setOnClickListener { + listener?.onSuggestionItemSelected(currentItem) + } + holder.binding.suggestionSearch.setOnLongClickListener { + listener?.onSuggestionItemLongClick(currentItem) + true + } + holder.binding.suggestionInsert.setOnClickListener { + listener?.onSuggestionItemInserted(currentItem) + } + } + + interface OnSuggestionItemSelected { + fun onSuggestionItemSelected(item: SuggestionItem) + + fun onSuggestionItemInserted(item: SuggestionItem) + + fun onSuggestionItemLongClick(item: SuggestionItem) + } + + class SuggestionItemHolder(val binding: ItemSearchSuggestionBinding) : + RecyclerView.ViewHolder(binding.getRoot()) { + fun updateFrom(item: SuggestionItem) { + binding.itemSuggestionIcon.setImageResource( + if (item.fromHistory) { + R.drawable.ic_history + } else { + R.drawable.ic_search + } + ) + binding.itemSuggestionQuery.text = item.query + } + } + + private class SuggestionItemCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: SuggestionItem, newItem: SuggestionItem): Boolean { + return oldItem.fromHistory == newItem.fromHistory && oldItem.query == newItem.query + } + + override fun areContentsTheSame(oldItem: SuggestionItem, newItem: SuggestionItem): Boolean { + return true // items' contents never change; the list of items themselves does + } + } +} From fef8a2455c2350c794b219d3a36b820d1d27baf7 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Sun, 28 Dec 2025 21:44:19 +0200 Subject: [PATCH 12/30] Convert newpipe/util/image/ImageStrategy to kotlin --- .../newpipe/util/image/ImageStrategy.java | 195 ------------------ .../newpipe/util/image/ImageStrategy.kt | 191 +++++++++++++++++ 2 files changed, 191 insertions(+), 195 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt diff --git a/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java deleted file mode 100644 index da97179b6..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.schabi.newpipe.util.image; - -import static org.schabi.newpipe.extractor.Image.HEIGHT_UNKNOWN; -import static org.schabi.newpipe.extractor.Image.WIDTH_UNKNOWN; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.schabi.newpipe.extractor.Image; - -import java.util.Comparator; -import java.util.List; - -public final class ImageStrategy { - - // when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred - // image quality is to these values (H stands for "Height") - private static final int BEST_LOW_H = 75; - private static final int BEST_MEDIUM_H = 250; - - private static PreferredImageQuality preferredImageQuality = PreferredImageQuality.MEDIUM; - - private ImageStrategy() { - } - - public static void setPreferredImageQuality(final PreferredImageQuality preferredImageQuality) { - ImageStrategy.preferredImageQuality = preferredImageQuality; - } - - public static boolean shouldLoadImages() { - return preferredImageQuality != PreferredImageQuality.NONE; - } - - - static double estimatePixelCount(final Image image, final double widthOverHeight) { - if (image.getHeight() == HEIGHT_UNKNOWN) { - if (image.getWidth() == WIDTH_UNKNOWN) { - // images whose size is completely unknown will be in their own subgroups, so - // any one of them will do, hence returning the same value for all of them - return 0; - } else { - return image.getWidth() * image.getWidth() / widthOverHeight; - } - } else if (image.getWidth() == WIDTH_UNKNOWN) { - return image.getHeight() * image.getHeight() * widthOverHeight; - } else { - return image.getHeight() * image.getWidth(); - } - } - - /** - * {@link #choosePreferredImage(List)} contains the description for this function's logic. - * - * @param images the images from which to choose - * @param nonNoneQuality the preferred quality (must NOT be {@link PreferredImageQuality#NONE}) - * @return the chosen preferred image, or {@link null} if the list is empty - * @see #choosePreferredImage(List) - */ - @Nullable - static String choosePreferredImage(@NonNull final List images, - final PreferredImageQuality nonNoneQuality) { - // this will be used to estimate the pixel count for images where only one of height or - // width are known - final double widthOverHeight = images.stream() - .filter(image -> image.getHeight() != HEIGHT_UNKNOWN - && image.getWidth() != WIDTH_UNKNOWN) - .mapToDouble(image -> ((double) image.getWidth()) / image.getHeight()) - .findFirst() - .orElse(1.0); - - final Image.ResolutionLevel preferredLevel = nonNoneQuality.toResolutionLevel(); - final Comparator initialComparator = Comparator - // the first step splits the images into groups of resolution levels - .comparingInt(i -> { - if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) { - return 3; // avoid unknowns as much as possible - } else if (i.getEstimatedResolutionLevel() == preferredLevel) { - return 0; // prefer a matching resolution level - } else if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.MEDIUM) { - return 1; // the preferredLevel is only 1 "step" away (either HIGH or LOW) - } else { - return 2; // the preferredLevel is the furthest away possible (2 "steps") - } - }) - // then each level's group is further split into two subgroups, one with known image - // size (which is also the preferred subgroup) and the other without - .thenComparing(image -> - image.getHeight() == HEIGHT_UNKNOWN && image.getWidth() == WIDTH_UNKNOWN); - - // The third step chooses, within each subgroup with known image size, the best image based - // on how close its size is to BEST_LOW_H or BEST_MEDIUM_H (with proper units). Subgroups - // without known image size will be left untouched since estimatePixelCount always returns - // the same number for those. - final Comparator finalComparator = switch (nonNoneQuality) { - case NONE -> initialComparator; // unreachable - case LOW -> initialComparator.thenComparingDouble(image -> { - final double pixelCount = estimatePixelCount(image, widthOverHeight); - return Math.abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight); - }); - case MEDIUM -> initialComparator.thenComparingDouble(image -> { - final double pixelCount = estimatePixelCount(image, widthOverHeight); - return Math.abs(pixelCount - BEST_MEDIUM_H * BEST_MEDIUM_H * widthOverHeight); - }); - case HIGH -> initialComparator.thenComparingDouble( - // this is reversed with a - so that the highest resolution is chosen - i -> -estimatePixelCount(i, widthOverHeight)); - }; - - return images.stream() - // using "min" basically means "take the first group, then take the first subgroup, - // then choose the best image, while ignoring all other groups and subgroups" - .min(finalComparator) - .map(Image::getUrl) - .orElse(null); - } - - /** - * Chooses an image amongst the provided list based on the user preference previously set with - * {@link #setPreferredImageQuality(PreferredImageQuality)}. {@code null} will be returned in - * case the list is empty or the user preference is to not show images. - *
- * These properties will be preferred, from most to least important: - *
    - *
  1. The image's {@link Image#getEstimatedResolutionLevel()} is not unknown and is close - * to {@link #preferredImageQuality}
  2. - *
  3. At least one of the image's width or height are known
  4. - *
  5. The highest resolution image is finally chosen if the user's preference is {@link - * PreferredImageQuality#HIGH}, otherwise the chosen image is the one that has the height - * closest to {@link #BEST_LOW_H} or {@link #BEST_MEDIUM_H}
  6. - *
- *
- * Use {@link #imageListToDbUrl(List)} if the URL is going to be saved to the database, to avoid - * saving nothing in case at the moment of saving the user preference is to not show images. - * - * @param images the images from which to choose - * @return the chosen preferred image, or {@link null} if the list is empty or the user disabled - * images - * @see #imageListToDbUrl(List) - */ - @Nullable - public static String choosePreferredImage(@NonNull final List images) { - if (preferredImageQuality == PreferredImageQuality.NONE) { - return null; // do not load images - } - - return choosePreferredImage(images, preferredImageQuality); - } - - /** - * Like {@link #choosePreferredImage(List)}, except that if {@link #preferredImageQuality} is - * {@link PreferredImageQuality#NONE} an image will be chosen anyway (with preferred quality - * {@link PreferredImageQuality#MEDIUM}. - *
- * To go back to a list of images (obviously with just the one chosen image) from a URL saved in - * the database use {@link #dbUrlToImageList(String)}. - * - * @param images the images from which to choose - * @return the chosen preferred image, or {@link null} if the list is empty - * @see #choosePreferredImage(List) - * @see #dbUrlToImageList(String) - */ - @Nullable - public static String imageListToDbUrl(@NonNull final List images) { - final PreferredImageQuality quality; - if (preferredImageQuality == PreferredImageQuality.NONE) { - quality = PreferredImageQuality.MEDIUM; - } else { - quality = preferredImageQuality; - } - - return choosePreferredImage(images, quality); - } - - /** - * Wraps the URL (coming from the database) in a {@code List} so that it is usable - * seamlessly in all of the places where the extractor would return a list of images, including - * allowing to build info objects based on database objects. - *
- * To obtain a url to save to the database from a list of images use {@link - * #imageListToDbUrl(List)}. - * - * @param url the URL to wrap coming from the database, or {@code null} to get an empty list - * @return a list containing just one {@link Image} wrapping the provided URL, with unknown - * image size fields, or an empty list if the URL is {@code null} - * @see #imageListToDbUrl(List) - */ - @NonNull - public static List dbUrlToImageList(@Nullable final String url) { - if (url == null) { - return List.of(); - } else { - return List.of(new Image(url, -1, -1, Image.ResolutionLevel.UNKNOWN)); - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt new file mode 100644 index 000000000..aa59b4d0a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.kt @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util.image + +import org.schabi.newpipe.extractor.Image +import org.schabi.newpipe.extractor.Image.ResolutionLevel +import kotlin.math.abs + +object ImageStrategy { + // when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred + // image quality is to these values (H stands for "Height") + private const val BEST_LOW_H = 75 + private const val BEST_MEDIUM_H = 250 + + private var preferredImageQuality = PreferredImageQuality.MEDIUM + + @JvmStatic + fun setPreferredImageQuality(preferredImageQuality: PreferredImageQuality) { + ImageStrategy.preferredImageQuality = preferredImageQuality + } + + @JvmStatic + fun shouldLoadImages(): Boolean { + return preferredImageQuality != PreferredImageQuality.NONE + } + + @JvmStatic + fun estimatePixelCount(image: Image, widthOverHeight: Double): Double { + if (image.height == Image.HEIGHT_UNKNOWN) { + if (image.width == Image.WIDTH_UNKNOWN) { + // images whose size is completely unknown will be in their own subgroups, so + // any one of them will do, hence returning the same value for all of them + return 0.0 + } else { + return image.width * image.width / widthOverHeight + } + } else if (image.width == Image.WIDTH_UNKNOWN) { + return image.height * image.height * widthOverHeight + } else { + return (image.height * image.width).toDouble() + } + } + + /** + * [choosePreferredImage] contains the description for this function's logic. + * + * @param images the images from which to choose + * @param nonNoneQuality the preferred quality (must NOT be [PreferredImageQuality.NONE]) + * @return the chosen preferred image, or `null` if the list is empty + * @see [choosePreferredImage] + */ + @JvmStatic + fun choosePreferredImage(images: List, nonNoneQuality: PreferredImageQuality): String? { + // this will be used to estimate the pixel count for images where only one of height or + // width are known + val widthOverHeight = images + .filter { image -> + image.height != Image.HEIGHT_UNKNOWN && image.width != Image.WIDTH_UNKNOWN + } + .map { image -> (image.width.toDouble()) / image.height } + .elementAtOrNull(0) ?: 1.0 + + val preferredLevel = nonNoneQuality.toResolutionLevel() + // TODO: rewrite using kotlin collections API `groupBy` will be handy + val initialComparator = + Comparator // the first step splits the images into groups of resolution levels + .comparingInt { i: Image -> + return@comparingInt when (i.estimatedResolutionLevel) { + // avoid unknowns as much as possible + ResolutionLevel.UNKNOWN -> 3 + + // prefer a matching resolution level + preferredLevel -> 0 + + // the preferredLevel is only 1 "step" away (either HIGH or LOW) + ResolutionLevel.MEDIUM -> 1 + + // the preferredLevel is the furthest away possible (2 "steps") + else -> 2 + } + } + // then each level's group is further split into two subgroups, one with known image + // size (which is also the preferred subgroup) and the other without + .thenComparing { image -> image.height == Image.HEIGHT_UNKNOWN && image.width == Image.WIDTH_UNKNOWN } + + // The third step chooses, within each subgroup with known image size, the best image based + // on how close its size is to BEST_LOW_H or BEST_MEDIUM_H (with proper units). Subgroups + // without known image size will be left untouched since estimatePixelCount always returns + // the same number for those. + val finalComparator = when (nonNoneQuality) { + PreferredImageQuality.NONE -> initialComparator + PreferredImageQuality.LOW -> initialComparator.thenComparingDouble { image -> + val pixelCount = estimatePixelCount(image, widthOverHeight) + abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight) + } + + PreferredImageQuality.MEDIUM -> initialComparator.thenComparingDouble { image -> + val pixelCount = estimatePixelCount(image, widthOverHeight) + abs(pixelCount - BEST_MEDIUM_H * BEST_MEDIUM_H * widthOverHeight) + } + + PreferredImageQuality.HIGH -> initialComparator.thenComparingDouble { image -> + // this is reversed with a - so that the highest resolution is chosen + -estimatePixelCount(image, widthOverHeight) + } + } + + return images.stream() // using "min" basically means "take the first group, then take the first subgroup, + // then choose the best image, while ignoring all other groups and subgroups" + .min(finalComparator) + .map(Image::getUrl) + .orElse(null) + } + + /** + * Chooses an image amongst the provided list based on the user preference previously set with + * [setPreferredImageQuality]. `null` will be returned in + * case the list is empty or the user preference is to not show images. + *
+ * These properties will be preferred, from most to least important: + * + * 1. The image's [Image.estimatedResolutionLevel] is not unknown and is close to [preferredImageQuality] + * 2. At least one of the image's width or height are known + * 3. The highest resolution image is finally chosen if the user's preference is + * [PreferredImageQuality.HIGH], otherwise the chosen image is the one that has the height + * closest to [BEST_LOW_H] or [BEST_MEDIUM_H] + * + *
+ * Use [imageListToDbUrl] if the URL is going to be saved to the database, to avoid + * saving nothing in case at the moment of saving the user preference is to not show images. + * + * @param images the images from which to choose + * @return the chosen preferred image, or `null` if the list is empty or the user disabled + * images + * @see [imageListToDbUrl] + */ + @JvmStatic + fun choosePreferredImage(images: List): String? { + if (preferredImageQuality == PreferredImageQuality.NONE) { + return null // do not load images + } + + return choosePreferredImage(images, preferredImageQuality) + } + + /** + * Like [choosePreferredImage], except that if [preferredImageQuality] is + * [PreferredImageQuality.NONE] an image will be chosen anyway (with preferred quality + * [PreferredImageQuality.MEDIUM]. + *

+ * To go back to a list of images (obviously with just the one chosen image) from a URL saved in + * the database use [dbUrlToImageList]. + * + * @param images the images from which to choose + * @return the chosen preferred image, or `null` if the list is empty + * @see [choosePreferredImage] + * @see [dbUrlToImageList] + */ + @JvmStatic + fun imageListToDbUrl(images: List): String? { + val quality = when (preferredImageQuality) { + PreferredImageQuality.NONE -> PreferredImageQuality.MEDIUM + else -> preferredImageQuality + } + + return choosePreferredImage(images, quality) + } + + /** + * Wraps the URL (coming from the database) in a `List` so that it is usable + * seamlessly in all of the places where the extractor would return a list of images, including + * allowing to build info objects based on database objects. + *

+ * To obtain a url to save to the database from a list of images use [imageListToDbUrl]. + * + * @param url the URL to wrap coming from the database, or `null` to get an empty list + * @return a list containing just one [Image] wrapping the provided URL, with unknown + * image size fields, or an empty list if the URL is `null` + * @see [imageListToDbUrl] + */ + @JvmStatic + fun dbUrlToImageList(url: String?): List { + return when (url) { + null -> listOf() + else -> listOf(Image(url, -1, -1, ResolutionLevel.UNKNOWN)) + } + } +} From 4ef4ed15f1d63d8a8d883f485a5f9b164614e737 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Sun, 28 Dec 2025 19:16:51 +0200 Subject: [PATCH 13/30] Convert newpipe/util/image/PreferredImageQuality to kotlin --- .../util/image/PreferredImageQuality.java | 39 ------------------- .../util/image/PreferredImageQuality.kt | 38 ++++++++++++++++++ 2 files changed, 38 insertions(+), 39 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.kt diff --git a/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java deleted file mode 100644 index 7106359b3..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.schabi.newpipe.util.image; - -import android.content.Context; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.extractor.Image; - -public enum PreferredImageQuality { - NONE, - LOW, - MEDIUM, - HIGH; - - public static PreferredImageQuality fromPreferenceKey(final Context context, final String key) { - if (context.getString(R.string.image_quality_none_key).equals(key)) { - return NONE; - } else if (context.getString(R.string.image_quality_low_key).equals(key)) { - return LOW; - } else if (context.getString(R.string.image_quality_high_key).equals(key)) { - return HIGH; - } else { - return MEDIUM; // default to medium - } - } - - public Image.ResolutionLevel toResolutionLevel() { - switch (this) { - case LOW: - return Image.ResolutionLevel.LOW; - case MEDIUM: - return Image.ResolutionLevel.MEDIUM; - case HIGH: - return Image.ResolutionLevel.HIGH; - default: - case NONE: - return Image.ResolutionLevel.UNKNOWN; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.kt b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.kt new file mode 100644 index 000000000..b90ba87aa --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/image/PreferredImageQuality.kt @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util.image + +import android.content.Context +import org.schabi.newpipe.R +import org.schabi.newpipe.extractor.Image.ResolutionLevel + +enum class PreferredImageQuality { + NONE, + LOW, + MEDIUM, + HIGH; + + fun toResolutionLevel(): ResolutionLevel { + return when (this) { + LOW -> ResolutionLevel.LOW + MEDIUM -> ResolutionLevel.MEDIUM + HIGH -> ResolutionLevel.HIGH + NONE -> ResolutionLevel.UNKNOWN + } + } + + companion object { + @JvmStatic + fun fromPreferenceKey(context: Context, key: String?): PreferredImageQuality { + return when (key) { + context.getString(R.string.image_quality_none_key) -> NONE + context.getString(R.string.image_quality_low_key) -> LOW + context.getString(R.string.image_quality_high_key) -> HIGH + else -> MEDIUM // default to medium + } + } + } +} From 873b2be9cae89fccefc031699cb2024e1ea5f75f Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Fri, 26 Dec 2025 18:32:30 +0200 Subject: [PATCH 14/30] Convert newpipe/util/text/TimestampLongPressClickableSpan.java to kotlin Also convert one class used by it into java record --- .../newpipe/util/text/TimestampExtractor.java | 26 +------ .../text/TimestampLongPressClickableSpan.java | 78 ------------------- .../text/TimestampLongPressClickableSpan.kt | 69 ++++++++++++++++ 3 files changed, 70 insertions(+), 103 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.kt diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java b/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java index be603f41a..b1357b943 100644 --- a/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java +++ b/app/src/main/java/org/schabi/newpipe/util/text/TimestampExtractor.java @@ -54,30 +54,6 @@ public final class TimestampExtractor { return new TimestampMatchDTO(timestampStart, timestampEnd, seconds); } - public static class TimestampMatchDTO { - private final int timestampStart; - private final int timestampEnd; - private final int seconds; - - public TimestampMatchDTO( - final int timestampStart, - final int timestampEnd, - final int seconds) { - this.timestampStart = timestampStart; - this.timestampEnd = timestampEnd; - this.seconds = seconds; - } - - public int timestampStart() { - return timestampStart; - } - - public int timestampEnd() { - return timestampEnd; - } - - public int seconds() { - return seconds; - } + public record TimestampMatchDTO(int timestampStart, int timestampEnd, int seconds) { } } diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java deleted file mode 100644 index 35a9fd996..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.schabi.newpipe.util.text; - -import static org.schabi.newpipe.util.text.InternalUrlsHandler.playOnPopup; - -import android.content.Context; -import android.view.View; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.extractor.ServiceList; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.util.external_communication.ShareUtils; - -import io.reactivex.rxjava3.disposables.CompositeDisposable; - -final class TimestampLongPressClickableSpan extends LongPressClickableSpan { - - @NonNull - private final Context context; - @NonNull - private final String descriptionText; - @NonNull - private final CompositeDisposable disposables; - @NonNull - private final StreamingService relatedInfoService; - @NonNull - private final String relatedStreamUrl; - @NonNull - private final TimestampExtractor.TimestampMatchDTO timestampMatchDTO; - - TimestampLongPressClickableSpan( - @NonNull final Context context, - @NonNull final String descriptionText, - @NonNull final CompositeDisposable disposables, - @NonNull final StreamingService relatedInfoService, - @NonNull final String relatedStreamUrl, - @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { - this.context = context; - this.descriptionText = descriptionText; - this.disposables = disposables; - this.relatedInfoService = relatedInfoService; - this.relatedStreamUrl = relatedStreamUrl; - this.timestampMatchDTO = timestampMatchDTO; - } - - @Override - public void onClick(@NonNull final View view) { - playOnPopup(context, relatedStreamUrl, relatedInfoService, - timestampMatchDTO.seconds()); - } - - @Override - public void onLongClick(@NonNull final View view) { - ShareUtils.copyToClipboard(context, getTimestampTextToCopy( - relatedInfoService, relatedStreamUrl, descriptionText, timestampMatchDTO)); - } - - @NonNull - private static String getTimestampTextToCopy( - @NonNull final StreamingService relatedInfoService, - @NonNull final String relatedStreamUrl, - @NonNull final String descriptionText, - @NonNull final TimestampExtractor.TimestampMatchDTO timestampMatchDTO) { - // TODO: use extractor methods to get timestamps when this feature will be implemented in it - if (relatedInfoService == ServiceList.YouTube) { - return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds(); - } else if (relatedInfoService == ServiceList.SoundCloud - || relatedInfoService == ServiceList.MediaCCC) { - return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds(); - } else if (relatedInfoService == ServiceList.PeerTube) { - return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds(); - } - - // Return timestamp text for other services - return descriptionText.subSequence(timestampMatchDTO.timestampStart(), - timestampMatchDTO.timestampEnd()).toString(); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.kt b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.kt new file mode 100644 index 000000000..a76c5c31a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.kt @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util.text + +import android.content.Context +import android.view.View +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.schabi.newpipe.extractor.ServiceList +import org.schabi.newpipe.extractor.StreamingService +import org.schabi.newpipe.util.external_communication.ShareUtils +import org.schabi.newpipe.util.text.TimestampExtractor.TimestampMatchDTO + +class TimestampLongPressClickableSpan( + private val context: Context, + private val descriptionText: String, + private val disposables: CompositeDisposable, + private val relatedInfoService: StreamingService, + private val relatedStreamUrl: String, + private val timestampMatchDTO: TimestampMatchDTO +) : LongPressClickableSpan() { + override fun onClick(view: View) { + InternalUrlsHandler.playOnPopup( + context, + relatedStreamUrl, + relatedInfoService, + timestampMatchDTO.seconds() + ) + } + + override fun onLongClick(view: View) { + ShareUtils.copyToClipboard( + context, + getTimestampTextToCopy( + relatedInfoService, + relatedStreamUrl, + descriptionText, + timestampMatchDTO + ) + ) + } + + companion object { + private fun getTimestampTextToCopy( + relatedInfoService: StreamingService, + relatedStreamUrl: String, + descriptionText: String, + timestampMatchDTO: TimestampMatchDTO + ): String { + // TODO: use extractor methods to get timestamps when this feature will be implemented in it + when (relatedInfoService) { + ServiceList.YouTube -> + return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds() + ServiceList.SoundCloud, ServiceList.MediaCCC -> + return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds() + ServiceList.PeerTube -> + return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds() + } + + // Return timestamp text for other services + return descriptionText.substring( + timestampMatchDTO.timestampStart(), + timestampMatchDTO.timestampEnd() + ) + } + } +} From 84c646713d232a6b787ebc4d19fdfe7afa05ddbb Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Fri, 26 Dec 2025 16:20:26 +0200 Subject: [PATCH 15/30] Convert newpipe/settings/preferencesearch/PreferenceSearchItem.java to kotlin --- .../PreferenceSearchItem.java | 102 ------------------ .../preferencesearch/PreferenceSearchItem.kt | 40 +++++++ 2 files changed, 40 insertions(+), 102 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.kt diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java deleted file mode 100644 index 33856326c..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.java +++ /dev/null @@ -1,102 +0,0 @@ -package org.schabi.newpipe.settings.preferencesearch; - -import androidx.annotation.NonNull; -import androidx.annotation.XmlRes; - -import java.util.List; -import java.util.Objects; - -/** - * Represents a preference-item inside the search. - */ -public class PreferenceSearchItem { - /** - * Key of the setting/preference. E.g. used inside {@link android.content.SharedPreferences}. - */ - @NonNull - private final String key; - /** - * Title of the setting, e.g. 'Default resolution' or 'Show higher resolutions'. - */ - @NonNull - private final String title; - /** - * Summary of the setting, e.g. '480p' or 'Only some devices can play 2k/4k'. - */ - @NonNull - private final String summary; - /** - * Possible entries of the setting, e.g. 480p,720p,... - */ - @NonNull - private final String entries; - /** - * Breadcrumbs - a hint where the setting is located e.g. 'Video and Audio > Player' - */ - @NonNull - private final String breadcrumbs; - /** - * The xml-resource where this item was found/built from. - */ - @XmlRes - private final int searchIndexItemResId; - - public PreferenceSearchItem( - @NonNull final String key, - @NonNull final String title, - @NonNull final String summary, - @NonNull final String entries, - @NonNull final String breadcrumbs, - @XmlRes final int searchIndexItemResId - ) { - this.key = Objects.requireNonNull(key); - this.title = Objects.requireNonNull(title); - this.summary = Objects.requireNonNull(summary); - this.entries = Objects.requireNonNull(entries); - this.breadcrumbs = Objects.requireNonNull(breadcrumbs); - this.searchIndexItemResId = searchIndexItemResId; - } - - @NonNull - public String getKey() { - return key; - } - - @NonNull - public String getTitle() { - return title; - } - - @NonNull - public String getSummary() { - return summary; - } - - @NonNull - public String getEntries() { - return entries; - } - - @NonNull - public String getBreadcrumbs() { - return breadcrumbs; - } - - public int getSearchIndexItemResId() { - return searchIndexItemResId; - } - - boolean hasData() { - return !key.isEmpty() && !title.isEmpty(); - } - - public List getAllRelevantSearchFields() { - return List.of(getTitle(), getSummary(), getEntries(), getBreadcrumbs()); - } - - @NonNull - @Override - public String toString() { - return "PreferenceItem: " + title + " " + summary + " " + key; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.kt b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.kt new file mode 100644 index 000000000..750e40eae --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchItem.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2022-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings.preferencesearch + +import androidx.annotation.XmlRes + +/** + * Represents a preference-item inside the search. + * + * @param key Key of the setting/preference. E.g. used inside [android.content.SharedPreferences]. + * @param title Title of the setting, e.g. 'Default resolution' or 'Show higher resolutions'. + * @param summary Summary of the setting, e.g. '480p' or 'Only some devices can play 2k/4k'. + * @param entries Possible entries of the setting, e.g. 480p,720p,... + * @param breadcrumbs Breadcrumbs - a hint where the setting is located e.g. 'Video and Audio > Player' + * @param searchIndexItemResId The xml-resource where this item was found/built from. + */ + +data class PreferenceSearchItem( + val key: String, + val title: String, + val summary: String, + val entries: String, + val breadcrumbs: String, + @XmlRes val searchIndexItemResId: Int +) { + fun hasData(): Boolean { + return !key.isEmpty() && !title.isEmpty() + } + + fun getAllRelevantSearchFields(): MutableList { + return mutableListOf(title, summary, entries, breadcrumbs) + } + + override fun toString(): String { + return "PreferenceItem: $title $summary $key" + } +} From f1b111212de9a12d91a26dc055a38f9f8a7a9624 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Fri, 26 Dec 2025 01:57:15 +0200 Subject: [PATCH 16/30] Convert newpipe/util/FilenameUtils.java to kotlin Co-authored-by: Aayush Gupta --- .../schabi/newpipe/util/FilenameUtils.java | 70 ------------------- .../org/schabi/newpipe/util/FilenameUtils.kt | 64 +++++++++++++++++ 2 files changed, 64 insertions(+), 70 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java create mode 100644 app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java deleted file mode 100644 index bc15f3f02..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.preference.PreferenceManager; - -import org.schabi.newpipe.R; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class FilenameUtils { - private static final String CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+"; - private static final String CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+"; - - private FilenameUtils() { } - - /** - * #143 #44 #42 #22: make sure that the filename does not contain illegal chars. - * - * @param context the context to retrieve strings and preferences from - * @param title the title to create a filename from - * @return the filename - */ - public static String createFilename(final Context context, final String title) { - final SharedPreferences sharedPreferences = PreferenceManager - .getDefaultSharedPreferences(context); - - final String charsetLd = context.getString(R.string.charset_letters_and_digits_value); - final String charsetMs = context.getString(R.string.charset_most_special_value); - final String defaultCharset = context.getString(R.string.default_file_charset_value); - - final String replacementChar = sharedPreferences.getString( - context.getString(R.string.settings_file_replacement_character_key), "_"); - String selectedCharset = sharedPreferences.getString( - context.getString(R.string.settings_file_charset_key), null); - - final String charset; - - if (selectedCharset == null || selectedCharset.isEmpty()) { - selectedCharset = defaultCharset; - } - - if (selectedCharset.equals(charsetLd)) { - charset = CHARSET_ONLY_LETTERS_AND_DIGITS; - } else if (selectedCharset.equals(charsetMs)) { - charset = CHARSET_MOST_SPECIAL; - } else { - charset = selectedCharset; // Is the user using a custom charset? - } - - final Pattern pattern = Pattern.compile(charset); - - return createFilename(title, pattern, Matcher.quoteReplacement(replacementChar)); - } - - /** - * Create a valid filename. - * - * @param title the title to create a filename from - * @param invalidCharacters patter matching invalid characters - * @param replacementChar the replacement - * @return the filename - */ - private static String createFilename(final String title, final Pattern invalidCharacters, - final String replacementChar) { - return title.replaceAll(invalidCharacters.pattern(), replacementChar); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt new file mode 100644 index 000000000..bfa50beef --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.kt @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util + +import android.content.Context +import androidx.preference.PreferenceManager +import org.schabi.newpipe.R +import org.schabi.newpipe.ktx.getStringSafe +import java.util.regex.Matcher + +object FilenameUtils { + private const val CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+" + private const val CHARSET_ONLY_LETTERS_AND_DIGITS = "[^\\w\\d]+" + + /** + * #143 #44 #42 #22: make sure that the filename does not contain illegal chars. + * + * @param context the context to retrieve strings and preferences from + * @param title the title to create a filename from + * @return the filename + */ + @JvmStatic + fun createFilename(context: Context, title: String): String { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + val charsetLd = context.getString(R.string.charset_letters_and_digits_value) + val charsetMs = context.getString(R.string.charset_most_special_value) + val defaultCharset = context.getString(R.string.default_file_charset_value) + + val replacementChar = sharedPreferences.getStringSafe( + context.getString(R.string.settings_file_replacement_character_key), "_" + ) + val selectedCharset = sharedPreferences.getStringSafe( + context.getString(R.string.settings_file_charset_key), "" + ).ifEmpty { defaultCharset } + + val charset = when (selectedCharset) { + charsetLd -> CHARSET_ONLY_LETTERS_AND_DIGITS + charsetMs -> CHARSET_MOST_SPECIAL + else -> selectedCharset // Is the user using a custom charset? + } + + return createFilename(title, charset, Matcher.quoteReplacement(replacementChar)) + } + + /** + * Create a valid filename. + * + * @param title the title to create a filename from + * @param invalidCharacters patter matching invalid characters + * @param replacementChar the replacement + * @return the filename + */ + private fun createFilename( + title: String, + invalidCharacters: String, + replacementChar: String + ): String { + return title.replace(invalidCharacters.toRegex(), replacementChar) + } +} From cd4cb40e6d9eff816f2283e2a74e46495bcce87c Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Thu, 25 Dec 2025 17:36:54 +0200 Subject: [PATCH 17/30] Convert newpipe/error/UserAction.java to kotlin --- .../error/{UserAction.java => UserAction.kt} | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) rename app/src/main/java/org/schabi/newpipe/error/{UserAction.java => UserAction.kt} (86%) diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.kt similarity index 86% rename from app/src/main/java/org/schabi/newpipe/error/UserAction.java rename to app/src/main/java/org/schabi/newpipe/error/UserAction.kt index d3af9d32e..2d2358310 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.kt @@ -1,9 +1,14 @@ -package org.schabi.newpipe.error; +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.error /** * The user actions that can cause an error. */ -public enum UserAction { +enum class UserAction(val message: String) { USER_REPORT("user report"), UI_ERROR("ui error"), DATABASE_IMPORT_EXPORT("database import or export"), @@ -36,14 +41,4 @@ public enum UserAction { GETTING_MAIN_SCREEN_TAB("getting main screen tab"), PLAY_ON_POPUP("play on popup"), SUBSCRIPTIONS("loading subscriptions"); - - private final String message; - - UserAction(final String message) { - this.message = message; - } - - public String getMessage() { - return message; - } } From 8379aa0a9d278c1eafbcc04c842ac577b4dffebe Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Wed, 31 Dec 2025 14:28:11 +0200 Subject: [PATCH 18/30] Refactor ExportPlaylist to use more idiomatic kotlin code --- .../newpipe/local/playlist/ExportPlaylist.kt | 27 +++++++++---------- .../local/playlist/ExportPlaylistTest.kt | 18 ++++++------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt b/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt index 0d4dcbfd0..8eb3ab3ae 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/ExportPlaylist.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.local.playlist import android.content.Context @@ -21,11 +26,7 @@ fun export( } } -fun exportWithTitles( - playlist: List, - context: Context -): String { - +private fun exportWithTitles(playlist: List, context: Context): String { return playlist.asSequence() .map { it.streamEntity } .map { entity -> @@ -38,18 +39,14 @@ fun exportWithTitles( .joinToString(separator = "\n") } -fun exportJustUrls(playlist: List): String { - - return playlist.asSequence() - .map { it.streamEntity.url } - .joinToString(separator = "\n") +private fun exportJustUrls(playlist: List): String { + return playlist.joinToString(separator = "\n") { it.streamEntity.url } } -fun exportAsYoutubeTempPlaylist(playlist: List): String { +private fun exportAsYoutubeTempPlaylist(playlist: List): String { val videoIDs = playlist.asReversed().asSequence() - .map { it.streamEntity.url } - .mapNotNull(::getYouTubeId) + .mapNotNull { getYouTubeId(it.streamEntity.url) } .take(50) // YouTube limitation: temp playlists can't have more than 50 items .toList() .asReversed() @@ -58,7 +55,7 @@ fun exportAsYoutubeTempPlaylist(playlist: List): String { return "https://www.youtube.com/watch_videos?video_ids=$videoIDs" } -val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance() +private val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFactory.getInstance() /** * Gets the video id from a YouTube URL. @@ -66,7 +63,7 @@ val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHandlerFacto * @param url YouTube URL * @return the video id */ -fun getYouTubeId(url: String): String? { +private fun getYouTubeId(url: String): String? { return try { linkHandler.getId(url) } catch (e: ParsingException) { null } } diff --git a/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt b/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt index d9be2271e..de90061a4 100644 --- a/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt +++ b/app/src/test/java/org/schabi/newpipe/local/playlist/ExportPlaylistTest.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.local.playlist import android.content.Context @@ -9,7 +14,6 @@ import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.local.playlist.PlayListShareMode.JUST_URLS import org.schabi.newpipe.local.playlist.PlayListShareMode.YOUTUBE_TEMP_PLAYLIST -import java.util.stream.Stream class ExportPlaylistTest { @@ -41,9 +45,7 @@ class ExportPlaylistTest { */ val playlist = asPlaylist( - (10..70) - .map { id -> "https://www.youtube.com/watch?v=aaaaaaaaa$id" } // YouTube video IDs are 11 characters long - .stream() + (10..70).map { id -> "https://www.youtube.com/watch?v=aaaaaaaaa$id" } // YouTube video IDs are 11 characters long ) val url = export(YOUTUBE_TEMP_PLAYLIST, playlist, mock(Context::class.java)) @@ -78,13 +80,11 @@ class ExportPlaylistTest { } fun asPlaylist(vararg urls: String): List { - return asPlaylist(Stream.of(*urls)) + return asPlaylist(listOf(*urls)) } -fun asPlaylist(urls: Stream): List { - return urls - .map { url: String -> newPlaylistStreamEntry(url) } - .toList() +fun asPlaylist(urls: List): List { + return urls.map { newPlaylistStreamEntry(it) } } fun newPlaylistStreamEntry(url: String): PlaylistStreamEntry { From 7d1d88fb8769f20b552108c0384bc86a5d44a080 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Thu, 1 Jan 2026 15:08:21 +0200 Subject: [PATCH 19/30] Convert newpipe/local/playlist/PlayListShareMode to kotlin --- .../newpipe/local/playlist/PlayListShareMode.java | 8 -------- .../newpipe/local/playlist/PlayListShareMode.kt | 12 ++++++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.java create mode 100644 app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.kt diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.java b/app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.java deleted file mode 100644 index f0433aba8..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.schabi.newpipe.local.playlist; - -public enum PlayListShareMode { - - JUST_URLS, - WITH_TITLES, - YOUTUBE_TEMP_PLAYLIST -} diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.kt b/app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.kt new file mode 100644 index 000000000..5595ce7fa --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/PlayListShareMode.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2025 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.local.playlist + +enum class PlayListShareMode { + JUST_URLS, + WITH_TITLES, + YOUTUBE_TEMP_PLAYLIST +} From ab3314eb1c8644b67546a65c9affb76295e91e51 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Thu, 1 Jan 2026 15:21:14 +0200 Subject: [PATCH 20/30] Convert newpipe/info_list/ItemViewMode to kotlin --- .../info_list/{ItemViewMode.java => ItemViewMode.kt} | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) rename app/src/main/java/org/schabi/newpipe/info_list/{ItemViewMode.java => ItemViewMode.kt} (65%) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.java b/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.kt similarity index 65% rename from app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.java rename to app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.kt index 447c540a0..703191bb9 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/ItemViewMode.kt @@ -1,9 +1,14 @@ -package org.schabi.newpipe.info_list; +/* + * SPDX-FileCopyrightText: 2023-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.info_list /** * Item view mode for streams & playlist listing screens. */ -public enum ItemViewMode { +enum class ItemViewMode { /** * Default mode. */ From d9682f5e0abb17590bdd5cea483cad332232940e Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Thu, 1 Jan 2026 15:23:21 +0200 Subject: [PATCH 21/30] Convert newpipe/player/PlayerType to kotlin --- .../java/org/schabi/newpipe/player/PlayerType.java | 7 ------- .../java/org/schabi/newpipe/player/PlayerType.kt | 12 ++++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/player/PlayerType.java create mode 100644 app/src/main/java/org/schabi/newpipe/player/PlayerType.kt diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java deleted file mode 100644 index f74389d79..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.player; - -public enum PlayerType { - MAIN, - AUDIO, - POPUP; -} diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerType.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerType.kt new file mode 100644 index 000000000..42b2e1131 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerType.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.player + +enum class PlayerType { + MAIN, + AUDIO, + POPUP +} From 83596ca90789a1c89992def0265a90f0c313953b Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Thu, 1 Jan 2026 15:27:32 +0200 Subject: [PATCH 22/30] Convert newpipe/settings/preferencesearch/PreferenceSearchResultListener to kotlin --- .../PreferenceSearchResultListener.java | 7 ------- .../preferencesearch/PreferenceSearchResultListener.kt | 10 ++++++++++ 2 files changed, 10 insertions(+), 7 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java create mode 100644 app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.kt diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java deleted file mode 100644 index 1f0636454..000000000 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.settings.preferencesearch; - -import androidx.annotation.NonNull; - -public interface PreferenceSearchResultListener { - void onSearchResultClicked(@NonNull PreferenceSearchItem result); -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.kt b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.kt new file mode 100644 index 000000000..7b7b7884a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchResultListener.kt @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2022-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.settings.preferencesearch + +interface PreferenceSearchResultListener { + fun onSearchResultClicked(result: PreferenceSearchItem) +} From 3ffcf11a3a18bafc5ae58fc950959e10c715b9c1 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Thu, 1 Jan 2026 21:06:04 +0200 Subject: [PATCH 23/30] Merge inheritors of newpipe/player/playqueue/PlayQueueEvent and convert it to kotlin --- .../player/playback/MediaSourceManager.java | 8 +-- .../newpipe/player/playqueue/PlayQueue.java | 17 +++--- .../player/playqueue/PlayQueueAdapter.java | 11 ++-- .../player/playqueue/PlayQueueEvent.kt | 55 +++++++++++++++++++ .../player/playqueue/events/AppendEvent.java | 18 ------ .../player/playqueue/events/ErrorEvent.java | 24 -------- .../player/playqueue/events/InitEvent.java | 8 --- .../player/playqueue/events/MoveEvent.java | 24 -------- .../playqueue/events/PlayQueueEvent.java | 7 --- .../playqueue/events/PlayQueueEventType.java | 27 --------- .../playqueue/events/RecoveryEvent.java | 24 -------- .../player/playqueue/events/RemoveEvent.java | 24 -------- .../player/playqueue/events/ReorderEvent.java | 24 -------- .../player/playqueue/events/SelectEvent.java | 24 -------- 14 files changed, 72 insertions(+), 223 deletions(-) create mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueEvent.kt delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/InitEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEventType.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java delete mode 100644 app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java index 88d7145bc..5cffc7f62 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java +++ b/app/src/main/java/org/schabi/newpipe/player/playback/MediaSourceManager.java @@ -17,10 +17,10 @@ import org.schabi.newpipe.player.mediasource.ManagedMediaSource; import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; -import org.schabi.newpipe.player.playqueue.events.MoveEvent; -import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent; -import org.schabi.newpipe.player.playqueue.events.RemoveEvent; -import org.schabi.newpipe.player.playqueue.events.ReorderEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent; import java.util.Collection; import java.util.Collections; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index 97196805d..d430faf0f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -4,15 +4,14 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.player.playqueue.events.AppendEvent; -import org.schabi.newpipe.player.playqueue.events.ErrorEvent; -import org.schabi.newpipe.player.playqueue.events.InitEvent; -import org.schabi.newpipe.player.playqueue.events.MoveEvent; -import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent; -import org.schabi.newpipe.player.playqueue.events.RecoveryEvent; -import org.schabi.newpipe.player.playqueue.events.RemoveEvent; -import org.schabi.newpipe.player.playqueue.events.ReorderEvent; -import org.schabi.newpipe.player.playqueue.events.SelectEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.InitEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RecoveryEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ReorderEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent; import java.io.Serializable; import java.util.ArrayList; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java index dd95fb4d5..2e19672e5 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueAdapter.java @@ -10,12 +10,11 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import org.schabi.newpipe.R; -import org.schabi.newpipe.player.playqueue.events.AppendEvent; -import org.schabi.newpipe.player.playqueue.events.ErrorEvent; -import org.schabi.newpipe.player.playqueue.events.MoveEvent; -import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent; -import org.schabi.newpipe.player.playqueue.events.RemoveEvent; -import org.schabi.newpipe.player.playqueue.events.SelectEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.AppendEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.ErrorEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.MoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.RemoveEvent; +import org.schabi.newpipe.player.playqueue.PlayQueueEvent.SelectEvent; import org.schabi.newpipe.util.FallbackViewHolder; import java.util.List; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueEvent.kt b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueEvent.kt new file mode 100644 index 000000000..f1952ef95 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueEvent.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2017-2026 NewPipe contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.player.playqueue + +import java.io.Serializable + +sealed interface PlayQueueEvent : Serializable { + fun type(): Type + + class InitEvent : PlayQueueEvent { + override fun type() = Type.INIT + } + + // sent when the index is changed + class SelectEvent(val oldIndex: Int, val newIndex: Int) : PlayQueueEvent { + override fun type() = Type.SELECT + } + + // sent when more streams are added to the play queue + class AppendEvent(val amount: Int) : PlayQueueEvent { + override fun type() = Type.APPEND + } + + // sent when a pending stream is removed from the play queue + class RemoveEvent(val removeIndex: Int, val queueIndex: Int) : PlayQueueEvent { + override fun type() = Type.REMOVE + } + + // sent when two streams swap place in the play queue + class MoveEvent(val fromIndex: Int, val toIndex: Int) : PlayQueueEvent { + override fun type() = Type.MOVE + } + + // sent when queue is shuffled + class ReorderEvent(val fromSelectedIndex: Int, val toSelectedIndex: Int) : PlayQueueEvent { + override fun type() = Type.REORDER + } + + // sent when recovery record is set on a stream + class RecoveryEvent(val index: Int, val position: Long) : PlayQueueEvent { + override fun type() = Type.RECOVERY + } + + // sent when the item at index has caused an exception + class ErrorEvent(val errorIndex: Int, val queueIndex: Int) : PlayQueueEvent { + override fun type() = Type.ERROR + } + + // It is necessary only for use in java code. Remove it and use kotlin pattern + // matching when all users of this enum are converted to kotlin + enum class Type { INIT, SELECT, APPEND, REMOVE, MOVE, REORDER, RECOVERY, ERROR } +} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java deleted file mode 100644 index cc922dbb1..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/AppendEvent.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class AppendEvent implements PlayQueueEvent { - private final int amount; - - public AppendEvent(final int amount) { - this.amount = amount; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.APPEND; - } - - public int getAmount() { - return amount; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java deleted file mode 100644 index 7b7e39212..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ErrorEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class ErrorEvent implements PlayQueueEvent { - private final int errorIndex; - private final int queueIndex; - - public ErrorEvent(final int errorIndex, final int queueIndex) { - this.errorIndex = errorIndex; - this.queueIndex = queueIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.ERROR; - } - - public int getErrorIndex() { - return errorIndex; - } - - public int getQueueIndex() { - return queueIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/InitEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/InitEvent.java deleted file mode 100644 index 559975b35..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/InitEvent.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class InitEvent implements PlayQueueEvent { - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.INIT; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java deleted file mode 100644 index 55d198923..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/MoveEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class MoveEvent implements PlayQueueEvent { - private final int fromIndex; - private final int toIndex; - - public MoveEvent(final int oldIndex, final int newIndex) { - this.fromIndex = oldIndex; - this.toIndex = newIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.MOVE; - } - - public int getFromIndex() { - return fromIndex; - } - - public int getToIndex() { - return toIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEvent.java deleted file mode 100644 index 431053e7b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEvent.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -import java.io.Serializable; - -public interface PlayQueueEvent extends Serializable { - PlayQueueEventType type(); -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEventType.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEventType.java deleted file mode 100644 index 1cc710c7b..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/PlayQueueEventType.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public enum PlayQueueEventType { - INIT, - - // sent when the index is changed - SELECT, - - // sent when more streams are added to the play queue - APPEND, - - // sent when a pending stream is removed from the play queue - REMOVE, - - // sent when two streams swap place in the play queue - MOVE, - - // sent when queue is shuffled - REORDER, - - // sent when recovery record is set on a stream - RECOVERY, - - // sent when the item at index has caused an exception - ERROR -} - diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java deleted file mode 100644 index 6f21b36cd..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RecoveryEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class RecoveryEvent implements PlayQueueEvent { - private final int index; - private final long position; - - public RecoveryEvent(final int index, final long position) { - this.index = index; - this.position = position; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.RECOVERY; - } - - public int getIndex() { - return index; - } - - public long getPosition() { - return position; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java deleted file mode 100644 index a5872906d..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/RemoveEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class RemoveEvent implements PlayQueueEvent { - private final int removeIndex; - private final int queueIndex; - - public RemoveEvent(final int removeIndex, final int queueIndex) { - this.removeIndex = removeIndex; - this.queueIndex = queueIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.REMOVE; - } - - public int getQueueIndex() { - return queueIndex; - } - - public int getRemoveIndex() { - return removeIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java deleted file mode 100644 index 4f4f14756..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/ReorderEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class ReorderEvent implements PlayQueueEvent { - private final int fromSelectedIndex; - private final int toSelectedIndex; - - public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) { - this.fromSelectedIndex = fromSelectedIndex; - this.toSelectedIndex = toSelectedIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.REORDER; - } - - public int getFromSelectedIndex() { - return fromSelectedIndex; - } - - public int getToSelectedIndex() { - return toSelectedIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java deleted file mode 100644 index 95e344211..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/events/SelectEvent.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.schabi.newpipe.player.playqueue.events; - -public class SelectEvent implements PlayQueueEvent { - private final int oldIndex; - private final int newIndex; - - public SelectEvent(final int oldIndex, final int newIndex) { - this.oldIndex = oldIndex; - this.newIndex = newIndex; - } - - @Override - public PlayQueueEventType type() { - return PlayQueueEventType.SELECT; - } - - public int getOldIndex() { - return oldIndex; - } - - public int getNewIndex() { - return newIndex; - } -} From 0747b3a0a59857fcce69eade2266d9c83271c58e Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Fri, 2 Jan 2026 12:25:25 +0200 Subject: [PATCH 24/30] Use "factory" method for creating db migrations --- .../org/schabi/newpipe/database/Migrations.kt | 606 +++++++++--------- 1 file changed, 295 insertions(+), 311 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt index 8988708e6..6566f7e6a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt @@ -34,335 +34,319 @@ object Migrations { private val TAG = Migrations::class.java.getName() private val isDebug = MainActivity.DEBUG - val MIGRATION_1_2 = object : Migration(DB_VER_1, DB_VER_2) { - override fun migrate(db: SupportSQLiteDatabase) { - if (isDebug) { - Log.d(TAG, "Start migrating database") - } + val MIGRATION_1_2 = Migration(DB_VER_1, DB_VER_2) { db -> + if (isDebug) { + Log.d(TAG, "Start migrating database") + } - /* - * Unfortunately these queries must be hardcoded due to the possibility of - * schema and names changing at a later date, thus invalidating the older migration - * scripts if they are not hardcoded. - * */ + /* + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if they are not hardcoded. + * */ - // Not much we can do about this, since room doesn't create tables before migration. - // It's either this or blasting the entire database anew. + // Not much we can do about this, since room doesn't create tables before migration. + // It's either this or blasting the entire database anew. + db.execSQL( + "CREATE INDEX `index_search_history_search` " + + "ON `search_history` (`search`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `streams` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, " + + "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, " + + "`thumbnail_url` TEXT)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_streams_service_id_url` " + + "ON `streams` (`service_id`, `url`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_history` " + + "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, " + + "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), " + + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE )" + ) + db.execSQL( + "CREATE INDEX `index_stream_history_stream_id` " + + "ON `stream_history` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_state` " + + "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, " + + "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) " + + "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT, `thumbnail_url` TEXT)" + ) + db.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlist_stream_join` " + + "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, " + + "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), " + + "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + db.execSQL( + "CREATE UNIQUE INDEX " + + "`index_playlist_stream_join_playlist_id_join_index` " + + "ON `playlist_stream_join` (`playlist_id`, `join_index`)" + ) + db.execSQL( + "CREATE INDEX `index_playlist_stream_join_stream_id` " + + "ON `playlist_stream_join` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `remote_playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)" + ) + db.execSQL( + "CREATE INDEX `index_remote_playlists_name` " + + "ON `remote_playlists` (`name`)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)" + ) + + // Populate streams table with existing entries in watch history + // Latest data first, thus ignoring older entries with the same indices + db.execSQL( + "INSERT OR IGNORE INTO streams (service_id, url, title, " + + "stream_type, duration, uploader, thumbnail_url) " + + + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + + "uploader, thumbnail_url " + + + "FROM watch_history " + + "ORDER BY creation_date DESC" + ) + + // Once the streams have PKs, join them with the normalized history table + // and populate it with the remaining data from watch history + db.execSQL( + "INSERT INTO stream_history (stream_id, access_date, repeat_count)" + + "SELECT uid, creation_date, 1 " + + "FROM watch_history INNER JOIN streams " + + "ON watch_history.service_id == streams.service_id " + + "AND watch_history.url == streams.url " + + "ORDER BY creation_date DESC" + ) + + db.execSQL("DROP TABLE IF EXISTS watch_history") + + if (isDebug) { + Log.d(TAG, "Stop migrating database") + } + } + + val MIGRATION_2_3 = Migration(DB_VER_2, DB_VER_3) { db -> + // Add NOT NULLs and new fields + db.execSQL( + "CREATE TABLE IF NOT EXISTS streams_new " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, " + + "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, " + + "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, " + + "textual_upload_date TEXT, upload_date INTEGER, " + + "is_upload_date_approximation INTEGER)" + ) + + db.execSQL( + "INSERT INTO streams_new (uid, service_id, url, title, stream_type, " + + "duration, uploader, thumbnail_url, view_count, textual_upload_date, " + + "upload_date, is_upload_date_approximation) " + + + "SELECT uid, service_id, url, ifnull(title, ''), " + + "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " + + "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " + + + "FROM streams WHERE url IS NOT NULL" + ) + + db.execSQL("DROP TABLE streams") + db.execSQL("ALTER TABLE streams_new RENAME TO streams") + db.execSQL( + "CREATE UNIQUE INDEX index_streams_service_id_url " + + "ON streams (service_id, url)" + ) + + // Tables for feed feature + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed " + + "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + + "PRIMARY KEY(stream_id, subscription_id), " + + "FOREIGN KEY(stream_id) REFERENCES streams(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + db.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_group " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " + + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)" + ) + db.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_group_subscription_join " + + "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + + "PRIMARY KEY(group_id, subscription_id), " + + "FOREIGN KEY(group_id) REFERENCES feed_group(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + db.execSQL( + "CREATE INDEX index_feed_group_subscription_join_subscription_id " + + "ON feed_group_subscription_join (subscription_id)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_last_updated " + + "(subscription_id INTEGER NOT NULL, last_updated INTEGER, " + + "PRIMARY KEY(subscription_id), " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + } + + val MIGRATION_3_4 = Migration(DB_VER_3, DB_VER_4) { db -> + db.execSQL("ALTER TABLE streams ADD COLUMN uploader_url TEXT") + } + + val MIGRATION_4_5 = Migration(DB_VER_4, DB_VER_5) { db -> + db.execSQL( + "ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + + val MIGRATION_5_6 = Migration(DB_VER_5, DB_VER_6) { db -> + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + + val MIGRATION_6_7 = Migration(DB_VER_6, DB_VER_7) { db -> + // Create a new column thumbnail_stream_id + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " + + "INTEGER NOT NULL DEFAULT -1" + ) + + // Migrate the thumbnail_url to the thumbnail_stream_id + db.execSQL( + "UPDATE playlists SET thumbnail_stream_id = (" + + " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" + + " FROM (" + + " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" + + " FROM playlists p" + + " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" + + " LEFT JOIN streams s ON s.uid = ps.stream_id" + + " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" + + " WHERE playlist_uid = playlists.uid)" + ) + + // Remove the thumbnail_url field in the playlist table + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists_new`" + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "name TEXT, " + + "is_thumbnail_permanent INTEGER NOT NULL, " + + "thumbnail_stream_id INTEGER NOT NULL)" + ) + + db.execSQL( + "INSERT INTO playlists_new" + + " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " + + " FROM playlists" + ) + + db.execSQL("DROP TABLE playlists") + db.execSQL("ALTER TABLE playlists_new RENAME TO playlists") + db.execSQL( + "CREATE INDEX IF NOT EXISTS " + + "`index_playlists_name` ON `playlists` (`name`)" + ) + } + + val MIGRATION_7_8 = Migration(DB_VER_7, DB_VER_8) { db -> + db.execSQL( + "DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " + + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)" + ) + db.execSQL("UPDATE search_history SET search = trim(search)") + } + + val MIGRATION_8_9 = Migration(DB_VER_8, DB_VER_9) { db -> + try { + db.beginTransaction() + + // Update playlists. + // Create a temp table to initialize display_index. db.execSQL( - "CREATE INDEX `index_search_history_search` " + - "ON `search_history` (`search`)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `streams` " + + "CREATE TABLE `playlists_tmp` " + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, " + - "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, " + - "`thumbnail_url` TEXT)" + "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " + + "`thumbnail_stream_id` INTEGER NOT NULL, " + + "`display_index` INTEGER NOT NULL)" ) db.execSQL( - "CREATE UNIQUE INDEX `index_streams_service_id_url` " + - "ON `streams` (`service_id`, `url`)" + "INSERT INTO `playlists_tmp` " + + "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "`display_index`) " + + "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "-1 " + + "FROM `playlists`" ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `playlists`") + db.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`") + + // Update remote_playlists. + // Create a temp table to initialize display_index. db.execSQL( - "CREATE TABLE IF NOT EXISTS `stream_history` " + - "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, " + - "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), " + - "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + - "ON UPDATE CASCADE ON DELETE CASCADE )" - ) - db.execSQL( - "CREATE INDEX `index_stream_history_stream_id` " + - "ON `stream_history` (`stream_id`)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `stream_state` " + - "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, " + - "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) " + - "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `playlists` " + - "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`name` TEXT, `thumbnail_url` TEXT)" - ) - db.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)") - db.execSQL( - "CREATE TABLE IF NOT EXISTS `playlist_stream_join` " + - "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, " + - "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), " + - "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) " + - "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + - "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + - "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" - ) - db.execSQL( - "CREATE UNIQUE INDEX " + - "`index_playlist_stream_join_playlist_id_join_index` " + - "ON `playlist_stream_join` (`playlist_id`, `join_index`)" - ) - db.execSQL( - "CREATE INDEX `index_playlist_stream_join_stream_id` " + - "ON `playlist_stream_join` (`stream_id`)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS `remote_playlists` " + + "CREATE TABLE `remote_playlists_tmp` " + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + - "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)" + "`thumbnail_url` TEXT, `uploader` TEXT, " + + "`display_index` INTEGER NOT NULL," + + "`stream_count` INTEGER)" ) db.execSQL( - "CREATE INDEX `index_remote_playlists_name` " + - "ON `remote_playlists` (`name`)" + "INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " + + "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " + + "`stream_count`)" + + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + + "-1, `stream_count` FROM `remote_playlists`" ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `remote_playlists`") + db.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`") + + // Create index on the new table. db.execSQL( "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + "ON `remote_playlists` (`service_id`, `url`)" ) - // Populate streams table with existing entries in watch history - // Latest data first, thus ignoring older entries with the same indices - db.execSQL( - "INSERT OR IGNORE INTO streams (service_id, url, title, " + - "stream_type, duration, uploader, thumbnail_url) " + - - "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + - "uploader, thumbnail_url " + - - "FROM watch_history " + - "ORDER BY creation_date DESC" - ) - - // Once the streams have PKs, join them with the normalized history table - // and populate it with the remaining data from watch history - db.execSQL( - "INSERT INTO stream_history (stream_id, access_date, repeat_count)" + - "SELECT uid, creation_date, 1 " + - "FROM watch_history INNER JOIN streams " + - "ON watch_history.service_id == streams.service_id " + - "AND watch_history.url == streams.url " + - "ORDER BY creation_date DESC" - ) - - db.execSQL("DROP TABLE IF EXISTS watch_history") - - if (isDebug) { - Log.d(TAG, "Stop migrating database") - } - } - } - - val MIGRATION_2_3 = object : Migration(DB_VER_2, DB_VER_3) { - override fun migrate(db: SupportSQLiteDatabase) { - // Add NOT NULLs and new fields - db.execSQL( - "CREATE TABLE IF NOT EXISTS streams_new " + - "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, " + - "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, " + - "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, " + - "textual_upload_date TEXT, upload_date INTEGER, " + - "is_upload_date_approximation INTEGER)" - ) - - db.execSQL( - "INSERT INTO streams_new (uid, service_id, url, title, stream_type, " + - "duration, uploader, thumbnail_url, view_count, textual_upload_date, " + - "upload_date, is_upload_date_approximation) " + - - "SELECT uid, service_id, url, ifnull(title, ''), " + - "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " + - "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " + - - "FROM streams WHERE url IS NOT NULL" - ) - - db.execSQL("DROP TABLE streams") - db.execSQL("ALTER TABLE streams_new RENAME TO streams") - db.execSQL( - "CREATE UNIQUE INDEX index_streams_service_id_url " + - "ON streams (service_id, url)" - ) - - // Tables for feed feature - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed " + - "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + - "PRIMARY KEY(stream_id, subscription_id), " + - "FOREIGN KEY(stream_id) REFERENCES streams(uid) " + - "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + - "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + - "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" - ) - db.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)") - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed_group " + - "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " + - "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)" - ) - db.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)") - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed_group_subscription_join " + - "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + - "PRIMARY KEY(group_id, subscription_id), " + - "FOREIGN KEY(group_id) REFERENCES feed_group(uid) " + - "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + - "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + - "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" - ) - db.execSQL( - "CREATE INDEX index_feed_group_subscription_join_subscription_id " + - "ON feed_group_subscription_join (subscription_id)" - ) - db.execSQL( - "CREATE TABLE IF NOT EXISTS feed_last_updated " + - "(subscription_id INTEGER NOT NULL, last_updated INTEGER, " + - "PRIMARY KEY(subscription_id), " + - "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + - "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" - ) - } - } - - val MIGRATION_3_4 = object : Migration(DB_VER_3, DB_VER_4) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL("ALTER TABLE streams ADD COLUMN uploader_url TEXT") - } - } - - val MIGRATION_4_5 = object : Migration(DB_VER_4, DB_VER_5) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL( - "ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " + - "INTEGER NOT NULL DEFAULT 0" - ) - } - } - - val MIGRATION_5_6 = object : Migration(DB_VER_5, DB_VER_6) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL( - "ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " + - "INTEGER NOT NULL DEFAULT 0" - ) - } - } - - val MIGRATION_6_7 = object : Migration(DB_VER_6, DB_VER_7) { - override fun migrate(db: SupportSQLiteDatabase) { - // Create a new column thumbnail_stream_id - db.execSQL( - "ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " + - "INTEGER NOT NULL DEFAULT -1" - ) - - // Migrate the thumbnail_url to the thumbnail_stream_id - db.execSQL( - "UPDATE playlists SET thumbnail_stream_id = (" + - " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" + - " FROM (" + - " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" + - " FROM playlists p" + - " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" + - " LEFT JOIN streams s ON s.uid = ps.stream_id" + - " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" + - " WHERE playlist_uid = playlists.uid)" - ) - - // Remove the thumbnail_url field in the playlist table - db.execSQL( - "CREATE TABLE IF NOT EXISTS `playlists_new`" + - "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "name TEXT, " + - "is_thumbnail_permanent INTEGER NOT NULL, " + - "thumbnail_stream_id INTEGER NOT NULL)" - ) - - db.execSQL( - "INSERT INTO playlists_new" + - " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " + - " FROM playlists" - ) - - db.execSQL("DROP TABLE playlists") - db.execSQL("ALTER TABLE playlists_new RENAME TO playlists") - db.execSQL( - "CREATE INDEX IF NOT EXISTS " + - "`index_playlists_name` ON `playlists` (`name`)" - ) - } - } - - val MIGRATION_7_8 = object : Migration(DB_VER_7, DB_VER_8) { - override fun migrate(db: SupportSQLiteDatabase) { - db.execSQL( - "DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " + - "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)" - ) - db.execSQL("UPDATE search_history SET search = trim(search)") - } - } - - val MIGRATION_8_9 = object : Migration(DB_VER_8, DB_VER_9) { - override fun migrate(db: SupportSQLiteDatabase) { - try { - db.beginTransaction() - - // Update playlists. - // Create a temp table to initialize display_index. - db.execSQL( - "CREATE TABLE `playlists_tmp` " + - "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " + - "`thumbnail_stream_id` INTEGER NOT NULL, " + - "`display_index` INTEGER NOT NULL)" - ) - db.execSQL( - "INSERT INTO `playlists_tmp` " + - "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + - "`display_index`) " + - "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + - "-1 " + - "FROM `playlists`" - ) - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - db.execSQL("DROP TABLE `playlists`") - db.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`") - - // Update remote_playlists. - // Create a temp table to initialize display_index. - db.execSQL( - "CREATE TABLE `remote_playlists_tmp` " + - "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + - "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + - "`thumbnail_url` TEXT, `uploader` TEXT, " + - "`display_index` INTEGER NOT NULL," + - "`stream_count` INTEGER)" - ) - db.execSQL( - "INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " + - "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " + - "`stream_count`)" + - "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + - "-1, `stream_count` FROM `remote_playlists`" - ) - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - db.execSQL("DROP TABLE `remote_playlists`") - db.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`") - - // Create index on the new table. - db.execSQL( - "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + - "ON `remote_playlists` (`service_id`, `url`)" - ) - - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } + db.setTransactionSuccessful() + } finally { + db.endTransaction() } } } From 35eb08baf0a5b2a1101acdf2410941b67f268aea Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Sun, 4 Jan 2026 13:53:43 +0200 Subject: [PATCH 25/30] Delete long orphaned file Was oprhaned at 004c2fa55a3eb918825ce1c9d37c42a3f24c607c --- .../local/history/HistoryEntryAdapter.java | 108 ------------------ 1 file changed, 108 deletions(-) delete mode 100644 app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java deleted file mode 100644 index 709a16b68..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryEntryAdapter.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.schabi.newpipe.local.history; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -import org.schabi.newpipe.util.Localization; - -import java.text.DateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; - - -/** - * This is an adapter for history entries. - * - * @param the type of the entries - * @param the type of the view holder - */ -public abstract class HistoryEntryAdapter - extends RecyclerView.Adapter { - private final ArrayList mEntries; - private final DateFormat mDateFormat; - private final Context mContext; - private OnHistoryItemClickListener onHistoryItemClickListener = null; - - public HistoryEntryAdapter(final Context context) { - super(); - mContext = context; - mEntries = new ArrayList<>(); - mDateFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, - Localization.getPreferredLocale(context)); - } - - public void setEntries(@NonNull final Collection historyEntries) { - mEntries.clear(); - mEntries.addAll(historyEntries); - notifyDataSetChanged(); - } - - public Collection getItems() { - return mEntries; - } - - public void clear() { - mEntries.clear(); - notifyDataSetChanged(); - } - - protected String getFormattedDate(final Date date) { - return mDateFormat.format(date); - } - - protected String getFormattedViewString(final long viewCount) { - return Localization.shortViewCount(mContext, viewCount); - } - - @Override - public int getItemCount() { - return mEntries.size(); - } - - @Override - public void onBindViewHolder(final VH holder, final int position) { - final E entry = mEntries.get(position); - holder.itemView.setOnClickListener(v -> { - if (onHistoryItemClickListener != null) { - onHistoryItemClickListener.onHistoryItemClick(entry); - } - }); - - holder.itemView.setOnLongClickListener(view -> { - if (onHistoryItemClickListener != null) { - onHistoryItemClickListener.onHistoryItemLongClick(entry); - return true; - } - return false; - }); - - onBindViewHolder(holder, entry, position); - } - - @Override - public void onViewRecycled(@NonNull final VH holder) { - super.onViewRecycled(holder); - holder.itemView.setOnClickListener(null); - } - - abstract void onBindViewHolder(VH holder, E entry, int position); - - public void setOnHistoryItemClickListener( - @Nullable final OnHistoryItemClickListener onHistoryItemClickListener) { - this.onHistoryItemClickListener = onHistoryItemClickListener; - } - - public boolean isEmpty() { - return mEntries.isEmpty(); - } - - public interface OnHistoryItemClickListener { - void onHistoryItemClick(E item); - - void onHistoryItemLongClick(E item); - } -} From 61c0d134d78816391891d46e81a4e44b28d282d5 Mon Sep 17 00:00:00 2001 From: "Yevhen Babiichuk (DustDFG)" Date: Mon, 5 Jan 2026 22:56:03 +0200 Subject: [PATCH 26/30] Commit all the playlist changes to db immediately + some additional minor code cleanup in the file --- .../local/playlist/LocalPlaylistFragment.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index f5562549c..1efc0a84c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -111,7 +111,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { @Override public void selected(final LocalItem selectedItem) { - if (selectedItem instanceof PlaylistStreamEntry) { - final StreamEntity item = - ((PlaylistStreamEntry) selectedItem).getStreamEntity(); + if (selectedItem instanceof PlaylistStreamEntry entry) { + final StreamEntity item = entry.getStreamEntity(); NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), item.getServiceId(), item.getUrl(), item.getTitle(), null, false); } @@ -496,6 +495,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment items = itemListAdapter.getItemsList(); final List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { - if (item instanceof PlaylistStreamEntry) { - streamIds.add(((PlaylistStreamEntry) item).getStreamId()); + if (item instanceof PlaylistStreamEntry entry) { + streamIds.add(entry.getStreamId()); } } @@ -767,6 +768,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment Date: Tue, 6 Jan 2026 09:03:44 -0500 Subject: [PATCH 27/30] Fix download resume corruption when server returns HTTP 200 When resuming a download after interruption, if the server returns HTTP 200 (full resource) instead of HTTP 206 (partial content), the code correctly resets mMission.done but fails to reset the 'start' variable. This causes the subsequent file seek to use a stale offset, writing new data at incorrect positions. This bug causes file corruption for large downloads (>5GB) that are interrupted and resumed, particularly when: - Switching between WiFi networks - Server CDN returning different responses - Connection drops during long downloads The corruption manifests as duplicate data regions in the file, which for MP4 downloads results in multiple MOOV atoms and broken seek functionality. Fix: Reset start=0 when HTTP 200 is received, ensuring the file write position correctly restarts from the beginning of the current resource. --- .../main/java/us/shandian/giga/get/DownloadRunnableFallback.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index eed5db463..1d2483e79 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -85,6 +85,7 @@ public class DownloadRunnableFallback extends Thread { if (mMission.unknownLength || mConn.getResponseCode() == 200) { // restart amount of bytes downloaded mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0]; + start = 0; // reset position to avoid writing at wrong offset } mF = mMission.storage.getStream(); From 20b43b521bc600dddd17ea34593821b968566d14 Mon Sep 17 00:00:00 2001 From: AbsurdlyLongUsername <22662897+absurdlylongusername@users.noreply.github.com> Date: Wed, 7 Jan 2026 07:48:32 +0000 Subject: [PATCH 28/30] Revert Google Material Components to 1.11.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 02c89fcef..a189e6d61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ leakcanary = "2.14" lifecycle = "2.9.4" # Newer versions require minSdk >= 23 localbroadcastmanager = "1.1.0" markwon = "4.6.2" -material = "1.13.0" +material = "1.11.0" media = "1.7.1" mockitoCore = "5.21.0" okhttp = "5.3.2" From a7e4afe7f70baf4b44097966791d638142257b21 Mon Sep 17 00:00:00 2001 From: AbsurdlyLongUsername <22662897+absurdlylongusername@users.noreply.github.com> Date: Wed, 7 Jan 2026 21:49:42 +0000 Subject: [PATCH 29/30] Add note to upgrade material components once they fix later versions --- 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 a189e6d61..6bf588242 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,7 +30,7 @@ leakcanary = "2.14" lifecycle = "2.9.4" # Newer versions require minSdk >= 23 localbroadcastmanager = "1.1.0" markwon = "4.6.2" -material = "1.11.0" +material = "1.11.0" # TODO: update to newer version after bug is fixed. See https://github.com/TeamNewPipe/NewPipe/pull/13018 media = "1.7.1" mockitoCore = "5.21.0" okhttp = "5.3.2" From d36a9f01d320c518156ba673630c0418b9b53cc8 Mon Sep 17 00:00:00 2001 From: Tobi Date: Thu, 8 Jan 2026 17:06:50 -0800 Subject: [PATCH 30/30] Add workflow to backport PRs to another branch (#12964) The workflow can be triggered by creating a comment on a merged PR: /backport The backport can only be triggered by people with write access to the repository. Co-authored-by: AbsurdlyLongUsername <22662897+absurdlylongusername@users.noreply.github.com> --- .github/workflows/backport-pr.yml | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/backport-pr.yml diff --git a/.github/workflows/backport-pr.yml b/.github/workflows/backport-pr.yml new file mode 100644 index 000000000..c7bcb117e --- /dev/null +++ b/.github/workflows/backport-pr.yml @@ -0,0 +1,46 @@ +name: Backport merged pull request +on: + issue_comment: + types: [created] +permissions: + contents: write # for comment creation on original PR + pull-requests: write +jobs: + backport: + name: Backport pull request + runs-on: ubuntu-latest + + # Only run when the comment starts with the `/backport` command on a PR and + # the commenter has write access to the repository. We do not want to allow + # everybody to trigger backports and create branches in our repository. + if: > + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/backport ') && + ( + github.event.comment.author_association == 'OWNER' || + github.event.comment.author_association == 'COLLABORATOR' || + github.event.comment.author_association == 'MEMBER' + ) + steps: + - uses: actions/checkout@v4 + - name: Get backport metadata + # the target branch is the first argument after `/backport` + run: | + set -euo pipefail + body="${{ github.event.comment.body }}" + + line=${body%%$'\n'*} # Get the first line + if [[ $line =~ ^/backport[[:space:]]+([^[:space:]]+) ]]; then + echo "BACKPORT_TARGET=${BASH_REMATCH[1]}" >> "$GITHUB_ENV" + else + echo "Usage: /backport " >&2 + exit 1 + fi + + - name: Create backport pull request + uses: korthout/backport-action@v4 + with: + add_labels: 'backport' + copy_labels_pattern: '.*' + label_pattern: '' + target_branches: ${{ env.BACKPORT_TARGET }} \ No newline at end of file