From 195163840b0e18f11f67af6f03c60eeaac86e858 Mon Sep 17 00:00:00 2001
From: Kelvin <kelvin@futo.org>
Date: Tue, 9 Jan 2024 22:25:41 +0100
Subject: [PATCH] DOMParser toNodeTree, Better subscription errors, Watch later
 ordering

---
 .../engine/packages/PackageDOMParser.kt       | 27 ++++++++++++++++++-
 .../channel/tab/ChannelContentsFragment.kt    |  6 ++++-
 .../main/SubscriptionsFeedFragment.kt         | 12 +++++++--
 .../platformplayer/states/StatePlaylists.kt   | 13 +++++++--
 .../SubscriptionsTaskFetchAlgorithm.kt        |  4 +--
 5 files changed, 54 insertions(+), 8 deletions(-)

diff --git a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageDOMParser.kt b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageDOMParser.kt
index c261487d..6460b2a9 100644
--- a/app/src/main/java/com/futo/platformplayer/engine/packages/PackageDOMParser.kt
+++ b/app/src/main/java/com/futo/platformplayer/engine/packages/PackageDOMParser.kt
@@ -5,8 +5,11 @@ import com.caoccao.javet.annotations.V8Function
 import com.caoccao.javet.annotations.V8Property
 import com.caoccao.javet.enums.V8ConversionMode
 import com.caoccao.javet.enums.V8ProxyMode
+import com.caoccao.javet.values.reference.V8ValueObject
 import com.futo.platformplayer.engine.V8Plugin
 import com.futo.platformplayer.engine.internal.V8BindObject
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
 import org.jsoup.Jsoup
 import org.jsoup.nodes.Element
 
@@ -65,7 +68,7 @@ class PackageDOMParser : V8Package {
             return result;
         }
         @V8Property
-        fun attributes(): Map<String, String> = _element.attributes().dataset();
+        fun attributes(): Map<String, String> = _element.attributes().associate { Pair(it.key, it.value) }
         @V8Property
         fun innerHTML(): String = _element.html();
         @V8Property
@@ -138,10 +141,32 @@ class PackageDOMParser : V8Package {
             super.dispose();
         }
 
+        @V8Function
+        fun toNodeTree(): SerializedNode {
+            return SerializedNode(
+                childNodes().map { it.toNodeTree() },
+                _element.tagName(),
+                _element.text(),
+                attributes()
+            );
+        }
+        @V8Function
+        fun toNodeTreeJson(): String {
+            return Json.encodeToString(SerializedNode.serializer(), toNodeTree());
+        }
+
         companion object {
             fun parse(parser: PackageDOMParser, str: String): DOMNode {
                 return DOMNode(parser, Jsoup.parse(str));
             }
         }
+
+        @Serializable
+        class SerializedNode(
+            val children: List<SerializedNode>,
+            val name: String,
+            val value: String,
+            val attributes: Map<String, String>
+        );
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
index 1aec934b..98fda554 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt
@@ -27,6 +27,7 @@ import com.futo.platformplayer.constructs.Event2
 import com.futo.platformplayer.constructs.TaskHandler
 import com.futo.platformplayer.engine.exceptions.PluginException
 import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
+import com.futo.platformplayer.exceptions.ChannelException
 import com.futo.platformplayer.fragment.mainactivity.main.FeedView
 import com.futo.platformplayer.fragment.mainactivity.main.PolycentricProfile
 import com.futo.platformplayer.logging.Logger
