From 81f03c0b449bec1fa60cf2936bfc3f66a5ad58c1 Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Tue, 17 Jan 2023 13:17:40 +0100 Subject: chore: Rename project --- src/backend/tau/api/channels.clj | 29 ------------- src/backend/tau/api/comments.clj | 45 -------------------- src/backend/tau/api/items.clj | 47 --------------------- src/backend/tau/api/playlists.clj | 31 -------------- src/backend/tau/api/services.clj | 79 ------------------------------------ src/backend/tau/api/streams.clj | 36 ---------------- src/backend/tau/core.clj | 12 ------ src/backend/tau/downloader_impl.clj | 71 -------------------------------- src/backend/tau/handler.clj | 70 -------------------------------- src/backend/tau/http.clj | 24 ----------- src/backend/tau/router.clj | 58 -------------------------- src/backend/tubo/api/channels.clj | 29 +++++++++++++ src/backend/tubo/api/comments.clj | 45 ++++++++++++++++++++ src/backend/tubo/api/items.clj | 46 +++++++++++++++++++++ src/backend/tubo/api/playlists.clj | 31 ++++++++++++++ src/backend/tubo/api/services.clj | 79 ++++++++++++++++++++++++++++++++++++ src/backend/tubo/api/streams.clj | 36 ++++++++++++++++ src/backend/tubo/core.clj | 12 ++++++ src/backend/tubo/downloader_impl.clj | 71 ++++++++++++++++++++++++++++++++ src/backend/tubo/handler.clj | 70 ++++++++++++++++++++++++++++++++ src/backend/tubo/http.clj | 24 +++++++++++ src/backend/tubo/router.clj | 58 ++++++++++++++++++++++++++ 22 files changed, 501 insertions(+), 502 deletions(-) delete mode 100644 src/backend/tau/api/channels.clj delete mode 100644 src/backend/tau/api/comments.clj delete mode 100644 src/backend/tau/api/items.clj delete mode 100644 src/backend/tau/api/playlists.clj delete mode 100644 src/backend/tau/api/services.clj delete mode 100644 src/backend/tau/api/streams.clj delete mode 100644 src/backend/tau/core.clj delete mode 100644 src/backend/tau/downloader_impl.clj delete mode 100644 src/backend/tau/handler.clj delete mode 100644 src/backend/tau/http.clj delete mode 100644 src/backend/tau/router.clj create mode 100644 src/backend/tubo/api/channels.clj create mode 100644 src/backend/tubo/api/comments.clj create mode 100644 src/backend/tubo/api/items.clj create mode 100644 src/backend/tubo/api/playlists.clj create mode 100644 src/backend/tubo/api/services.clj create mode 100644 src/backend/tubo/api/streams.clj create mode 100644 src/backend/tubo/core.clj create mode 100644 src/backend/tubo/downloader_impl.clj create mode 100644 src/backend/tubo/handler.clj create mode 100644 src/backend/tubo/http.clj create mode 100644 src/backend/tubo/router.clj (limited to 'src/backend') diff --git a/src/backend/tau/api/channels.clj b/src/backend/tau/api/channels.clj deleted file mode 100644 index 2c82767..0000000 --- a/src/backend/tau/api/channels.clj +++ /dev/null @@ -1,29 +0,0 @@ -(ns tau.api.channels - (:require - [clojure.java.data :as j] - [ring.util.codec :refer [url-decode]] - [tau.api.items :as items]) - (:import - org.schabi.newpipe.extractor.channel.ChannelInfo - org.schabi.newpipe.extractor.NewPipe - org.schabi.newpipe.extractor.Page)) - -(defn get-channel - ([url] - (let [info (ChannelInfo/getInfo (url-decode url))] - {:id (.getId info) - :name (.getName info) - :verified? (.isVerified info) - :banner (.getBannerUrl info) - :avatar (.getAvatarUrl info) - :description (.getDescription info) - :subscriber-count (when-not (= (.getSubscriberCount info) -1) (.getSubscriberCount info)) - :donation-links (.getDonationLinks info) - :next-page (j/from-java (.getNextPage info)) - :related-streams (items/get-items (.getRelatedItems info)) - :service-id (.getServiceId info)})) - ([url page-url] - (let [service (NewPipe/getServiceByUrl (url-decode url)) - info (ChannelInfo/getMoreItems service (url-decode url) (Page. (url-decode page-url)))] - {:related-streams (items/get-items (.getItems info)) - :next-page (j/from-java (.getNextPage info))}))) diff --git a/src/backend/tau/api/comments.clj b/src/backend/tau/api/comments.clj deleted file mode 100644 index c0891ea..0000000 --- a/src/backend/tau/api/comments.clj +++ /dev/null @@ -1,45 +0,0 @@ -(ns tau.api.comments - (:require - [clojure.java.data :as j] - [ring.util.codec :refer [url-decode]]) - (:import - org.schabi.newpipe.extractor.NewPipe - org.schabi.newpipe.extractor.Page - org.schabi.newpipe.extractor.ListExtractor - org.schabi.newpipe.extractor.comments.CommentsInfoItem - org.schabi.newpipe.extractor.comments.CommentsInfo)) - -(defn get-comment-item - [item extractor] - {:id (.getCommentId item) - :text (.getCommentText item) - :uploader-name (.getUploaderName item) - :uploader-avatar (.getUploaderAvatarUrl item) - :uploader-url (.getUploaderUrl item) - :uploader-verified? (.isUploaderVerified item) - :upload-date (.getTextualUploadDate item) - :like-count (when-not (= (.getLikeCount item) -1) (.getLikeCount item)) - :reply-count (when-not (= (.getReplyCount item) -1) (.getReplyCount item)) - :hearted-by-uploader? (.isHeartedByUploader item) - :pinned? (.isPinned item) - :stream-position (when-not (= (.getStreamPosition item) -1) (.getStreamPosition item)) - :replies (when (.getReplies item) - (if extractor - (let [comments-page (.getPage extractor (.getReplies item))] - {:next-page (when (.hasNextPage comments-page) (j/from-java (.getNextPage comments-page))) - :items (map #(get-comment-item % extractor) (.getItems comments-page))}) - (j/from-java (.getReplies item))))}) - -(defn get-comments - ([url] - (let [info (CommentsInfo/getInfo (url-decode url)) - extractor (.getCommentsExtractor info)] - {:comments (map #(get-comment-item % extractor) (.getRelatedItems info)) - :next-page (j/from-java (.getNextPage info)) - :disabled? (.isCommentsDisabled info)})) - ([url page-url] - (let [service (NewPipe/getServiceByUrl (url-decode url)) - info (CommentsInfo/getMoreItems service (url-decode url) (Page. (url-decode page-url)))] - {:comments (map #(get-comment-item % nil) (.getItems info)) - :next-page (j/from-java (.getNextPage info)) - :disabled? false}))) diff --git a/src/backend/tau/api/items.clj b/src/backend/tau/api/items.clj deleted file mode 100644 index 676af3e..0000000 --- a/src/backend/tau/api/items.clj +++ /dev/null @@ -1,47 +0,0 @@ -(ns tau.api.items) - -(defn get-stream-item - [stream] - {:type :stream - :url (.getUrl stream) - :name (.getName stream) - :thumbnail-url (.getThumbnailUrl stream) - :uploader-name (.getUploaderName stream) - :uploader-url (.getUploaderUrl stream) - :uploader-avatar (.getUploaderAvatarUrl stream) - :upload-date (.getTextualUploadDate stream) - :short-description (.getShortDescription stream) - :duration (.getDuration stream) - :view-count (when-not (= (.getViewCount stream) -1) (.getViewCount stream)) - :uploaded (if (.getUploadDate stream) - (.. stream (getUploadDate) (offsetDateTime) (toInstant) (toEpochMilli)) - false) - :verified? (.isUploaderVerified stream)}) - -(defn get-channel-item - [channel] - {:type :channel - :url (.getUrl channel) - :name (.getName channel) - :thumbnail-url (.getThumbnailUrl channel) - :description (.getDescription channel) - :subscriber-count (when-not (= (.getSubscriberCount channel) -1) (.getSubscriberCount channel)) - :stream-count (when-not (= (.getStreamCount channel) -1) (.getStreamCount channel)) - :verified? (.isVerified channel)}) - -(defn get-playlist-item - [playlist] - {:type :playlist - :url (.getUrl playlist) - :name (.getName playlist) - :thumbnail-url (.getThumbnailUrl playlist) - :uploader-name (.getUploaderName playlist) - :stream-count (when-not (= (.getStreamCount playlist) -1) (.getStreamCount playlist))}) - -(defn get-items - [items] - (map #(case (.name (.getInfoType %)) - "STREAM" (get-stream-item %) - "CHANNEL" (get-channel-item %) - "PLAYLIST" (get-playlist-item %)) - items)) diff --git a/src/backend/tau/api/playlists.clj b/src/backend/tau/api/playlists.clj deleted file mode 100644 index 5520d2b..0000000 --- a/src/backend/tau/api/playlists.clj +++ /dev/null @@ -1,31 +0,0 @@ -(ns tau.api.playlists - (:require - [clojure.java.data :as j] - [ring.util.codec :refer [url-decode]] - [tau.api.items :as items]) - (:import - org.schabi.newpipe.extractor.playlist.PlaylistInfo - org.schabi.newpipe.extractor.Page - org.schabi.newpipe.extractor.NewPipe)) - -(defn get-playlist - ([url] - (let [service (NewPipe/getServiceByUrl (url-decode url)) - info (PlaylistInfo/getInfo service (url-decode url))] - {:id (.getId info) - :name (.getName info) - :playlist-type (j/from-java (.getPlaylistType info)) - :thumbnail-url (.getThumbnailUrl info) - :banner-url (.getBannerUrl info) - :uploader-name (.getUploaderName info) - :uploader-url (.getUploaderUrl info) - :uploader-avatar (.getUploaderAvatarUrl info) - :stream-count (.getStreamCount info) - :next-page (j/from-java (.getNextPage info)) - :related-streams (items/get-items (.getRelatedItems info)) - :service-id (.getServiceId info)})) - ([url page-url] - (let [service (NewPipe/getServiceByUrl (url-decode url)) - info (PlaylistInfo/getMoreItems service url (Page. (url-decode page-url)))] - {:next-page (j/from-java (.getNextPage info)) - :related-streams (items/get-items (.getItems info))}))) diff --git a/src/backend/tau/api/services.clj b/src/backend/tau/api/services.clj deleted file mode 100644 index 8b9638f..0000000 --- a/src/backend/tau/api/services.clj +++ /dev/null @@ -1,79 +0,0 @@ -(ns tau.api.services - (:require - [clojure.java.data :as j] - [ring.util.codec :refer [url-encode url-decode]] - [tau.api.items :as items]) - (:import - org.schabi.newpipe.extractor.kiosk.KioskInfo - org.schabi.newpipe.extractor.kiosk.KioskList - org.schabi.newpipe.extractor.InfoItem - org.schabi.newpipe.extractor.NewPipe - org.schabi.newpipe.extractor.Page - org.schabi.newpipe.extractor.StreamingService - org.schabi.newpipe.extractor.search.SearchInfo)) - -(defn search - ([service-id query content-filters sort-filter] - (let [service (NewPipe/getService service-id) - query-handler (.. service - (getSearchQHFactory) - (fromQuery query (or content-filters '()) (or sort-filter ""))) - info (SearchInfo/getInfo service query-handler)] - {:items (items/get-items (.getRelatedItems info)) - :next-page (j/from-java (.getNextPage info)) - :search-suggestion (.getSearchSuggestion info) - :corrected-search? (.isCorrectedSearch info)})) - ([service-id query content-filters sort-filter page-url] - (let [service (NewPipe/getService service-id) - url (url-decode page-url) - query-handler (.. service - (getSearchQHFactory) - (fromQuery query (or content-filters '()) (or sort-filter ""))) - info (SearchInfo/getMoreItems service query-handler (Page. url))] - {:items (items/get-items (.getItems info)) - :next-page (j/from-java (.getNextPage info))}))) - -(defn get-kiosk - ([service-id] - (let [service (NewPipe/getService service-id) - extractor (doto (.getDefaultKioskExtractor (.getKioskList service)) - (.fetchPage)) - info (KioskInfo/getInfo extractor)] - {:id (.getId info) - :url (.getUrl info) - :next-page (j/from-java (.getNextPage info)) - :related-streams (items/get-items (.getRelatedItems info))})) - ([kiosk-id service-id] - (let [service (NewPipe/getService service-id) - extractor (doto (.getExtractorById (.getKioskList service) kiosk-id nil) - (.fetchPage)) - info (KioskInfo/getInfo extractor)] - {:id (.getId info) - :url (.getUrl info) - :next-page (j/from-java (.getNextPage info)) - :related-streams (items/get-items (.getRelatedItems info))})) - ([kiosk-id service-id page-url] - (let [service (NewPipe/getService service-id) - extractor (.getExtractorById (.getKioskList service) kiosk-id nil) - url (url-decode page-url) - kiosk-info (KioskInfo/getInfo extractor) - info (KioskInfo/getMoreItems service (.getUrl kiosk-info) (Page. url))] - {:next-page (j/from-java (.getNextPage info)) - :related-streams (items/get-items (.getItems info))}))) - -(defn get-kiosks - [service-id] - (let [service (NewPipe/getService service-id) - kiosks (.getKioskList service)] - {:default-kiosk (.getDefaultKioskId kiosks) - :available-kiosks (.getAvailableKiosks kiosks)})) - -(defn get-service - [service] - {:id (.getServiceId service) - :info (j/from-java (.getServiceInfo service)) - :base-url (.getBaseUrl service)}) - -(defn get-services - [] - (map get-service (NewPipe/getServices))) diff --git a/src/backend/tau/api/streams.clj b/src/backend/tau/api/streams.clj deleted file mode 100644 index e1bd637..0000000 --- a/src/backend/tau/api/streams.clj +++ /dev/null @@ -1,36 +0,0 @@ -(ns tau.api.streams - (:require - [clojure.java.data :as j] - [ring.util.codec :refer [url-decode]] - [tau.api.items :as items]) - (:import - org.schabi.newpipe.extractor.stream.StreamInfo - org.schabi.newpipe.extractor.NewPipe - org.schabi.newpipe.extractor.localization.DateWrapper - java.time.Instant)) - -(defn get-stream - [url] - (let [info (StreamInfo/getInfo (url-decode url))] - {:name (.getName info) - :url (.getUrl info) - :description (.. info (getDescription) (getContent)) - :upload-date (.getTextualUploadDate info) - :uploader-name (.getUploaderName info) - :uploader-url (.getUploaderUrl info) - :uploader-avatar (.getUploaderAvatarUrl info) - :uploader-verified? (.isUploaderVerified info) - :service-id (.getServiceId info) - :thumbnail-url (.getThumbnailUrl info) - :duration (.getDuration info) - :tags (.getTags info) - :category (.getCategory info) - :view-count (.getViewCount info) - :like-count (when-not (= (.getLikeCount info) -1) (.getLikeCount info)) - :dislike-count (when-not (= (.getDislikeCount info) -1) (.getDislikeCount info)) - :subscriber-count (when-not (= (.getUploaderSubscriberCount info) -1) (.getUploaderSubscriberCount info)) - :audio-streams (j/from-java (.getAudioStreams info)) - :video-streams (j/from-java (.getVideoStreams info)) - :hls-url (.getHlsUrl info) - :dash-mpd-url (.getDashMpdUrl info) - :related-streams (items/get-items (.getRelatedStreams info))})) diff --git a/src/backend/tau/core.clj b/src/backend/tau/core.clj deleted file mode 100644 index 662f5b9..0000000 --- a/src/backend/tau/core.clj +++ /dev/null @@ -1,12 +0,0 @@ -(ns tau.core - (:gen-class) - (:require - [tau.http :as http])) - -(defn -main - [& _] - (http/start-server!)) - -(defn reset - [] - (http/stop-server!)) diff --git a/src/backend/tau/downloader_impl.clj b/src/backend/tau/downloader_impl.clj deleted file mode 100644 index cf355e1..0000000 --- a/src/backend/tau/downloader_impl.clj +++ /dev/null @@ -1,71 +0,0 @@ -(ns tau.downloader-impl - (:import - [org.schabi.newpipe.extractor.downloader Response Request] - [okhttp3 Request$Builder OkHttpClient$Builder RequestBody])) - -(gen-class - :name tau.DownloaderImpl - :constructors {[okhttp3.OkHttpClient$Builder] []} - :extends org.schabi.newpipe.extractor.downloader.Downloader - :init downloader-impl) - -(gen-class - :name tau.DownloaderImpl - :constructors {[okhttp3.OkHttpClient$Builder] []} - :extends org.schabi.newpipe.extractor.downloader.Downloader - :prefix "-" - :main false - :state state - :init downloader-impl - :methods [#^{:static true} [init [] tau.DownloaderImpl] - #^{:static true} [getInstance [] tau.DownloaderImpl]]) - -(def user-agent "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0") -(def instance (atom nil)) - -(defn -downloader-impl [builder] - [[] (atom {:client - (.. builder - (readTimeout 30 (java.util.concurrent.TimeUnit/SECONDS)) - (build))})]) - -(defn -init - ([] - (-init (OkHttpClient$Builder.))) - ([builder] - (reset! instance (tau.DownloaderImpl. builder)))) - -(defn -getInstance [] - (or @instance (-init))) - -(defn -execute [this request] - (let [http-method (.httpMethod request) - url (.url request) - headers (.headers request) - data-to-send (.dataToSend request) - request-body (when data-to-send (RequestBody/create nil data-to-send)) - request-builder (.. (Request$Builder.) - (method http-method request-body) - (url url) - (addHeader "User-Agent" user-agent))] - (doseq [pair (.entrySet headers)] - (let [header-name (.getKey pair) - header-value-list (.getValue pair)] - (if (> (.size header-value-list) 1) - (do - (.removeHeader request-builder header-name) - (doseq [header-value header-value-list] - (.addHeader request-builder header-name header-value))) - (if (= (.size header-value-list) 1) - (.header request-builder header-name (.get header-value-list 0)))))) - (let [response (.. (@(.state this) :client) (newCall (.build request-builder)) (execute)) - body (.body response) - response-body-to-return (when body (.string body)) - latest-url (.. response (request) (url) (toString))] - (when (= (.code response) 429) - (.close response)) - (Response. (.code response) - (.message response) - (.. response (headers) (toMultimap)) - response-body-to-return - latest-url)))) diff --git a/src/backend/tau/handler.clj b/src/backend/tau/handler.clj deleted file mode 100644 index 3ec4469..0000000 --- a/src/backend/tau/handler.clj +++ /dev/null @@ -1,70 +0,0 @@ -(ns tau.handler - (:require - [clojure.string :as str] - [hiccup.page :as hiccup] - [ring.util.response :refer [response]] - [tau.api.streams :as streams] - [tau.api.channels :as channels] - [tau.api.playlists :as playlists] - [tau.api.comments :as comments] - [tau.api.services :as services])) - -(defn index - [_] - (response - (hiccup/html5 - [:head - [:meta {:charset "UTF-8"}] - [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] - [:title "Tau"] - (hiccup/include-css "/css/tau.css")] - [:body - [:div#app] - (hiccup/include-js "/js/main.js") - [:script "tau.core.init();"]]))) - -(defn search - [{:keys [parameters] :as req}] - (let [{:keys [service-id]} (:path parameters) - {:keys [q]} (:query parameters) - {:strs [contentFilters sortFilter nextPage]} (:query-params req) - content-filters (and contentFilters (str/split contentFilters #","))] - (response (if nextPage - (services/search service-id q contentFilters sortFilter nextPage) - (services/search service-id q contentFilters sortFilter))))) - -(defn channel - [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}] - (response (if nextPage - (channels/get-channel url nextPage) - (channels/get-channel url)))) - -(defn playlist - [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}] - (response (if nextPage - (playlists/get-playlist url nextPage) - (playlists/get-playlist url)))) - -(defn comments - [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}] - (response (if nextPage - (comments/get-comments url nextPage) - (comments/get-comments url)))) - -(defn services - [_] - (response (services/get-services))) - -(defn kiosks - [{{{:keys [service-id]} :path} :parameters}] - (response (services/get-kiosks service-id))) - -(defn kiosk - [{{{:keys [kiosk-id service-id]} :path} :parameters {:strs [nextPage]} :query-params}] - (response (cond - (and kiosk-id service-id nextPage) (services/get-kiosk kiosk-id service-id nextPage) - (and kiosk-id service-id) (services/get-kiosk kiosk-id service-id) - :else (services/get-kiosk service-id)))) - -(defn stream [{{:keys [url]} :path-params}] - (response (streams/get-stream url))) diff --git a/src/backend/tau/http.clj b/src/backend/tau/http.clj deleted file mode 100644 index cbb5896..0000000 --- a/src/backend/tau/http.clj +++ /dev/null @@ -1,24 +0,0 @@ -(ns tau.http - (:require - [org.httpkit.server :refer [run-server]] - [tau.router :as router]) - (:import - tau.DownloaderImpl - org.schabi.newpipe.extractor.NewPipe - org.schabi.newpipe.extractor.localization.Localization)) - -(defonce server (atom nil)) - -(defn start-server! - ([] - (start-server! 3000)) - ([port] - (NewPipe/init (DownloaderImpl/init) (Localization. "en" "GB")) - (reset! server (run-server #'router/app {:port port})) - (println "Server running in port" port))) - -(defn stop-server! - [] - (when @server - (@server :timeout 100) - (reset! server nil))) diff --git a/src/backend/tau/router.clj b/src/backend/tau/router.clj deleted file mode 100644 index 5bab278..0000000 --- a/src/backend/tau/router.clj +++ /dev/null @@ -1,58 +0,0 @@ -(ns tau.router - (:require - [malli.experimental.lite :as l] - [reitit.ring :as ring] - [reitit.coercion :as coercion] - [reitit.ring.coercion :as rrc] - [reitit.coercion.malli] - [ring.middleware.reload :refer [wrap-reload]] - [ring.middleware.params :refer [wrap-params]] - [ring.middleware.json :refer [wrap-json-response]] - [ring.middleware.cors :refer [wrap-cors]] - [tau.handler :as handler])) - -(def router - (ring/router - [["/" handler/index] - ["/search" handler/index] - ["/stream" handler/index] - ["/channel" handler/index] - ["/playlist" handler/index] - ["/kiosk" handler/index] - ["/api" - ["/services" - ["" {:get handler/services}] - ["/:service-id/search" - {:get {:coercion reitit.coercion.malli/coercion - :parameters {:path {:service-id int?} - :query {:q string?}} - :handler handler/search}}] - ["/:service-id" - ["/default-kiosk" {:get {:coercion reitit.coercion.malli/coercion - :parameters {:path {:service-id int?}} - :handler handler/kiosk}}] - ["/kiosks" - ["" {:get {:coercion reitit.coercion.malli/coercion - :parameters {:path {:service-id int?}} - :handler handler/kiosks}}] - ["/:kiosk-id" {:get {:coercion reitit.coercion.malli/coercion - :parameters {:path {:service-id int? :kiosk-id string?}} - :handler handler/kiosk}}]]]] - ["/streams/:url" {:get handler/stream}] - ["/channels/:url" {:get handler/channel}] - ["/playlists/:url" {:get handler/playlist}] - ["/comments/:url" {:get handler/comments}]]] - {:data {:middleware [rrc/coerce-request-middleware - rrc/coerce-response-middleware - rrc/coerce-exceptions-middleware]}})) - -(def app - (ring/ring-handler - router - (ring/routes - (ring/create-resource-handler {:path "/"}) - (ring/create-default-handler - {:not-found (constantly {:status 404, :body "Not found"})})) - {:middleware [wrap-params - [wrap-json-response {:pretty true}] - wrap-reload]})) diff --git a/src/backend/tubo/api/channels.clj b/src/backend/tubo/api/channels.clj new file mode 100644 index 0000000..f19a65e --- /dev/null +++ b/src/backend/tubo/api/channels.clj @@ -0,0 +1,29 @@ +(ns tubo.api.channels + (:require + [clojure.java.data :as j] + [ring.util.codec :refer [url-decode]] + [tubo.api.items :as items]) + (:import + org.schabi.newpipe.extractor.channel.ChannelInfo + org.schabi.newpipe.extractor.NewPipe + org.schabi.newpipe.extractor.Page)) + +(defn get-channel + ([url] + (let [info (ChannelInfo/getInfo (url-decode url))] + {:id (.getId info) + :name (.getName info) + :verified? (.isVerified info) + :banner (.getBannerUrl info) + :avatar (.getAvatarUrl info) + :description (.getDescription info) + :subscriber-count (when-not (= (.getSubscriberCount info) -1) (.getSubscriberCount info)) + :donation-links (.getDonationLinks info) + :next-page (j/from-java (.getNextPage info)) + :related-streams (items/get-items (.getRelatedItems info)) + :service-id (.getServiceId info)})) + ([url page-url] + (let [service (NewPipe/getServiceByUrl (url-decode url)) + info (ChannelInfo/getMoreItems service (url-decode url) (Page. (url-decode page-url)))] + {:related-streams (items/get-items (.getItems info)) + :next-page (j/from-java (.getNextPage info))}))) diff --git a/src/backend/tubo/api/comments.clj b/src/backend/tubo/api/comments.clj new file mode 100644 index 0000000..b4f03ba --- /dev/null +++ b/src/backend/tubo/api/comments.clj @@ -0,0 +1,45 @@ +(ns tubo.api.comments + (:require + [clojure.java.data :as j] + [ring.util.codec :refer [url-decode]]) + (:import + org.schabi.newpipe.extractor.NewPipe + org.schabi.newpipe.extractor.Page + org.schabi.newpipe.extractor.ListExtractor + org.schabi.newpipe.extractor.comments.CommentsInfoItem + org.schabi.newpipe.extractor.comments.CommentsInfo)) + +(defn get-comment-item + [item extractor] + {:id (.getCommentId item) + :text (.getCommentText item) + :uploader-name (.getUploaderName item) + :uploader-avatar (.getUploaderAvatarUrl item) + :uploader-url (.getUploaderUrl item) + :uploader-verified? (.isUploaderVerified item) + :upload-date (.getTextualUploadDate item) + :like-count (when-not (= (.getLikeCount item) -1) (.getLikeCount item)) + :reply-count (when-not (= (.getReplyCount item) -1) (.getReplyCount item)) + :hearted-by-uploader? (.isHeartedByUploader item) + :pinned? (.isPinned item) + :stream-position (when-not (= (.getStreamPosition item) -1) (.getStreamPosition item)) + :replies (when (.getReplies item) + (if extractor + (let [comments-page (.getPage extractor (.getReplies item))] + {:next-page (when (.hasNextPage comments-page) (j/from-java (.getNextPage comments-page))) + :items (map #(get-comment-item % extractor) (.getItems comments-page))}) + (j/from-java (.getReplies item))))}) + +(defn get-comments + ([url] + (let [info (CommentsInfo/getInfo (url-decode url)) + extractor (.getCommentsExtractor info)] + {:comments (map #(get-comment-item % extractor) (.getRelatedItems info)) + :next-page (j/from-java (.getNextPage info)) + :disabled? (.isCommentsDisabled info)})) + ([url page-url] + (let [service (NewPipe/getServiceByUrl (url-decode url)) + info (CommentsInfo/getMoreItems service (url-decode url) (Page. (url-decode page-url)))] + {:comments (map #(get-comment-item % nil) (.getItems info)) + :next-page (j/from-java (.getNextPage info)) + :disabled? false}))) diff --git a/src/backend/tubo/api/items.clj b/src/backend/tubo/api/items.clj new file mode 100644 index 0000000..63678c1 --- /dev/null +++ b/src/backend/tubo/api/items.clj @@ -0,0 +1,46 @@ +(ns tubo.api.items) + +(defn get-stream-item + [stream] + {:type :stream + :url (.getUrl stream) + :name (.getName stream) + :thumbnail-url (.getThumbnailUrl stream) + :uploader-name (.getUploaderName stream) + :uploader-url (.getUploaderUrl stream) + :uploader-avatar (.getUploaderAvatarUrl stream) + :upload-date (.getTextualUploadDate stream) + :short-description (.getShortDescription stream) + :duration (.getDuration stream) + :view-count (when-not (= (.getViewCount stream) -1) (.getViewCount stream)) + :uploaded (when (.getUploadDate stream) + (.. stream (getUploadDate) (offsetDateTime) (toInstant) (toEpochMilli))) + :verified? (.isUploaderVerified stream)}) + +(defn get-channel-item + [channel] + {:type :channel + :url (.getUrl channel) + :name (.getName channel) + :thumbnail-url (.getThumbnailUrl channel) + :description (.getDescription channel) + :subscriber-count (when-not (= (.getSubscriberCount channel) -1) (.getSubscriberCount channel)) + :stream-count (when-not (= (.getStreamCount channel) -1) (.getStreamCount channel)) + :verified? (.isVerified channel)}) + +(defn get-playlist-item + [playlist] + {:type :playlist + :url (.getUrl playlist) + :name (.getName playlist) + :thumbnail-url (.getThumbnailUrl playlist) + :uploader-name (.getUploaderName playlist) + :stream-count (when-not (= (.getStreamCount playlist) -1) (.getStreamCount playlist))}) + +(defn get-items + [items] + (map #(case (.name (.getInfoType %)) + "STREAM" (get-stream-item %) + "CHANNEL" (get-channel-item %) + "PLAYLIST" (get-playlist-item %)) + items)) diff --git a/src/backend/tubo/api/playlists.clj b/src/backend/tubo/api/playlists.clj new file mode 100644 index 0000000..9ded200 --- /dev/null +++ b/src/backend/tubo/api/playlists.clj @@ -0,0 +1,31 @@ +(ns tubo.api.playlists + (:require + [clojure.java.data :as j] + [ring.util.codec :refer [url-decode]] + [tubo.api.items :as items]) + (:import + org.schabi.newpipe.extractor.playlist.PlaylistInfo + org.schabi.newpipe.extractor.Page + org.schabi.newpipe.extractor.NewPipe)) + +(defn get-playlist + ([url] + (let [service (NewPipe/getServiceByUrl (url-decode url)) + info (PlaylistInfo/getInfo service (url-decode url))] + {:id (.getId info) + :name (.getName info) + :playlist-type (j/from-java (.getPlaylistType info)) + :thumbnail-url (.getThumbnailUrl info) + :banner-url (.getBannerUrl info) + :uploader-name (.getUploaderName info) + :uploader-url (.getUploaderUrl info) + :uploader-avatar (.getUploaderAvatarUrl info) + :stream-count (.getStreamCount info) + :next-page (j/from-java (.getNextPage info)) + :related-streams (items/get-items (.getRelatedItems info)) + :service-id (.getServiceId info)})) + ([url page-url] + (let [service (NewPipe/getServiceByUrl (url-decode url)) + info (PlaylistInfo/getMoreItems service url (Page. (url-decode page-url)))] + {:next-page (j/from-java (.getNextPage info)) + :related-streams (items/get-items (.getItems info))}))) diff --git a/src/backend/tubo/api/services.clj b/src/backend/tubo/api/services.clj new file mode 100644 index 0000000..0bc44af --- /dev/null +++ b/src/backend/tubo/api/services.clj @@ -0,0 +1,79 @@ +(ns tubo.api.services + (:require + [clojure.java.data :as j] + [ring.util.codec :refer [url-encode url-decode]] + [tubo.api.items :as items]) + (:import + org.schabi.newpipe.extractor.kiosk.KioskInfo + org.schabi.newpipe.extractor.kiosk.KioskList + org.schabi.newpipe.extractor.InfoItem + org.schabi.newpipe.extractor.NewPipe + org.schabi.newpipe.extractor.Page + org.schabi.newpipe.extractor.StreamingService + org.schabi.newpipe.extractor.search.SearchInfo)) + +(defn search + ([service-id query content-filters sort-filter] + (let [service (NewPipe/getService service-id) + query-handler (.. service + (getSearchQHFactory) + (fromQuery query (or content-filters '()) (or sort-filter ""))) + info (SearchInfo/getInfo service query-handler)] + {:items (items/get-items (.getRelatedItems info)) + :next-page (j/from-java (.getNextPage info)) + :search-suggestion (.getSearchSuggestion info) + :corrected-search? (.isCorrectedSearch info)})) + ([service-id query content-filters sort-filter page-url] + (let [service (NewPipe/getService service-id) + url (url-decode page-url) + query-handler (.. service + (getSearchQHFactory) + (fromQuery query (or content-filters '()) (or sort-filter ""))) + info (SearchInfo/getMoreItems service query-handler (Page. url))] + {:items (items/get-items (.getItems info)) + :next-page (j/from-java (.getNextPage info))}))) + +(defn get-kiosk + ([service-id] + (let [service (NewPipe/getService service-id) + extractor (doto (.getDefaultKioskExtractor (.getKioskList service)) + (.fetchPage)) + info (KioskInfo/getInfo extractor)] + {:id (.getId info) + :url (.getUrl info) + :next-page (j/from-java (.getNextPage info)) + :related-streams (items/get-items (.getRelatedItems info))})) + ([kiosk-id service-id] + (let [service (NewPipe/getService service-id) + extractor (doto (.getExtractorById (.getKioskList service) kiosk-id nil) + (.fetchPage)) + info (KioskInfo/getInfo extractor)] + {:id (.getId info) + :url (.getUrl info) + :next-page (j/from-java (.getNextPage info)) + :related-streams (items/get-items (.getRelatedItems info))})) + ([kiosk-id service-id page-url] + (let [service (NewPipe/getService service-id) + extractor (.getExtractorById (.getKioskList service) kiosk-id nil) + url (url-decode page-url) + kiosk-info (KioskInfo/getInfo extractor) + info (KioskInfo/getMoreItems service (.getUrl kiosk-info) (Page. url))] + {:next-page (j/from-java (.getNextPage info)) + :related-streams (items/get-items (.getItems info))}))) + +(defn get-kiosks + [service-id] + (let [service (NewPipe/getService service-id) + kiosks (.getKioskList service)] + {:default-kiosk (.getDefaultKioskId kiosks) + :available-kiosks (.getAvailableKiosks kiosks)})) + +(defn get-service + [service] + {:id (.getServiceId service) + :info (j/from-java (.getServiceInfo service)) + :base-url (.getBaseUrl service)}) + +(defn get-services + [] + (map get-service (NewPipe/getServices))) diff --git a/src/backend/tubo/api/streams.clj b/src/backend/tubo/api/streams.clj new file mode 100644 index 0000000..274d5f2 --- /dev/null +++ b/src/backend/tubo/api/streams.clj @@ -0,0 +1,36 @@ +(ns tubo.api.streams + (:require + [clojure.java.data :as j] + [ring.util.codec :refer [url-decode]] + [tubo.api.items :as items]) + (:import + org.schabi.newpipe.extractor.stream.StreamInfo + org.schabi.newpipe.extractor.NewPipe + org.schabi.newpipe.extractor.localization.DateWrapper + java.time.Instant)) + +(defn get-stream + [url] + (let [info (StreamInfo/getInfo (url-decode url))] + {:name (.getName info) + :url (.getUrl info) + :description (.. info (getDescription) (getContent)) + :upload-date (.getTextualUploadDate info) + :uploader-name (.getUploaderName info) + :uploader-url (.getUploaderUrl info) + :uploader-avatar (.getUploaderAvatarUrl info) + :uploader-verified? (.isUploaderVerified info) + :service-id (.getServiceId info) + :thumbnail-url (.getThumbnailUrl info) + :duration (.getDuration info) + :tags (.getTags info) + :category (.getCategory info) + :view-count (.getViewCount info) + :like-count (when-not (= (.getLikeCount info) -1) (.getLikeCount info)) + :dislike-count (when-not (= (.getDislikeCount info) -1) (.getDislikeCount info)) + :subscriber-count (when-not (= (.getUploaderSubscriberCount info) -1) (.getUploaderSubscriberCount info)) + :audio-streams (j/from-java (.getAudioStreams info)) + :video-streams (j/from-java (.getVideoStreams info)) + :hls-url (.getHlsUrl info) + :dash-mpd-url (.getDashMpdUrl info) + :related-streams (items/get-items (.getRelatedStreams info))})) diff --git a/src/backend/tubo/core.clj b/src/backend/tubo/core.clj new file mode 100644 index 0000000..a587909 --- /dev/null +++ b/src/backend/tubo/core.clj @@ -0,0 +1,12 @@ +(ns tubo.core + (:gen-class) + (:require + [tubo.http :as http])) + +(defn -main + [& _] + (http/start-server!)) + +(defn reset + [] + (http/stop-server!)) diff --git a/src/backend/tubo/downloader_impl.clj b/src/backend/tubo/downloader_impl.clj new file mode 100644 index 0000000..1582159 --- /dev/null +++ b/src/backend/tubo/downloader_impl.clj @@ -0,0 +1,71 @@ +(ns tubo.downloader-impl + (:import + [org.schabi.newpipe.extractor.downloader Response Request] + [okhttp3 Request$Builder OkHttpClient$Builder RequestBody])) + +(gen-class + :name tubo.DownloaderImpl + :constructors {[okhttp3.OkHttpClient$Builder] []} + :extends org.schabi.newpipe.extractor.downloader.Downloader + :init downloader-impl) + +(gen-class + :name tubo.DownloaderImpl + :constructors {[okhttp3.OkHttpClient$Builder] []} + :extends org.schabi.newpipe.extractor.downloader.Downloader + :prefix "-" + :main false + :state state + :init downloader-impl + :methods [#^{:static true} [init [] tubo.DownloaderImpl] + #^{:static true} [getInstance [] tubo.DownloaderImpl]]) + +(def user-agent "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0") +(def instance (atom nil)) + +(defn -downloader-impl [builder] + [[] (atom {:client + (.. builder + (readTimeout 30 (java.util.concurrent.TimeUnit/SECONDS)) + (build))})]) + +(defn -init + ([] + (-init (OkHttpClient$Builder.))) + ([builder] + (reset! instance (tubo.DownloaderImpl. builder)))) + +(defn -getInstance [] + (or @instance (-init))) + +(defn -execute [this request] + (let [http-method (.httpMethod request) + url (.url request) + headers (.headers request) + data-to-send (.dataToSend request) + request-body (when data-to-send (RequestBody/create nil data-to-send)) + request-builder (.. (Request$Builder.) + (method http-method request-body) + (url url) + (addHeader "User-Agent" user-agent))] + (doseq [pair (.entrySet headers)] + (let [header-name (.getKey pair) + header-value-list (.getValue pair)] + (if (> (.size header-value-list) 1) + (do + (.removeHeader request-builder header-name) + (doseq [header-value header-value-list] + (.addHeader request-builder header-name header-value))) + (if (= (.size header-value-list) 1) + (.header request-builder header-name (.get header-value-list 0)))))) + (let [response (.. (@(.state this) :client) (newCall (.build request-builder)) (execute)) + body (.body response) + response-body-to-return (when body (.string body)) + latest-url (.. response (request) (url) (toString))] + (when (= (.code response) 429) + (.close response)) + (Response. (.code response) + (.message response) + (.. response (headers) (toMultimap)) + response-body-to-return + latest-url)))) diff --git a/src/backend/tubo/handler.clj b/src/backend/tubo/handler.clj new file mode 100644 index 0000000..2012504 --- /dev/null +++ b/src/backend/tubo/handler.clj @@ -0,0 +1,70 @@ +(ns tubo.handler + (:require + [clojure.string :as str] + [hiccup.page :as hiccup] + [ring.util.response :refer [response]] + [tubo.api.streams :as streams] + [tubo.api.channels :as channels] + [tubo.api.playlists :as playlists] + [tubo.api.comments :as comments] + [tubo.api.services :as services])) + +(defn index + [_] + (response + (hiccup/html5 + [:head + [:meta {:charset "UTF-8"}] + [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] + [:title "Tubo"] + (hiccup/include-css "/css/tubo.css")] + [:body + [:div#app] + (hiccup/include-js "/js/main.js") + [:script "tubo.core.init();"]]))) + +(defn search + [{:keys [parameters] :as req}] + (let [{:keys [service-id]} (:path parameters) + {:keys [q]} (:query parameters) + {:strs [contentFilters sortFilter nextPage]} (:query-params req) + content-filters (and contentFilters (str/split contentFilters #","))] + (response (if nextPage + (services/search service-id q contentFilters sortFilter nextPage) + (services/search service-id q contentFilters sortFilter))))) + +(defn channel + [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}] + (response (if nextPage + (channels/get-channel url nextPage) + (channels/get-channel url)))) + +(defn playlist + [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}] + (response (if nextPage + (playlists/get-playlist url nextPage) + (playlists/get-playlist url)))) + +(defn comments + [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}] + (response (if nextPage + (comments/get-comments url nextPage) + (comments/get-comments url)))) + +(defn services + [_] + (response (services/get-services))) + +(defn kiosks + [{{{:keys [service-id]} :path} :parameters}] + (response (services/get-kiosks service-id))) + +(defn kiosk + [{{{:keys [kiosk-id service-id]} :path} :parameters {:strs [nextPage]} :query-params}] + (response (cond + (and kiosk-id service-id nextPage) (services/get-kiosk kiosk-id service-id nextPage) + (and kiosk-id service-id) (services/get-kiosk kiosk-id service-id) + :else (services/get-kiosk service-id)))) + +(defn stream [{{:keys [url]} :path-params}] + (response (streams/get-stream url))) diff --git a/src/backend/tubo/http.clj b/src/backend/tubo/http.clj new file mode 100644 index 0000000..9e1359f --- /dev/null +++ b/src/backend/tubo/http.clj @@ -0,0 +1,24 @@ +(ns tubo.http + (:require + [org.httpkit.server :refer [run-server]] + [tubo.router :as router]) + (:import + tubo.DownloaderImpl + org.schabi.newpipe.extractor.NewPipe + org.schabi.newpipe.extractor.localization.Localization)) + +(defonce server (atom nil)) + +(defn start-server! + ([] + (start-server! 3000)) + ([port] + (NewPipe/init (DownloaderImpl/init) (Localization. "en" "GB")) + (reset! server (run-server #'router/app {:port port})) + (println "Server running in port" port))) + +(defn stop-server! + [] + (when @server + (@server :timeout 100) + (reset! server nil))) diff --git a/src/backend/tubo/router.clj b/src/backend/tubo/router.clj new file mode 100644 index 0000000..73e75ee --- /dev/null +++ b/src/backend/tubo/router.clj @@ -0,0 +1,58 @@ +(ns tubo.router + (:require + [malli.experimental.lite :as l] + [reitit.ring :as ring] + [reitit.coercion :as coercion] + [reitit.ring.coercion :as rrc] + [reitit.coercion.malli] + [ring.middleware.reload :refer [wrap-reload]] + [ring.middleware.params :refer [wrap-params]] + [ring.middleware.json :refer [wrap-json-response]] + [ring.middleware.cors :refer [wrap-cors]] + [tubo.handler :as handler])) + +(def router + (ring/router + [["/" handler/index] + ["/search" handler/index] + ["/stream" handler/index] + ["/channel" handler/index] + ["/playlist" handler/index] + ["/kiosk" handler/index] + ["/api" + ["/services" + ["" {:get handler/services}] + ["/:service-id/search" + {:get {:coercion reitit.coercion.malli/coercion + :parameters {:path {:service-id int?} + :query {:q string?}} + :handler handler/search}}] + ["/:service-id" + ["/default-kiosk" {:get {:coercion reitit.coercion.malli/coercion + :parameters {:path {:service-id int?}} + :handler handler/kiosk}}] + ["/kiosks" + ["" {:get {:coercion reitit.coercion.malli/coercion + :parameters {:path {:service-id int?}} + :handler handler/kiosks}}] + ["/:kiosk-id" {:get {:coercion reitit.coercion.malli/coercion + :parameters {:path {:service-id int? :kiosk-id string?}} + :handler handler/kiosk}}]]]] + ["/streams/:url" {:get handler/stream}] + ["/channels/:url" {:get handler/channel}] + ["/playlists/:url" {:get handler/playlist}] + ["/comments/:url" {:get handler/comments}]]] + {:data {:middleware [rrc/coerce-request-middleware + rrc/coerce-response-middleware + rrc/coerce-exceptions-middleware]}})) + +(def app + (ring/ring-handler + router + (ring/routes + (ring/create-resource-handler {:path "/"}) + (ring/create-default-handler + {:not-found (constantly {:status 404, :body "Not found"})})) + {:middleware [wrap-params + [wrap-json-response {:pretty true}] + wrap-reload]})) -- cgit v1.2.3