From c51a3a828efa03e6d9bb3190ea2d9a804428638e Mon Sep 17 00:00:00 2001
From: Bnyro <82752168+Bnyro@users.noreply.github.com>
Date: Mon, 1 Aug 2022 16:16:06 +0200
Subject: [PATCH] unauthenticated subscriptions (#1270)

* hmm

* unauthenticated feed

* unauthenticated rss

* Small improvements to code.

* add unauthenticated subscriptions

* cleanup

* Sort subs locally.

* Fix some bugs and small improvements.

Co-authored-by: Kavin <20838718+FireMasterK@users.noreply.github.com>
---
 src/components/ChannelPage.vue       | 29 +++++++++++--------
 src/components/FeedPage.vue          | 29 +++++++++++--------
 src/components/ImportPage.vue        | 42 ++++++++++++++++++----------
 src/components/NavBar.vue            |  4 +--
 src/components/SubscriptionsPage.vue | 29 +++++++++++--------
 src/components/WatchVideo.vue        | 28 +++++++++++--------
 src/main.js                          | 21 ++++++++++++++
 7 files changed, 118 insertions(+), 64 deletions(-)

diff --git a/src/components/ChannelPage.vue b/src/components/ChannelPage.vue
index 3755f54d..42dceb4d 100644
--- a/src/components/ChannelPage.vue
+++ b/src/components/ChannelPage.vue
@@ -14,7 +14,6 @@
         </p>
 
         <button
-            v-if="authenticated"
             class="btn"
             @click="subscribeHandler"
             v-t="{
@@ -50,7 +49,7 @@ export default {
     data() {
         return {
             channel: null,
-            subscribed: false,
+            subscribed: this.authenticated ? false : this.isSubscribedLocally(this.channelId),
         };
     },
     mounted() {
@@ -69,6 +68,8 @@ export default {
     },
     methods: {
         async fetchSubscribedStatus() {
+            if (!this.channelId || !this.authenticated) return;
+
             this.fetchJson(
                 this.authApiUrl() + "/subscribed",
                 {
@@ -113,16 +114,20 @@ export default {
             }
         },
         subscribeHandler() {
-            this.fetchJson(this.authApiUrl() + (this.subscribed ? "/unsubscribe" : "/subscribe"), null, {
-                method: "POST",
-                body: JSON.stringify({
-                    channelId: this.channel.id,
-                }),
-                headers: {
-                    Authorization: this.getAuthToken(),
-                    "Content-Type": "application/json",
-                },
-            });
+            if (this.authenticated) {
+                this.fetchJson(this.authApiUrl() + (this.subscribed ? "/unsubscribe" : "/subscribe"), null, {
+                    method: "POST",
+                    body: JSON.stringify({
+                        channelId: this.channel.id,
+                    }),
+                    headers: {
+                        Authorization: this.getAuthToken(),
+                        "Content-Type": "application/json",
+                    },
+                });
+            } else {
+                this.handleLocalSubscriptions(this.channel.id);
+            }
             this.subscribed = !this.subscribed;
         },
     },
diff --git a/src/components/FeedPage.vue b/src/components/FeedPage.vue
index 66c8b6d6..fea59f56 100644
--- a/src/components/FeedPage.vue
+++ b/src/components/FeedPage.vue
@@ -1,7 +1,7 @@
 <template>
     <h1 v-t="'titles.feed'" class="font-bold text-center my-4" />
 
-    <button v-if="authenticated" class="btn mr-2" @click="exportHandler">
+    <button class="btn mr-2" @click="exportHandler">
         <router-link to="/subscriptions">Subscriptions</router-link>
     </button>
 
@@ -41,17 +41,16 @@ export default {
     },
     computed: {
         getRssUrl(_this) {
-            return _this.authApiUrl() + "/feed/rss?authToken=" + _this.getAuthToken();
+            if (_this.authenticated) return _this.authApiUrl() + "/feed/rss?authToken=" + _this.getAuthToken();
+            else return _this.authApiUrl() + "/feed/unauthenticated/rss?channels=" + _this.getUnauthenticatedChannels();
         },
     },
     mounted() {
-        if (this.authenticated)
-            this.fetchFeed().then(videos => {
-                this.videosStore = videos;
-                this.loadMoreVideos();
-                this.updateWatched(this.videos);
-            });
-        else this.$router.push("/login");
+        this.fetchFeed().then(videos => {
+            this.videosStore = videos;
+            this.loadMoreVideos();
+            this.updateWatched(this.videos);
+        });
     },
     activated() {
         document.title = this.$t("titles.feed") + " - Piped";
@@ -66,9 +65,15 @@ export default {
     },
     methods: {
         async fetchFeed() {
-            return await this.fetchJson(this.authApiUrl() + "/feed", {
-                authToken: this.getAuthToken(),
-            });
+            if (this.authenticated) {
+                return await this.fetchJson(this.authApiUrl() + "/feed", {
+                    authToken: this.getAuthToken(),
+                });
+            } else {
+                return await this.fetchJson(this.authApiUrl() + "/feed/unauthenticated", {
+                    channels: this.getUnauthenticatedChannels(),
+                });
+            }
         },
         loadMoreVideos() {
             this.currentVideoCount = Math.min(this.currentVideoCount + this.videoStep, this.videosStore.length);
diff --git a/src/components/ImportPage.vue b/src/components/ImportPage.vue
index dcc354b0..4d8791d8 100644
--- a/src/components/ImportPage.vue
+++ b/src/components/ImportPage.vue
@@ -69,7 +69,6 @@ export default {
         },
     },
     activated() {
-        if (!this.authenticated) this.$router.push("/login");
         document.title = "Import - Piped";
     },
     methods: {
@@ -132,21 +131,34 @@ export default {
             });
         },
         handleImport() {
-            this.fetchJson(
-                this.authApiUrl() + "/import",
-                {
-                    override: this.override,
-                },
-                {
-                    method: "POST",
-                    headers: {
-                        Authorization: this.getAuthToken(),
+            if (this.authenticated) {
+                this.fetchJson(
+                    this.authApiUrl() + "/import",
+                    {
+                        override: this.override,
                     },
-                    body: JSON.stringify(this.subscriptions),
-                },
-            ).then(json => {
-                if (json.message === "ok") window.location = "/feed";
-            });
+                    {
+                        method: "POST",
+                        headers: {
+                            Authorization: this.getAuthToken(),
+                        },
+                        body: JSON.stringify(this.subscriptions),
+                    },
+                ).then(json => {
+                    if (json.message === "ok") window.location = "/feed";
+                });
+            } else {
+                this.importSubscriptionsLocally(this.subscriptions);
+                window.location = "/feed";
+            }
+        },
+        importSubscriptionsLocally(newChannels) {
+            const subscriptions = this.override
+                ? [...new Set(newChannels)]
+                : [...new Set(this.getLocalSubscriptions().concat(newChannels))];
+            // Sort for better cache hits
+            subscriptions.sort();
+            localStorage.setItem("localSubscriptions", JSON.stringify(subscriptions));
         },
     },
 };
diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue
index aa3707df..31787bac 100644
--- a/src/components/NavBar.vue
+++ b/src/components/NavBar.vue
@@ -52,7 +52,7 @@
             <li v-if="authenticated">
                 <router-link v-t="'titles.playlists'" to="/playlists" />
             </li>
-            <li v-if="authenticated && !shouldShowTrending">
+            <li v-if="!shouldShowTrending">
                 <router-link v-t="'titles.feed'" to="/feed" />
             </li>
         </ul>
@@ -81,7 +81,7 @@
         <li v-if="authenticated">
             <router-link v-t="'titles.playlists'" to="/playlists" />
         </li>
-        <li v-if="authenticated && !shouldShowTrending">
+        <li v-if="!shouldShowTrending">
             <router-link v-t="'titles.feed'" to="/feed" />
         </li>
     </ul>
diff --git a/src/components/SubscriptionsPage.vue b/src/components/SubscriptionsPage.vue
index 793e8288..4d7affaf 100644
--- a/src/components/SubscriptionsPage.vue
+++ b/src/components/SubscriptionsPage.vue
@@ -1,7 +1,7 @@
 <template>
     <h1 class="font-bold text-center my-4" v-t="'titles.subscriptions'" />
 
-    <div v-if="authenticated" class="flex justify-between w-full">
+    <div class="flex justify-between w-full">
         <div class="flex">
             <button class="btn mx-1">
                 <router-link to="/import" v-t="'actions.import_from_json'" />
@@ -40,21 +40,28 @@ export default {
         };
     },
     mounted() {
-        if (this.authenticated)
-            this.fetchJson(this.authApiUrl() + "/subscriptions", null, {
-                headers: {
-                    Authorization: this.getAuthToken(),
-                },
-            }).then(json => {
-                this.subscriptions = json;
-                this.subscriptions.forEach(subscription => (subscription.subscribed = true));
-            });
-        else this.$router.push("/login");
+        this.fetchSubscriptions().then(json => {
+            this.subscriptions = json;
+            this.subscriptions.forEach(subscription => (subscription.subscribed = true));
+        });
     },
     activated() {
         document.title = "Subscriptions - Piped";
     },
     methods: {
+        async fetchSubscriptions() {
+            if (this.authenticated) {
+                return await this.fetchJson(this.authApiUrl() + "/subscriptions", null, {
+                    headers: {
+                        Authorization: this.getAuthToken(),
+                    },
+                });
+            } else {
+                return await this.fetchJson(this.authApiUrl() + "/subscriptions/unauthenticated", {
+                    channels: this.getUnauthenticatedChannels(),
+                });
+            }
+        },
         handleButton(subscription) {
             this.fetchJson(this.authApiUrl() + (subscription.subscribed ? "/unsubscribe" : "/subscribe"), null, {
                 method: "POST",
diff --git a/src/components/WatchVideo.vue b/src/components/WatchVideo.vue
index c9a562c9..deb65dae 100644
--- a/src/components/WatchVideo.vue
+++ b/src/components/WatchVideo.vue
@@ -80,7 +80,6 @@
                     </button>
                     <button
                         class="btn"
-                        v-if="authenticated"
                         @click="subscribeHandler"
                         v-t="{
                             path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
@@ -427,7 +426,8 @@ export default {
             this.fetchComments().then(data => (this.comments = data));
         },
         async fetchSubscribedStatus() {
-            if (!this.channelId || !this.authenticated) return;
+            if (!this.channelId) return;
+            if (!this.authenticated) this.subscribed = this.isSubscribedLocally(this.channelId);
 
             this.fetchJson(
                 this.authApiUrl() + "/subscribed",
@@ -444,16 +444,20 @@ export default {
             });
         },
         subscribeHandler() {
-            this.fetchJson(this.authApiUrl() + (this.subscribed ? "/unsubscribe" : "/subscribe"), null, {
-                method: "POST",
-                body: JSON.stringify({
-                    channelId: this.channelId,
-                }),
-                headers: {
-                    Authorization: this.getAuthToken(),
-                    "Content-Type": "application/json",
-                },
-            });
+            if (this.authenticated) {
+                this.fetchJson(this.authApiUrl() + (this.subscribed ? "/unsubscribe" : "/subscribe"), null, {
+                    method: "POST",
+                    body: JSON.stringify({
+                        channelId: this.channelId,
+                    }),
+                    headers: {
+                        Authorization: this.getAuthToken(),
+                        "Content-Type": "application/json",
+                    },
+                });
+            } else {
+                this.handleLocalSubscriptions(this.channelId);
+            }
             this.subscribed = !this.subscribed;
         },
         handleScroll() {
diff --git a/src/main.js b/src/main.js
index 7e64637b..171e6a33 100644
--- a/src/main.js
+++ b/src/main.js
@@ -199,6 +199,27 @@ const mixin = {
                 });
             }
         },
+        getLocalSubscriptions() {
+            return JSON.parse(localStorage.getItem("localSubscriptions"));
+        },
+        isSubscribedLocally(channelId) {
+            const localSubscriptions = this.getLocalSubscriptions();
+            if (localSubscriptions == null) return false;
+            return localSubscriptions.includes(channelId);
+        },
+        handleLocalSubscriptions(channelId) {
+            var localSubscriptions = this.getLocalSubscriptions() ?? [];
+            if (localSubscriptions.includes(channelId))
+                localSubscriptions.splice(localSubscriptions.indexOf(channelId));
+            else localSubscriptions.push(channelId);
+            // Sort for better cache hits
+            localSubscriptions.sort();
+            localStorage.setItem("localSubscriptions", JSON.stringify(localSubscriptions));
+        },
+        getUnauthenticatedChannels() {
+            const localSubscriptions = this.getLocalSubscriptions();
+            return localSubscriptions.join(",");
+        },
     },
     computed: {
         theme() {