Browse Source

feat: add PeerTube instances selection

Miguel Ángel Moreno 1 week ago
parent
commit
4c9237ea37

+ 3 - 2
resources/config.edn

@@ -1,5 +1,6 @@
-{:backend
- {:port #or [#env BACKEND_PORT 3000]
+{:peertube {:instances [{:url "https://framatube.org" :name "FramaTube"}]}
+ :backend
+ {:port #long #or [#env BACKEND_PORT 3000]
   :bg-helper-url
   #or
    [#env BG_HELPER_URL #join ["http://localhost:" #ref [:bg-helper :port]]]}

+ 30 - 1
src/backend/tubo/handlers/services.clj

@@ -1,9 +1,13 @@
 (ns tubo.handlers.services
   (:require
    [clojure.java.data :refer [from-java]]
-   [ring.util.response :refer [response]])
+   [ring.util.response :refer [response]]
+   [ring.util.codec :refer [url-decode]]
+   [ring.util.response :as res])
   (:import
    org.schabi.newpipe.extractor.NewPipe
+   org.schabi.newpipe.extractor.ServiceList
+   org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
    java.util.Locale))
 
 (defn get-service
@@ -29,3 +33,28 @@
 (defn create-services-handler
   [_]
   (response (map get-service (NewPipe/getServices))))
+
+(defn fetch-instance-metadata
+  [url]
+  (from-java (doto (PeertubeInstance. (url-decode url))
+               (.fetchInstanceMetaData))))
+
+(defn create-instance-handler
+  [_]
+  (response (from-java (.getInstance ServiceList/PeerTube))))
+
+(defn create-instance-metadata-handler
+  [{{:keys [url]} :path-params}]
+  (response (fetch-instance-metadata url)))
+
+(defn create-change-instance-handler
+  [{:keys [body-params]}]
+  (if body-params
+    (do
+      (fetch-instance-metadata (:url body-params))
+      (.setInstance ServiceList/PeerTube
+                    (PeertubeInstance. (:url body-params)
+                                       (:name body-params)))
+      (response (str "PeerTube instanced changed to "
+                     (:name body-params))))
+    (throw (ex-info "There was a problem changing PeerTube instance" {}))))

+ 5 - 0
src/backend/tubo/http.clj

@@ -8,6 +8,8 @@
   (:import
    org.schabi.newpipe.extractor.NewPipe
    org.schabi.newpipe.extractor.localization.Localization
+   org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
+   org.schabi.newpipe.extractor.ServiceList
    org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor))
 
 (defonce server (atom nil))
@@ -20,6 +22,9 @@
    (when (config/get-in [:backend :bg-helper-url])
      (YoutubeStreamExtractor/setPoTokenProvider
       (potoken/create-po-token-provider)))
+   (when-let [instance (first (config/get-in [:peertube :instances]))]
+     (.setInstance ServiceList/PeerTube
+                   (PeertubeInstance. (:url instance) (:name instance))))
    (reset! server (run-server #'router/app {:port port}))
    (println "Backend server running on port" port)))
 

+ 9 - 0
src/backend/tubo/router.clj

@@ -38,6 +38,15 @@
                               :parameters {:path  {:service-id int?}
                                            :query {:q string?}}
                               :handler search/create-suggestions-handler}}
+      :api/instance {:get {:summary
+                           "returns the current instance for a given service"
+                           :handler services/create-instance-handler}}
+      :api/instance-metadata
+      {:get {:summary "returns instance metadata for a given service"
+             :handler services/create-instance-metadata-handler}}
+      :api/change-instance
+      {:post {:summary "changes the instance for a given service"
+              :handler services/create-change-instance-handler}}
       :api/default-kiosk {:get
                           {:summary
                            "returns default kiosk entries for a given service"

+ 95 - 73
src/frontend/tubo/events.cljs

@@ -34,79 +34,84 @@
    (let [if-nil #(if (nil? %1) %2 %1)]
      {:fx [[:dispatch
             [:services/fetch-all
-             [:services/load] [:bad-response]]]]
+             [:services/load] [:bad-response]]]
+           [:dispatch
+            [:api/get "services/3/instance" [:peertube/load-active-instance]
+             [:bad-response]]]]
       :db
-      {:player/paused    true
-       :player/muted     (:player/muted store)
-       :player/shuffled  (:player/shuffled store)
-       :player/loop      (if-nil (:player/loop store) :playlist)
-       :player/volume    (if-nil (:player/volume store) 100)
-       :bg-player/show   (:bg-player/show store)
-       :queue            (if-nil (:queue store) [])
-       :queue/position   (if-nil (:queue/position store) 0)
-       :queue/unshuffled (:queue/unshuffled store)
-       :service-id       (if-nil (:service-id store) 0)
-       :bookmarks        (if-nil (:bookmarks store)
-                                 [{:id (nano-id) :name "Liked Streams"}])
-       :settings         {:theme                (if-nil (-> store
-                                                            :settings
-                                                            :theme)
-                                                        "auto")
-                          :show-comments        (if-nil (-> store
-                                                            :settings
-                                                            :show-comments)
-                                                        true)
-                          :show-related         (if-nil (-> store
-                                                            :settings
-                                                            :show-related)
-                                                        true)
-                          :show-description     (if-nil (-> store
-                                                            :settings
-                                                            :show-description)
-                                                        true)
-                          :items-layout         (if-nil (-> store
-                                                            :settings
-                                                            :items-layout)
-                                                        "list")
-                          :default-resolution   (if-nil
-                                                 (-> store
-                                                     :settings
-                                                     :default-resolution)
-                                                 "720p")
-                          :default-video-format (if-nil
-                                                 (-> store
-                                                     :settings
-                                                     :default-video-format)
-                                                 "MPEG-4")
-                          :default-audio-format (if-nil
-                                                 (-> store
-                                                     :settings
-                                                     :default-audio-format)
-                                                 "m4a")
-                          :instance             (if-nil (-> store
-                                                            :settings
-                                                            :instance)
-                                                        (config/get-in
-                                                         [:frontend
-                                                          :backend-url]))
-                          :default-country      (if-nil (-> store
-                                                            :settings
-                                                            :default-country)
-                                                        {0 {:name
-                                                            "United States"
-                                                            :code "US"}})
-                          :default-kiosk        (if-nil (-> store
-                                                            :settings
-                                                            :default-kiosk)
-                                                        {0 "Trending"})
-                          :default-filter       (if-nil (-> store
-                                                            :settings
-                                                            :default-filter)
-                                                        {0 "all"})
-                          :default-service      (if-nil (-> store
-                                                            :settings
-                                                            :default-service)
-                                                        0)}}})))
+      {:player/paused      true
+       :player/muted       (:player/muted store)
+       :player/shuffled    (:player/shuffled store)
+       :player/loop        (if-nil (:player/loop store) :playlist)
+       :player/volume      (if-nil (:player/volume store) 100)
+       :bg-player/show     (:bg-player/show store)
+       :queue              (if-nil (:queue store) [])
+       :queue/position     (if-nil (:queue/position store) 0)
+       :queue/unshuffled   (:queue/unshuffled store)
+       :service-id         (if-nil (:service-id store) 0)
+       :peertube/instances (if-nil (:peertube/instances store)
+                                   (config/get-in [:peertube :instances]))
+       :bookmarks          (if-nil (:bookmarks store)
+                                   [{:id (nano-id) :name "Liked Streams"}])
+       :settings           {:theme                (if-nil (-> store
+                                                              :settings
+                                                              :theme)
+                                                          "auto")
+                            :show-comments        (if-nil (-> store
+                                                              :settings
+                                                              :show-comments)
+                                                          true)
+                            :show-related         (if-nil (-> store
+                                                              :settings
+                                                              :show-related)
+                                                          true)
+                            :show-description     (if-nil (-> store
+                                                              :settings
+                                                              :show-description)
+                                                          true)
+                            :items-layout         (if-nil (-> store
+                                                              :settings
+                                                              :items-layout)
+                                                          "list")
+                            :default-resolution   (if-nil
+                                                   (-> store
+                                                       :settings
+                                                       :default-resolution)
+                                                   "720p")
+                            :default-video-format (if-nil
+                                                   (-> store
+                                                       :settings
+                                                       :default-video-format)
+                                                   "MPEG-4")
+                            :default-audio-format (if-nil
+                                                   (-> store
+                                                       :settings
+                                                       :default-audio-format)
+                                                   "m4a")
+                            :instance             (if-nil (-> store
+                                                              :settings
+                                                              :instance)
+                                                          (config/get-in
+                                                           [:frontend
+                                                            :backend-url]))
+                            :default-country      (if-nil (-> store
+                                                              :settings
+                                                              :default-country)
+                                                          {0 {:name
+                                                              "United States"
+                                                              :code "US"}})
+                            :default-kiosk        (if-nil (-> store
+                                                              :settings
+                                                              :default-kiosk)
+                                                          {0 "Trending"})
+                            :default-filter       (if-nil (-> store
+                                                              :settings
+                                                              :default-filter)
+                                                          {0 "all"})
+                            :default-service      (if-nil (-> store
+                                                              :settings
+                                                              :default-service)
+                                                          0)}}})))
 
 (rf/reg-fx
  :scroll-to-top
@@ -146,7 +151,24 @@
      :request-content-type   :json
      :response-content-types {#"application/.*json" :json}
      :mode                   :cors
-     :credentials            :same-origin
+     :credentials            :omit
+     :on-success             on-success
+     :on-failure             on-failure}}))
+
+(rf/reg-event-fx
+ :api/post
+ (fn [{:keys [db]} [_ path body on-success on-failure params]]
+   {:fetch
+    {:method                 :post
+     :url                    (str (get-in db [:settings :instance])
+                                  "/api/v1/"
+                                  path)
+     :params                 (or params {})
+     :body                   body
+     :request-content-type   :json
+     :response-content-types {#"application/.*json" :json}
+     :mode                   :cors
+     :credentials            :omit
      :on-success             on-success
      :on-failure             on-failure}}))
 

+ 64 - 0
src/frontend/tubo/services/events.cljs

@@ -27,3 +27,67 @@
  :services/load
  (fn [db [_ {:keys [body]}]]
    (assoc db :services body)))