@@ -336,8 +337,11 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
                 context?.let {
                     lifecycleScope.launch(Dispatchers.Main) {
                         try {
+                            val channel = if(kv.value is ChannelException) (kv.value as ChannelException).channelNameOrUrl else null;
                             if(jsVideoPager != null)
-                                UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n${kv.value.message}", false);
+                                UIDialogs.toast(it, "Plugin ${jsVideoPager.getPluginConfig().name} failed:\n" +
+                                        (if(!channel.isNullOrEmpty()) "(${channel}) " else "") +
+                                        "${kv.value.message}", false);
                             else
                                 UIDialogs.toast(it, kv.value.message ?: "", false);
                         } catch (e: Throwable) {
diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
index bf6ba06f..76c60e1f 100644
--- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
+++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt
@@ -31,6 +31,7 @@ import com.futo.platformplayer.stores.FragmentedStorage
 import com.futo.platformplayer.stores.FragmentedStorageFileJson
 import com.futo.platformplayer.views.FeedStyle
 import com.futo.platformplayer.views.NoResultsView
+import com.futo.platformplayer.views.ToastView
 import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
 import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
 import com.futo.platformplayer.views.adapters.InsertedViewHolder
@@ -44,6 +45,7 @@ import kotlinx.coroutines.withContext
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
+import java.nio.channels.Channel
 import java.time.OffsetDateTime
 import kotlin.system.measureTimeMillis
 
@@ -440,14 +442,17 @@ class SubscriptionsFeedFragment : MainFragment() {
                                 }
                                 Logger.e(TAG, "Channel [${channel}] failed", ex);
                                 if (toShow is PluginException)
-                                    UIDialogs.appToast(
-                                        context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", toShow.config.name).replace("{message}", toShow.message ?: "")
+                                    UIDialogs.appToast(ToastView.Toast(
+                                                toShow.message +
+                                                (if(channel != null) "\nChannel: " + channel else ""), false, null,
+                                        "Plugin ${toShow.config.name} failed")
                                     );
                                 else
                                     UIDialogs.appToast(ex.message ?: "");
                             }
                         }
                         else {
+                            val failedChannels = exs.filterIsInstance<ChannelException>().map { it.channelNameOrUrl }.distinct().toList();
                             val failedPlugins = exs.filter { it is PluginException || (it is ChannelException && it.cause is PluginException) }
                                 .map { if(it is ChannelException) (it.cause as PluginException) else if(it is PluginException) it else null  }
                                 .filter { it != null }
@@ -456,6 +461,9 @@ class SubscriptionsFeedFragment : MainFragment() {
                                 .toList();
                             for(distinctPluginFail in failedPlugins)
                                 UIDialogs.appToast(context.getString(R.string.plugin_pluginname_failed_message).replace("{pluginName}", distinctPluginFail.config.name).replace("{message}", distinctPluginFail.message ?: ""));
+                            if(failedChannels.isNotEmpty())
+                                UIDialogs.appToast(ToastView.Toast(failedChannels.take(3).map { "- ${it}" }.joinToString("\n") +
+                                        (if(failedChannels.size >= 3) "\nAnd ${failedChannels.size - 3} more" else ""), false, null, "Failed Channels"));
                         }
                     } catch (e: Throwable) {
                         Logger.e(TAG, "Failed to handle exceptions", e)
diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
index 5aff46e4..008a5d1c 100644
--- a/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
+++ b/app/src/main/java/com/futo/platformplayer/states/StatePlaylists.kt
@@ -16,6 +16,7 @@ import com.futo.platformplayer.exceptions.ReconstructionException
 import com.futo.platformplayer.logging.Logger
 import com.futo.platformplayer.models.Playlist
 import com.futo.platformplayer.stores.FragmentedStorage
+import com.futo.platformplayer.stores.StringArrayStorage
 import com.futo.platformplayer.stores.v2.ManagedStore
 import com.futo.platformplayer.stores.v2.ReconstructStore
 import kotlinx.serialization.encodeToString
@@ -35,6 +36,8 @@ class StatePlaylists {
                 = SerializedPlatformVideo.fromVideo(StatePlatform.instance.getContentDetails(backup).await() as IPlatformVideoDetails);
         })
         .load();
+    private val _watchlistOrderStore = FragmentedStorage.get<StringArrayStorage>("watchListOrder"); //Temporary workaround to add order..
+
     val playlistStore = FragmentedStorage.storeJson<Playlist>("playlists")
         .withRestore(PlaylistBackup())
         .load();
@@ -48,26 +51,32 @@ class StatePlaylists {
     }
     fun getWatchLater() : List<SerializedPlatformVideo> {
         synchronized(_watchlistStore) {
-            return _watchlistStore.getItems();
+            val order = _watchlistOrderStore.getAllValues();
+            return _watchlistStore.getItems().sortedBy { order.indexOf(it.url) };
         }
     }
     fun updateWatchLater(updated: List<SerializedPlatformVideo>) {
         synchronized(_watchlistStore) {
             _watchlistStore.deleteAll();
             _watchlistStore.saveAllAsync(updated);
+            _watchlistOrderStore.set(*updated.map { it.url }.toTypedArray());
+            _watchlistOrderStore.save();
         }
         onWatchLaterChanged.emit();
     }
     fun removeFromWatchLater(video: SerializedPlatformVideo) {
         synchronized(_watchlistStore) {
             _watchlistStore.delete(video);
+            _watchlistOrderStore.set(*_watchlistOrderStore.values.filter { it != video.url }.toTypedArray());
+            _watchlistOrderStore.save();
         }
-
         onWatchLaterChanged.emit();
     }
     fun addToWatchLater(video: SerializedPlatformVideo) {
         synchronized(_watchlistStore) {
             _watchlistStore.saveAsync(video);
+            _watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
+            _watchlistOrderStore.save();
         }
         onWatchLaterChanged.emit();
     }
diff --git a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
index a7b57b74..5acbc4cd 100644
--- a/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
+++ b/app/src/main/java/com/futo/platformplayer/subscription/SubscriptionsTaskFetchAlgorithm.kt
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.structures.MultiChronoContentPager
 import com.futo.platformplayer.engine.exceptions.PluginException
 import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
 import com.futo.platformplayer.engine.exceptions.ScriptCriticalException
+import com.futo.platformplayer.engine.exceptions.ScriptException
 import com.futo.platformplayer.exceptions.ChannelException
 import com.futo.platformplayer.findNonRuntimeException
 import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionsFeedFragment
@@ -69,7 +70,6 @@ abstract class SubscriptionsTaskFetchAlgorithm(
         val cachedChannels = mutableListOf<String>()
         val forkTasks = executeSubscriptionTasks(tasks, failedPlugins, cachedChannels);
 
-
         val taskResults = arrayListOf<SubscriptionTaskResult>();
         val timeTotal = measureTimeMillis {
             for(task in forkTasks) {
@@ -200,7 +200,7 @@ abstract class SubscriptionsTaskFetchAlgorithm(
                     else {
                         Logger.i(StateSubscriptions.TAG, "Channel ${task.sub.channel.name} failed, substituting with cache");
                         pager = StateCache.instance.getChannelCachePager(task.sub.channel.url);
-                        taskEx = ex;
+                        taskEx = channelEx;
                         return@submit SubscriptionTaskResult(task, pager, taskEx);
                     }
                 }
-- 
GitLab