+
+(rf/reg-event-fx
+ :peertube/delete-instance
+ [(rf/inject-cofx :store)]
+ (fn [{:keys [db store]} [_ instance]]
+   (let [pos        (.indexOf (:peertube/instances db) instance)
+         updated-db (update db
+                            :peertube/instances
+                            #(into (subvec % 0 pos) (subvec % (inc pos))))]
+     {:db    updated-db
+      :store (assoc store
+                    :peertube/instances
+                    (:peertube/instances updated-db))})))
+
+(rf/reg-event-fx
+ :peertube/add-instance
+ [(rf/inject-cofx :store)]
+ (fn [{:keys [db store]} [_ {:keys [body]}]]
+   (if (some #(= (:url %) (:url body)) (:peertube/instances db))
+     {:fx [[:dispatch [:notifications/error "Instance already exists"]]]}
+     {:db    (update db :peertube/instances conj body)
+      :store (update store :peertube/instances conj body)
+      :fx    [[:dispatch [:modals/close]]]})))
+
+(rf/reg-event-fx
+ :peertube/load-instances
+ (fn [_ [_ instance {:keys [body]}]]
+   {:fx [[:dispatch [:peertube/load-active-instance {:body instance}]]
+         [:dispatch [:notifications/success body]]]}))
+
+(rf/reg-event-fx
+ :peertube/load-active-instance
+ [(rf/inject-cofx :store)]
+ (fn [{:keys [db store]} [_ {:keys [body]}]]
+   (let [updated-db (update db
+                            :peertube/instances
+                            #(into []
+                                   (map (fn [it]
+                                          (if (= (:url it) (:url body))
+                                            (assoc it :active? true)
+                                            (assoc it :active? false)))
+                                        %)))]
+     {:db    updated-db
+      :store (assoc store
+                    :peertube/instances
+                    (:peertube/instances updated-db))})))
+
+(rf/reg-event-fx
+ :peertube/change-instance
+ (fn [_ [_ instance]]
+   {:fx [[:dispatch
+          [:api/post "services/3/change-instance" instance
+           [:peertube/load-instances instance]
+           [:bad-response]]]]}))
+
+(rf/reg-event-fx
+ :peertube/create-instance
+ (fn [{:keys [db]} [_ instance]]
+   {:fx [[:dispatch
+          [:api/get
+           (str "services/3/instance-metadata/"
+                (js/encodeURIComponent (:url instance)))
+           [:peertube/add-instance]
+           [:bad-response]]]]}))

+ 13 - 2
src/frontend/tubo/services/subs.cljs

@@ -1,8 +1,7 @@
 (ns tubo.services.subs
   (:require
    [re-frame.core :as rf]
-   [tubo.utils :as utils]
-   [tubo.services.views :as services]))
+   [tubo.utils :as utils]))
 
 (rf/reg-sub
  :service-id
@@ -16,6 +15,18 @@
  (fn [id]
    (and id (utils/get-service-color id))))
 
+(rf/reg-sub
+ :peertube/instances
+ (fn [db]
+   (:peertube/instances db)))
+
+(rf/reg-sub
+ :peertube/active-instance
+ (fn [_]
+   (rf/subscribe [:peertube/instances]))
+ (fn [instances]
+   (first (filter :active? instances))))
+
 (rf/reg-sub
  :service-name
  (fn []

+ 21 - 18
src/frontend/tubo/services/views.cljs

@@ -4,21 +4,24 @@
 
 (defn services-dropdown
   [services service-id service-color]
-  [:div.relative.flex.flex-col.items-center-justify-center.text-white.px-2
-   {:style {:background service-color}}
-   [:div.w-full.box-border.z-10.lg:z-0
-    [:select.border-none.focus:ring-transparent.bg-blend-color-dodge.font-bold.w-full
-     {:on-change #(rf/dispatch [:kiosks/change-page
-                                (js/parseInt (.. % -target -value))])
-      :value     service-id
-      :style     {:background :transparent}}
-     (when services
-       (for [[i service] (map-indexed vector services)]
-         ^{:key i}
-         [:option.text-white.bg-neutral-900.border-none
-          {:value (:id service)}
-          (-> service
-              :info
-              :name)]))]]
-   [:div.flex.items-center.justify-end.absolute.min-h-full.top-0.right-4.lg:right-0.z-0
-    [:i.fa-solid.fa-caret-down]]])
+  (let [peertube-instance @(rf/subscribe [:peertube/active-instance])]
+    [:div.relative.flex.flex-col.items-center-justify-center.text-white.px-2
+     {:style {:background service-color}}
+     [:div.w-full.box-border.z-10.lg:z-0
+      [:select.border-none.focus:ring-transparent.bg-blend-color-dodge.font-bold.w-full
+       {:on-change #(rf/dispatch [:kiosks/change-page
+                                  (js/parseInt (.. % -target -value))])
+        :value     service-id
+        :style     {:background :transparent}}
+       (when services
+         (for [[i service] (map-indexed vector services)]
+           ^{:key i}
+           [:option.text-white.bg-neutral-900.border-none
+            {:value (:id service)}
+            (if (= (:id service) 3)
+              (str "PeerTube [" (:name peertube-instance) "]")
+              (-> service
+                  :info
+                  :name))]))]]
+     [:div.flex.items-center.justify-end.absolute.min-h-full.top-0.right-4.lg:right-0.z-0
+      [:i.fa-solid.fa-caret-down]]]))

+ 49 - 3
src/frontend/tubo/settings/views.cljs

@@ -2,7 +2,8 @@
   (:require
    [re-frame.core :as rf]
    [reagent.core :as r]
-   [tubo.layout.views :as layout]))
+   [tubo.layout.views :as layout]
+   [tubo.modals.views :as modals]))
 
 (defn boolean-input
   [label keys value]
@@ -27,6 +28,48 @@
    [select-input "Theme" [:theme] theme #{:auto :light :dark}]
    [select-input "List view mode" [:items-layout] items-layout #{:grid :list}]])
 
+(defn add-peertube-instance
+  []
+  (let [!instance (r/atom {})]
+    (fn []
+      [modals/modal-content "Create New PeerTube Instance?"
+       [layout/text-field "URL" (:url @!instance)
+        #(swap! !instance assoc :url (.. % -target -value))
+        "Instance URL"]
+       [layout/secondary-button "Cancel"
+        #(rf/dispatch [:modals/close])]
+       [layout/primary-button "Create"
+        #(rf/dispatch [:peertube/create-instance @!instance])]])))
+
+(defn peertube-instances-modal
+  []
+  (let [instances @(rf/subscribe [:peertube/instances])]
+    [modals/modal-content "PeerTube instances"
+     [:fieldset.flex.gap-y-2.flex-col
+      (for [[i instance] (map-indexed vector instances)]
+        ^{:key i}
+        [:div.bg-neutral-300.dark:bg-neutral-800.flex.rounded.py-2.px-4.justify-between.flex.items-center
+         [:div.flex.flex-col
+          [:label.text-lg {:for (:name instance)} (:name instance)]
+          [:a.text-red-500
+           {:href (:url instance) :target "blank" :rel "noopener"}
+           (:url instance)]]
+         [:div.flex.gap-x-8
+          (when-not (:active? instance)
+            [:i.fa-solid.fa-trash.cursor-pointer
+             {:on-click #(rf/dispatch [:peertube/delete-instance instance])}])
+          [:input
+           {:type            "radio"
+            :id              (:name instance)
+            :name            "instance"
+            :on-change       #(rf/dispatch [:peertube/change-instance instance])
+            :default-checked (:active? instance)
+            :default-value   (:url instance)}]]])]
+     [layout/secondary-button "Cancel"
+      #(rf/dispatch [:modals/close])]
+     [layout/primary-button "Add"
+      #(rf/dispatch [:modals/open [add-peertube-instance]])]]))
+
 (defn content-settings
   [{:keys [default-service default-country default-kiosk default-filter
            show-description show-comments show-related instance]}]
@@ -68,7 +111,10 @@
      [select-input "Default filter" [:default-filter service-id]
       (or (get default-filter service-id) (first (:content-filters service)))
       (:content-filters service)]
-     [text-input "Instance" [:instance] instance]
+     [text-input "Backend instance" [:instance] instance]
+     [layout/generic-input "PeerTube instances"
+      [layout/primary-button "Edit"
+       #(rf/dispatch [:modals/open [peertube-instances-modal]])]]
      [boolean-input "Show comments" [:show-comments] show-comments]
      [boolean-input "Show description" [:show-description] show-description]
      [boolean-input "Show 'Next' and 'Similar' videos" [:show-related]
@@ -104,7 +150,7 @@
              :left-icon [:i.fa-solid.fa-globe]}]
            :selected-id @!active-tab
            :on-change #(reset! !active-tab %)]
-          [:form.flex.flex-wrap.py-4.gap-y-4
+          [:form.flex.flex-wrap.py-4.gap-y-4 {:on-submit #(.preventDefault %)}
            (case @!active-tab
              :appearance  [appearance-settings settings]
              :content     [content-settings settings]

+ 5 - 1
src/shared/tubo/routes.cljc

@@ -23,7 +23,11 @@
       ["/default-kiosk" :api/default-kiosk]
       ["/kiosks"
        ["" :api/all-kiosks]
-       ["/:kiosk-id" :api/kiosk]]]]
+       ["/:kiosk-id" :api/kiosk]]]
+     ["/3"
+      ["/instance" :api/instance]
+      ["/instance-metadata/:url" :api/instance-metadata]
+      ["/change-instance" :api/change-instance]]]
     ["/streams/:url" :api/stream]
     ["/channels"
      ["/:url"