From 6b9ee104f347b911599dbf60395b4c23802fd387 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Sat, 21 Aug 2021 17:23:57 -0700 Subject: [PATCH 01/36] Started overseerr integration, running into problems with URL encoding --- src/doplarr/core.clj | 1 + src/doplarr/overseerr.clj | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/doplarr/overseerr.clj diff --git a/src/doplarr/core.clj b/src/doplarr/core.clj index c5fd00a..5efcbec 100644 --- a/src/doplarr/core.clj +++ b/src/doplarr/core.clj @@ -124,6 +124,7 @@ {:id (:id interaction) :type (interaction-types (:type interaction)) :token (:token interaction) + :user-id (s/select-one [:member :user :id] interaction) :payload {:component-type (component-types (get-in interaction [:data :component-type])) :component-id (s/select-one [:data :custom-id] interaction) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj new file mode 100644 index 0000000..8a6f00a --- /dev/null +++ b/src/doplarr/overseerr.clj @@ -0,0 +1,84 @@ +(ns doplarr.overseerr + (:require + [com.rpl.specter :as s] + [clojure.core.async :as a] + [config.core :refer [env]] + [doplarr.arr-utils :as utils])) + +(def base-url (delay (str (:overseerr-url env) "/api/v1"))) +(def api-key (delay (:overseerr-api env))) +(def rootfolder (delay (utils/rootfolder @base-url @api-key))) + +; Test if overseerr is enabled +; Find discord user in overseer +; Somehow determine if the user is able to actually perform the request +; Feed that information back to to the bot +; Make screen for permission error + +(defn GET [endpoint & [params]] + (utils/http-request + :get + (str @base-url endpoint) + @api-key + params)) + +(defn POST [endpoint & [params]] + (utils/http-request + :post + (str @base-url endpoint) + @api-key + params)) + +(defn all-users [] + (let [chan (a/promise-chan)] + (a/pipeline + 1 + chan + (map (comp :results :body)) + (GET "/user")) + chan)) + +(defn user-discord-id [id] + (let [chan (a/promise-chan)] + (a/pipeline + 1 + chan + (map (comp :discordId :body)) + (GET (str "/user/" id "/settings/notifications"))) + chan)) + +(defn discord-users [] + (a/go-loop [ids (map :id (a/ Date: Sat, 21 Aug 2021 17:33:30 -0700 Subject: [PATCH 02/36] Hack to fix overseerr's incorrect query parsing --- src/doplarr/overseerr.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index 8a6f00a..04a1ecd 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -61,7 +61,7 @@ 1 chan (s/traverse-all [:body :results (s/filterer :mediaType (s/pred= media-type))]) - (GET "/search" {:query-params {:query term}})) + (GET (str "/search?query=" term))) ; This is a hack, due to Overseerr not playing well with "properly" encoded spaces chan)) (defn search-movie [term] From b2bb5e8f34dc684737d04a360882f768f5cf1393 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Sat, 21 Aug 2021 18:01:14 -0700 Subject: [PATCH 03/36] Made configuration a little nicer, catch quota error --- src/doplarr/config.clj | 25 +++++++++++++++++++++++++ src/doplarr/core.clj | 23 ++++------------------- src/doplarr/overseerr.clj | 13 ++++++++----- 3 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 src/doplarr/config.clj diff --git a/src/doplarr/config.clj b/src/doplarr/config.clj new file mode 100644 index 0000000..558fb4b --- /dev/null +++ b/src/doplarr/config.clj @@ -0,0 +1,25 @@ +(ns doplarr.config + (:require + [config.core :refer [env]])) + +(def bot-requirements #{:bot-token + :role-id}) + +(def direct-requirements #{:sonarr-url + :sonarr-api + :radarr-url + :radarr-api}) + +(def overseerr-requirements #{:overseerr-url + :overseerr-api}) + +;; Default to overseerr if both are configured +(defn backend [] + (cond + (every? env overseerr-requirements) :overseerr + (every? env direct-requirements) :direct + :else nil)) + +(defn validate-env [] + (and (every? env bot-requirements) + (keyword? (backend)))) diff --git a/src/doplarr/core.clj b/src/doplarr/core.clj index 5efcbec..a6ebea5 100644 --- a/src/doplarr/core.clj +++ b/src/doplarr/core.clj @@ -3,6 +3,7 @@ [com.rpl.specter :as s] [doplarr.sonarr :as sonarr] [doplarr.radarr :as radarr] + [doplarr.config :as config] [discljord.messaging :as m] [discljord.connections :as c] [discljord.events :as e] @@ -293,26 +294,10 @@ (m/stop-connection! messaging-ch) (a/close! event-ch))))) -(defn check-config-entry [entry] - (when (nil? (entry env)) - (throw (Exception. (str "Double check the configuration of" entry))))) - -(defn validate-config [] - (let [entries [:sonarr-url - :sonarr-api - :radarr-url - :radarr-api - :bot-token - :role-id]] - (doseq [entry entries] - (try - (check-config-entry entry) - (catch Exception e - (println e) - (System/exit -1)))))) - (defn -main [& _] - (validate-config) + (when-not (config/validate-env) + (println "Error in configuration") + (System/exit -1)) (run) (shutdown-agents)) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index 04a1ecd..752c667 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -77,8 +77,11 @@ (defn request [body] (a/go - (POST - "/request" - {:form-params body - :content-type :json})) - nil) + (let [resp (a/ Date: Sat, 21 Aug 2021 22:31:45 -0700 Subject: [PATCH 04/36] Broke apart repeated functionality Almost done getting the overseerr payload to look like that from sonarr/radarr so we can reuse the same embeds --- README.md | 6 + src/doplarr/config.clj | 2 +- src/doplarr/core.clj | 277 +++----------------------------------- src/doplarr/direct.clj | 68 ++++++++++ src/doplarr/discord.clj | 189 ++++++++++++++++++++++++++ src/doplarr/overseerr.clj | 27 ++-- src/doplarr/proxied.clj | 50 +++++++ 7 files changed, 345 insertions(+), 274 deletions(-) create mode 100644 src/doplarr/direct.clj create mode 100644 src/doplarr/discord.clj create mode 100644 src/doplarr/proxied.clj diff --git a/README.md b/README.md index dddaa4b..f3d8849 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ To do this: 1. Copy out your API keys from Settings -> General +### Overseerr + +Sonarr/Radarr and Overseerr are mutually exclusive - you only need to configure +one. If you are using Overseerr, your users must have associated discord IDs, or +the request will fail. + ## Running with Docker Simply run with diff --git a/src/doplarr/config.clj b/src/doplarr/config.clj index 558fb4b..3ad012c 100644 --- a/src/doplarr/config.clj +++ b/src/doplarr/config.clj @@ -16,7 +16,7 @@ ;; Default to overseerr if both are configured (defn backend [] (cond - (every? env overseerr-requirements) :overseerr + (every? env overseerr-requirements) :proxied (every? env direct-requirements) :direct :else nil)) diff --git a/src/doplarr/core.clj b/src/doplarr/core.clj index a6ebea5..6b4f1e2 100644 --- a/src/doplarr/core.clj +++ b/src/doplarr/core.clj @@ -1,286 +1,47 @@ (ns doplarr.core (:require - [com.rpl.specter :as s] - [doplarr.sonarr :as sonarr] - [doplarr.radarr :as radarr] + [doplarr.discord :as discord] [doplarr.config :as config] + [doplarr.direct :as direct] + [doplarr.proxied :as proxied] [discljord.messaging :as m] [discljord.connections :as c] [discljord.events :as e] [config.core :refer [env]] - [clojure.core.async :as a] - [clojure.core.cache.wrapped :as cache] - [clojure.string :as str]) + [clojure.core.async :as a]) (:gen-class)) -(defonce state (atom nil)) -(defonce cache (cache/ttl-cache-factory {} :ttl 900000)) ; 15 Minute cache expiration, coinciding with the interaction token +(def backend (delay (config/backend))) -(def channel-timeout 600000) - -;; Slash command setup -(def request-command - {:name "request" - :description "Requests a series or movie" - :default_permission false - :options - [{:type 1 - :name "series" - :description "Requests a series" - :options - [{:type 3 - :name "term" - :description "Search term" - :required true}]} - {:type 1 - :name "movie" - :description "Requests a movie", - :options - [{:type 3 - :name "term" - :description "Search term" - :required true}]}]}) - -(def max-results (delay (:max-results env 10))) - -(def search-fn {:series sonarr/search - :movie radarr/search}) - -(def profiles-fn {:series sonarr/quality-profiles - :movie radarr/quality-profiles}) - -(def request-fn {:series sonarr/request - :movie radarr/request}) - -(def timed-out-response {:content "Request timed out, please try again"}) - -(def interaction-types {1 :ping - 2 :application-command - 3 :message-component}) - -(def component-types {1 :action-row - 2 :button - 3 :select-menu}) - -(def request-thumbnail - {:series "https://thetvdb.com/images/logo.png" - :movie "https://i.imgur.com/44ueTES.png"}) - -;; Discljord setup -(defn register-commands [guild-id] - (m/bulk-overwrite-guild-application-commands! - (:messaging @state) - (:id @state) - guild-id - [request-command])) - -(defn set-permission [guild-id command-id] - (m/edit-application-command-permissions! - (:messaging @state) - (:id @state) - guild-id - command-id - [{:id (:role-id env) - :type 1 - :permission true}])) - -(defn interaction-response [interaction-id interaction-token type & {:keys [ephemeral? content components embeds]}] - (m/create-interaction-response! - (:messaging @state) - interaction-id - interaction-token - type - :data - (cond-> {} - ephemeral? (assoc :flags 64) - content (assoc :content content) - components (assoc :components components) - embeds (assoc :embeds embeds)))) - -(defn followup-repsonse [interaction-token & {:keys [ephermeral? content components embeds]}] - (m/create-followup-message! - (:messaging @state) - (:id @state) - interaction-token - (cond-> {} - ephermeral? (assoc :flags 64) - content (assoc :content content) - components (assoc :components components) - embeds (assoc :embeds embeds)))) - -(defn update-interaction-response [interaction-token & {:keys [content components embeds]}] - (m/edit-original-interaction-response! - (:messaging @state) - (:id @state) - interaction-token - :content content - :components components - :embeds embeds)) - -(defn application-command-interaction-option-data [app-com-int-opt] - [(keyword (:name app-com-int-opt)) - (into {} (map (juxt (comp keyword :name) :value)) (:options app-com-int-opt))]) - -(defn interaction-data [interaction] - {:id (:id interaction) - :type (interaction-types (:type interaction)) - :token (:token interaction) - :user-id (s/select-one [:member :user :id] interaction) - :payload - {:component-type (component-types (get-in interaction [:data :component-type])) - :component-id (s/select-one [:data :custom-id] interaction) - :name (s/select-one [:data :name] interaction) - :values (s/select-one [:data :values] interaction) - :options (into {} (map application-command-interaction-option-data) (get-in interaction [:data :options]))}}) - -(defn request-button [uuid enabled?] - {:type 2 - :style 1 - :disabled (not enabled?) - :custom_id (str "request:" uuid) - :label "Request"}) - -(defn select-menu-option [index result] - {:label (:title result) - :description (:year result) - :value index}) - -(defn dropdown [content id options] - {:content content - :components [{:type 1 - :components [{:type 3 - :custom_id id - :options options}]}]}) - -(defn search-response [results uuid] - (if (empty? results) - {:content "Search result returned no hits"} - (dropdown "Choose one of the following results" - (str "select:" uuid) - (map-indexed select-menu-option results)))) - -(defn selection-embed [selection & {:keys [season profile]}] - {:title (:title selection) - :description (:overview selection) - :image {:url (:remotePoster selection)} - :thumbnail {:url (request-thumbnail (if season :series :movie))} - :fields (filterv - identity - [{:name "Profile" - :value profile} - (when season - {:name "Season" - :value (if (= season -1) - "All" - season)})])}) - -(defn request [selection uuid & {:keys [season profile]}] - {:content (str "Request this " (if season "series" "movie") " ?") - :embeds [(selection-embed selection :season season :profile profile)] - :components [{:type 1 :components [(request-button uuid true)]}]}) - -(defn request-alert [selection & {:keys [season profile]}] - {:content "This has been requested!" - :embeds [(selection-embed selection :season season :profile profile)]}) - -(defn select-season [series uuid] - (dropdown "Which season?" - (str "select_season:" uuid) - (conj (map #(hash-map :label (str "Season: " %) :value %) - (range 1 (inc (:seasonCount series)))) - {:label "All Seasons" :value "-1"}))) - -(defn select-profile [profiles uuid] - (dropdown "Which quality profile?" - (str "select_profile:" uuid) - (map #(hash-map :label (:name %) :value (:id %)) profiles))) - -(defn await-interaction [chan token] - (a/go - (a/alt! - (a/timeout channel-timeout) (do - (update-interaction-response token timed-out-response) - nil) - chan ([v] v)))) - -(defn make-request [interaction] - (let [uuid (str (java.util.UUID/randomUUID)) - id (:id interaction) - token (:token interaction) - search (:options (:payload interaction)) - request-type (first (keys search)) - request-term (s/select-one [request-type :term] search) - chan (a/chan)] - ; Send the in-progress response - (interaction-response id token 5 :ephemeral? true) - ; Create this command's channel - (swap! cache assoc uuid chan) - (a/go - (let [results (->> ((search-fn request-type) request-term) - a/> ((profiles-fn request-type)) - a/> ((search-fn request-type) request-term) + a/> ((profiles-fn request-type)) + a/ {} + ephemeral? (assoc :flags 64) + content (assoc :content content) + components (assoc :components components) + embeds (assoc :embeds embeds)))) + +(defn followup-repsonse [interaction-token & {:keys [ephermeral? content components embeds]}] + (m/create-followup-message! + (:messaging @state) + (:id @state) + interaction-token + (cond-> {} + ephermeral? (assoc :flags 64) + content (assoc :content content) + components (assoc :components components) + embeds (assoc :embeds embeds)))) + +(defn update-interaction-response [interaction-token & {:keys [content components embeds]}] + (m/edit-original-interaction-response! + (:messaging @state) + (:id @state) + interaction-token + :content content + :components components + :embeds embeds)) + +(defn application-command-interaction-option-data [app-com-int-opt] + [(keyword (:name app-com-int-opt)) + (into {} (map (juxt (comp keyword :name) :value)) (:options app-com-int-opt))]) + +(defn interaction-data [interaction] + {:id (:id interaction) + :type (interaction-types (:type interaction)) + :token (:token interaction) + :user-id (s/select-one [:member :user :id] interaction) + :payload + {:component-type (component-types (get-in interaction [:data :component-type])) + :component-id (s/select-one [:data :custom-id] interaction) + :name (s/select-one [:data :name] interaction) + :values (s/select-one [:data :values] interaction) + :options (into {} (map application-command-interaction-option-data) (get-in interaction [:data :options]))}}) + +(defn request-button [uuid enabled?] + {:type 2 + :style 1 + :disabled (not enabled?) + :custom_id (str "request:" uuid) + :label "Request"}) + +(defn select-menu-option [index result] + {:label (:title result) + :description (:year result) + :value index}) + +(defn dropdown [content id options] + {:content content + :components [{:type 1 + :components [{:type 3 + :custom_id id + :options options}]}]}) + +(defn search-response [results uuid] + (if (empty? results) + {:content "Search result returned no hits"} + (dropdown "Choose one of the following results" + (str "select:" uuid) + (map-indexed select-menu-option results)))) + +(defn selection-embed [selection & {:keys [season profile]}] + {:title (:title selection) + :description (:overview selection) + :image {:url (:remotePoster selection)} + :thumbnail {:url (request-thumbnail (if season :series :movie))} + :fields (filterv + identity + [(when profile + {:name "Profile" + :value profile}) + (when season + {:name "Season" + :value (if (= season -1) + "All" + season)})])}) + +(defn request [selection uuid & {:keys [season profile]}] + {:content (str "Request this " (if season "series" "movie") " ?") + :embeds [(selection-embed selection :season season :profile profile)] + :components [{:type 1 :components [(request-button uuid true)]}]}) + +(defn request-alert [selection & {:keys [season profile]}] + {:content "This has been requested!" + :embeds [(selection-embed selection :season season :profile profile)]}) + +(defn select-season [series uuid] + (dropdown "Which season?" + (str "select_season:" uuid) + (conj (map #(hash-map :label (str "Season: " %) :value %) + (range 1 (inc (:seasonCount series)))) + {:label "All Seasons" :value "-1"}))) + +(defn await-interaction [chan token] + (a/go + (a/alt! + (a/timeout channel-timeout) (do + (update-interaction-response token timed-out-response) + nil) + chan ([v] v)))) + +(defn continue-request [interaction] + (let [[_ uuid] (str/split (s/select-one [:payload :component-id] interaction) #":")] + (interaction-response (:id interaction) (:token interaction) 6) + (a/offer! (get @cache uuid) interaction))) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index 752c667..a7f0164 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -3,17 +3,11 @@ [com.rpl.specter :as s] [clojure.core.async :as a] [config.core :refer [env]] - [doplarr.arr-utils :as utils])) + [doplarr.arr-utils :as utils] + [clojure.set :as set])) (def base-url (delay (str (:overseerr-url env) "/api/v1"))) (def api-key (delay (:overseerr-api env))) -(def rootfolder (delay (utils/rootfolder @base-url @api-key))) - -; Test if overseerr is enabled -; Find discord user in overseer -; Somehow determine if the user is able to actually perform the request -; Feed that information back to to the bot -; Make screen for permission error (defn GET [endpoint & [params]] (utils/http-request @@ -56,13 +50,12 @@ (recur (rest ids) (assoc users (a/> (set/rename-keys selection {:overview :description}) + (#(assoc-in % [:remotePoster :url] (:posterPath %))))) + (defn request [body] (a/go (let [resp (a/> ((search-fn request-type) request-term) + a/ Date: Mon, 23 Aug 2021 13:51:44 -0700 Subject: [PATCH 05/36] Requesting movies almost works Selection payload now matches radarr/sonarr --- src/doplarr/discord.clj | 2 +- src/doplarr/overseerr.clj | 6 ++++-- src/doplarr/proxied.clj | 5 ++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/doplarr/discord.clj b/src/doplarr/discord.clj index 9a280b2..6aae34b 100644 --- a/src/doplarr/discord.clj +++ b/src/doplarr/discord.clj @@ -180,7 +180,7 @@ (a/alt! (a/timeout channel-timeout) (do (update-interaction-response token timed-out-response) - nil) + (throw (Exception. "Interaction timed out"))) chan ([v] v)))) (defn continue-request [interaction] diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index a7f0164..9e2ae54 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -9,6 +9,8 @@ (def base-url (delay (str (:overseerr-url env) "/api/v1"))) (def api-key (delay (:overseerr-api env))) +(def tmdb-poster-path "https://image.tmdb.org/t/p/original") + (defn GET [endpoint & [params]] (utils/http-request :get @@ -69,8 +71,8 @@ :userId user-id}) (defn selection-to-embedable [selection] - (->> (set/rename-keys selection {:overview :description}) - (#(assoc-in % [:remotePoster :url] (:posterPath %))))) + (->> (assoc selection :description (:overview selection)) + (#(assoc % :remotePoster (str tmdb-poster-path (:posterPath %)))))) (defn request [body] (a/go diff --git a/src/doplarr/proxied.clj b/src/doplarr/proxied.clj index b0d4114..bfef889 100644 --- a/src/doplarr/proxied.clj +++ b/src/doplarr/proxied.clj @@ -25,9 +25,9 @@ (let [results (->> ((search-fn request-type) request-term) a/ Date: Mon, 23 Aug 2021 17:27:28 -0700 Subject: [PATCH 06/36] Started using fmnoise/flow for err handling Requesting movies works now, and throws the appropriate errors on quota and missing user. Radarr/Sonarr need to be cleaned up with the new error passing convention and overseer now needs work on TV shows --- deps.edn | 1 + src/doplarr/arr_utils.clj | 33 +++++++++++---------- src/doplarr/discord.clj | 7 ++--- src/doplarr/overseerr.clj | 60 ++++++++++++++++++--------------------- src/doplarr/proxied.clj | 51 ++++++++++++++++++++++----------- 5 files changed, 85 insertions(+), 67 deletions(-) diff --git a/deps.edn b/deps.edn index bab0665..3c286fe 100644 --- a/deps.edn +++ b/deps.edn @@ -8,6 +8,7 @@ org.suskalo/discljord {:git/url "https://github.com/IGJoshua/discljord" :sha "a417b0d543c68820ce0633b31d7c98c6b328c857"} org.clojure/core.async {:mvn/version "1.3.618"} cheshire/cheshire {:mvn/version "5.10.1"} + fmnoise/flow {:mvn/version "4.1.0"} hato/hato {:mvn/version "0.8.2"}} :jvm-opts ["-Dconfig=config.edn"] diff --git a/src/doplarr/arr_utils.clj b/src/doplarr/arr_utils.clj index bf4ca74..9eab0e2 100644 --- a/src/doplarr/arr_utils.clj +++ b/src/doplarr/arr_utils.clj @@ -2,10 +2,18 @@ (:require [clojure.tools.logging :as log] [clojure.core.async :as a] + [fmnoise.flow :as flow :refer [then else]] [hato.client :as hc])) +(defn fatal-error [ex] + (log/fatal ex) + (System/exit -1)) + (defn http-request [method url key & [params]] - (let [chan (a/chan)] + (let [chan (a/chan) + put-close #(do + (a/put! chan %) + (a/close! chan))] (hc/request (merge {:method method @@ -14,23 +22,18 @@ :async? true :headers {"X-API-Key" key}} params) - #(do - (a/put! chan %) - (a/close! chan)) - #(log/error % (ex-data %))) + put-close + put-close) chan)) (defn rootfolder [base-url key] - (let [chan (a/promise-chan)] - (a/pipeline - 1 - chan - (map (comp :path first :body)) - (http-request - :get - (str base-url "/rootfolder") - key)) - chan)) + (a/go + (->> (a/> (a/> (a/> (a/> (assoc selection :description (:overview selection)) - (#(assoc % :remotePoster (str tmdb-poster-path (:posterPath %)))))) + (as-> selection s + (assoc s :description (:overview s)) + (assoc s :remotePoster (str tmdb-poster-path (:posterPath s))))) -(defn request [body] +(defn request [body & {:keys [season]}] (a/go - (let [resp (a/> (a/> (a/> (a/> (a/> (a/ Date: Sun, 12 Sep 2021 12:18:43 -0700 Subject: [PATCH 07/36] Update dockerfile --- Dockerfile | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6dc9b09..70dfa09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,15 @@ -FROM clojure:openjdk-11-tools-deps-slim-buster - -ENV \ - DEBCONF_NONINTERACTIVE_SEEN=true \ - DEBIAN_FRONTEND="noninteractive" +FROM adoptopenjdk/openjdk11:alpine-jre WORKDIR /app RUN \ - apt-get -qq update \ - && apt-get install -qy \ + apk add --no-cache \ ca-certificates \ tini \ - tzdata \ - && \ - apt-get remove -y jq \ - && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ - && apt-get autoremove -y \ - && apt-get clean \ - && \ - rm -rf \ - /tmp/* \ - /var/lib/apt/lists/* \ - /var/cache/apt/* \ - /var/tmp/* + tzdata COPY . /app -ENTRYPOINT ["/usr/bin/tini", "--"] +ENTRYPOINT ["/sbin/tini", "--"] CMD ["java","-jar","/app/target/Doplarr.jar"] LABEL "maintainer"="Kiran Shila " From f3f123ef332de0d400178a3b08bb3283d6175e51 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Sun, 12 Sep 2021 12:38:52 -0700 Subject: [PATCH 08/36] Fixed bug of final requested pane not showing up --- src/doplarr/proxied.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/doplarr/proxied.clj b/src/doplarr/proxied.clj index 1c01a87..42b3a97 100644 --- a/src/doplarr/proxied.clj +++ b/src/doplarr/proxied.clj @@ -59,8 +59,8 @@ (flet [_ (->> (a/ Date: Sun, 12 Sep 2021 14:27:45 -0700 Subject: [PATCH 09/36] Requesting TV shows works now --- src/doplarr/discord.clj | 2 +- src/doplarr/overseerr.clj | 33 ++++++++++++++++++++++++--------- src/doplarr/proxied.clj | 7 ++++--- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/doplarr/discord.clj b/src/doplarr/discord.clj index d25c0f6..f1bb131 100644 --- a/src/doplarr/discord.clj +++ b/src/doplarr/discord.clj @@ -126,7 +126,7 @@ :label "Request"}) (defn select-menu-option [index result] - {:label (:title result) + {:label (or (:title result) (:name result)) :description (:year result) :value index}) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index be68b0c..992d15c 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -9,7 +9,7 @@ (def base-url (delay (str (:overseerr-url env) "/api/v1"))) (def api-key (delay (:overseerr-api env))) -(def tmdb-poster-path "https://image.tmdb.org/t/p/original") +(def poster-path "https://image.tmdb.org/t/p/original") (defn GET [endpoint & [params]] (utils/http-request @@ -53,9 +53,11 @@ (s/filterer :mediaType (s/pred= media-type)) (s/transformed s/ALL #(assoc % :year (.getYear (java.time.LocalDate/parse - (if (empty? (:releaseDate %)) + (if (empty? (or (:firstAirDate %) + (:releaseDate %))) "0000-01-01" - (:releaseDate %))))))] + (or (:firstAirDate %) + (:releaseDate %)))))))] resp))) (else utils/fatal-error)))) @@ -65,17 +67,30 @@ (defn search-series [term] (search term "tv")) -(defn result-to-request [user-id result] - {:mediaType (:mediaType result) - :mediaId (:id result) - :userId user-id}) +(defn details [id media-type] + (a/go + (->> (a/ {:mediaType (:mediaType result) + :mediaId (:id result) + :userId user-id} + (= "tv" (:mediaType result)) (assoc :seasons (if (= -1 season) + (into [] (range 1 (:seasonCount result))) + [season])))) (defn selection-to-embedable [selection] (as-> selection s + (assoc s :seasonCount (:numberOfSeasons s)) (assoc s :description (:overview s)) - (assoc s :remotePoster (str tmdb-poster-path (:posterPath s))))) + (assoc s :remotePoster (str poster-path (:posterPath s))))) -(defn request [body & {:keys [season]}] +(defn request [body] (a/go (->> (a/> (a/> (a/ Date: Sun, 12 Sep 2021 15:09:11 -0700 Subject: [PATCH 10/36] Added more docs to the README --- README.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0dde47e..6d5e948 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,12 @@ ### Caveats I wanted a clean app for the sole purpose of requesting movies/TV shows. -I personally didn't need Siri, Overseerr, or Ombi integration - so those are missing in this bot (for now). +I personally didn't need Siri or Ombi integration - so those are missing in this +bot. If you need Ombi support, I suggest you check out Overseerr instead. There is only a boolean permission (role gated) for who has access to the bot, nothing fancy. If any of these don't suit your fancy, check out [Requestrr](https://github.com/darkalfx/requestrr) -Please consider this software "beta enough" as some of the error handling is pretty rough around the edges - however I've been using this for a bit now on my own server without any issues, but of course your mileage may vary. - ### Screenshots @@ -56,6 +55,9 @@ To do this: 1. Enable Developer Mode (User Settings -> Advanced -> Developer Mode) 2. Under your server settings, go to Roles, find the role and "Copy ID" +Every user that you wish to have access to the slash commands needs to be +assigned this role (even the server owner/admins). + ### Sonarr/Radarr 1. Copy out your API keys from Settings -> General @@ -66,6 +68,13 @@ Sonarr/Radarr and Overseerr are mutually exclusive - you only need to configure one. If you are using Overseerr, your users must have associated discord IDs, or the request will fail. +This bot isn't meant to wrap the entirety of what Overseerr can do, just the +necessary bits for requesting with optional 4K and quota support. Just use the +web interface to Overseerr if you need more features. + +In the config, you replace `SONARR_URL`, `SONARR_API`, `RADARR_URL`, +`RADARR_API` with `OVERSEERR_URL` and `OVERSEERR_API`. + ## Running with Docker Simply run with @@ -79,7 +88,6 @@ docker run \ -e BOT_TOKEN='bot_token' \ -e ROLE_ID='role_id' \ --name doplarr ghcr.io/kiranshila/doplarr:latest - ``` ## Building and Running Locally From 5c68b56c35be43e597b6600211ea28c4202920a2 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Sun, 12 Sep 2021 16:45:02 -0700 Subject: [PATCH 11/36] Fix request permissions, show proper error on permission --- src/doplarr/arr_utils.clj | 7 ++++++- src/doplarr/discord.clj | 11 ++++++++++- src/doplarr/overseerr.clj | 17 +++++++++++++---- src/doplarr/proxied.clj | 30 +++++++++++++++++++----------- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/doplarr/arr_utils.clj b/src/doplarr/arr_utils.clj index 9eab0e2..0ecf2c3 100644 --- a/src/doplarr/arr_utils.clj +++ b/src/doplarr/arr_utils.clj @@ -9,13 +9,18 @@ (log/fatal ex) (System/exit -1)) +(defn deep-merge [a & maps] + (if (map? a) + (apply merge-with deep-merge a maps) + (apply merge-with deep-merge maps))) + (defn http-request [method url key & [params]] (let [chan (a/chan) put-close #(do (a/put! chan %) (a/close! chan))] (hc/request - (merge + (deep-merge {:method method :url url :as :json diff --git a/src/doplarr/discord.clj b/src/doplarr/discord.clj index f1bb131..4a86319 100644 --- a/src/doplarr/discord.clj +++ b/src/doplarr/discord.clj @@ -125,6 +125,13 @@ :custom_id (str "request:" uuid) :label "Request"}) +(defn request-4k-button [uuid enabled?] + {:type 2 + :style 1 + :disabled (not enabled?) + :custom_id (str "request-4k:" uuid) + :label "Request 4K"}) + (defn select-menu-option [index result] {:label (or (:title result) (:name result)) :description (:year result) @@ -163,7 +170,9 @@ (defn request [selection uuid & {:keys [season profile]}] {:content (str "Request this " (if season "series" "movie") " ?") :embeds [(selection-embed selection :season season :profile profile)] - :components [{:type 1 :components [(request-button uuid true)]}]}) + :components [{:type 1 :components (filterv identity [(request-button uuid true) + (when (:backend-4k selection) + (request-4k-button uuid true))])}]}) (defn request-alert [selection & {:keys [season profile]}] {:content "This has been requested!" diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index 992d15c..e2afad9 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -45,6 +45,14 @@ (let [id (first ids)] (recur (rest ids) (assoc users (a/> (a/> (:body %) + (map :is4k) + (some identity))) + (else utils/fatal-error)))) + (defn search [term media-type] (a/go (->> (a/ {:mediaType (:mediaType result) :mediaId (:id result) - :userId user-id} + :is4k is4k} (= "tv" (:mediaType result)) (assoc :seasons (if (= -1 season) (into [] (range 1 (:seasonCount result))) [season])))) @@ -90,8 +98,9 @@ (assoc s :description (:overview s)) (assoc s :remotePoster (str poster-path (:posterPath s))))) -(defn request [body] +(defn request [body user-id] (a/go (->> (a/> (a/> (a/> (a/ Date: Sat, 18 Sep 2021 13:50:29 -0700 Subject: [PATCH 12/36] Fixed getting all users, used smaller poster image, returns message on duplicate request --- src/doplarr/overseerr.clj | 79 ++++++++++++++++++++++++++------------- src/doplarr/proxied.clj | 33 +++++++++------- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index e2afad9..ce8e1c6 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -9,7 +9,7 @@ (def base-url (delay (str (:overseerr-url env) "/api/v1"))) (def api-key (delay (:overseerr-api env))) -(def poster-path "https://image.tmdb.org/t/p/original") +(def poster-path "https://image.tmdb.org/t/p/w500") (defn GET [endpoint & [params]] (utils/http-request @@ -25,26 +25,6 @@ @api-key params)) -(defn all-users [] - (a/go - (->> (a/> (a/> (a/> (a/> (a/> (s/select-one [:body :results] %) + (map :id) + (into []))) + (else utils/fatal-error)))) + +(defn discord-id [ovsr-id] + (a/go + (->> (a/ {:mediaType (:mediaType result) - :mediaId (:id result) +(defn season-requested? [selection season & {:keys [is4k]}] + (when-let [info (:mediaInfo selection)] + ; Seasons exist, check if the selected season exists + (let [seasons (:seasons info)] + (if (empty? seasons) + true + (= 5 ((if is4k :status4k :status) (nth seasons (dec season)))))))) + +(defn movie-requested? [selection & {:keys [is4k]}] + (when-let [info (:mediaInfo selection)] + (= 5 ((if is4k :status4k :status) info)))) + +(defn selection-requested? [selection & {:keys [season is4k]}] + (case (:mediaType selection) + "tv" (season-requested? selection season :is4k is4k) + "movie" (movie-requested? selection :is4k is4k))) + +(defn selection-to-request [selection & {:keys [season is4k]}] + (cond-> {:mediaType (:mediaType selection) + :mediaId (:id selection) :is4k is4k} - (= "tv" (:mediaType result)) (assoc :seasons (if (= -1 season) - (into [] (range 1 (:seasonCount result))) - [season])))) + (= "tv" (:mediaType selection)) (assoc :seasons (if (= -1 season) + (into [] (range 1 (:seasonCount selection))) + [season])))) (defn selection-to-embedable [selection] (as-> selection s diff --git a/src/doplarr/proxied.clj b/src/doplarr/proxied.clj index 93b1296..0828ed7 100644 --- a/src/doplarr/proxied.clj +++ b/src/doplarr/proxied.clj @@ -21,6 +21,9 @@ (def missing-user-response {:content "You do not have an associated account on Overseerr" :components []}) +(def request-exists-response {:content "This has already been requested" + :components []}) + (defn make-request [interaction] (let [uuid (str (java.util.UUID/randomUUID)) id (:id interaction) @@ -49,12 +52,12 @@ fourK-backend? (a/> (a/> (a/> (a/> (a/ Date: Sun, 19 Sep 2021 11:44:03 -0700 Subject: [PATCH 13/36] Moved all the interaction flow to a multimethod-based state machine --- src/doplarr/arr_utils.clj | 2 +- src/doplarr/core.clj | 14 +-- src/doplarr/direct.clj | 68 ----------- src/doplarr/discord.clj | 36 +++--- src/doplarr/interaction_state_machine.clj | 132 ++++++++++++++++++++++ src/doplarr/overseerr.clj | 46 ++++---- src/doplarr/proxied.clj | 82 -------------- 7 files changed, 178 insertions(+), 202 deletions(-) delete mode 100644 src/doplarr/direct.clj create mode 100644 src/doplarr/interaction_state_machine.clj delete mode 100644 src/doplarr/proxied.clj diff --git a/src/doplarr/arr_utils.clj b/src/doplarr/arr_utils.clj index 0ecf2c3..a5e03fa 100644 --- a/src/doplarr/arr_utils.clj +++ b/src/doplarr/arr_utils.clj @@ -7,7 +7,7 @@ (defn fatal-error [ex] (log/fatal ex) - (System/exit -1)) + #_(System/exit -1)) (defn deep-merge [a & maps] (if (map? a) diff --git a/src/doplarr/core.clj b/src/doplarr/core.clj index 6b4f1e2..f6a957e 100644 --- a/src/doplarr/core.clj +++ b/src/doplarr/core.clj @@ -1,9 +1,8 @@ (ns doplarr.core (:require - [doplarr.discord :as discord] [doplarr.config :as config] - [doplarr.direct :as direct] - [doplarr.proxied :as proxied] + [doplarr.interaction-state-machine :as ism] + [doplarr.discord :as discord] [discljord.messaging :as m] [discljord.connections :as c] [discljord.events :as e] @@ -11,11 +10,6 @@ [clojure.core.async :as a]) (:gen-class)) -(def backend (delay (config/backend))) - -(def backend-request {:direct #'doplarr.direct/make-request - :proxied #'doplarr.proxied/make-request}) - ;;;;;;;;;;;;;;;;;;;;;;;; Gateway event handlers (defmulti handle-event (fn [event-type _] @@ -25,8 +19,8 @@ [_ data] (let [interaction (discord/interaction-data data)] (case (:type interaction) - :application-command ((backend-request @backend) interaction) ; These will all be requests as that is the only top level command - :message-component (discord/continue-request interaction)))) + :application-command (ism/start-interaction interaction) + :message-component (ism/continue-interaction interaction)))) (defmethod handle-event :ready [_ {{id :id} :user}] diff --git a/src/doplarr/direct.clj b/src/doplarr/direct.clj deleted file mode 100644 index 95b1c2b..0000000 --- a/src/doplarr/direct.clj +++ /dev/null @@ -1,68 +0,0 @@ -(ns doplarr.direct - (:require - [doplarr.discord :as discord] - [doplarr.sonarr :as sonarr] - [doplarr.radarr :as radarr] - [com.rpl.specter :as s] - [clojure.core.async :as a])) - -(def search-fn {:series sonarr/search - :movie radarr/search}) - -(def profiles-fn {:series sonarr/quality-profiles - :movie radarr/quality-profiles}) - -(def request-fn {:series sonarr/request - :movie radarr/request}) - -(defn select-profile [profiles uuid] - (discord/dropdown "Which quality profile?" - (str "select_profile:" uuid) - (map #(hash-map :label (:name %) :value (:id %)) profiles))) - -(defn make-request [interaction] - (let [uuid (str (java.util.UUID/randomUUID)) - id (:id interaction) - token (:token interaction) - search (:options (:payload interaction)) - request-type (first (keys search)) - request-term (s/select-one [request-type :term] search) - chan (a/chan)] - ; Send the in-progress response - (discord/interaction-response id token 5 :ephemeral? true) - ; Create this command's channel - (swap! discord/cache assoc uuid chan) - (a/go - (let [results (->> ((search-fn request-type) request-term) - a/> ((profiles-fn request-type)) - a/> (a/> (a/> (a/ (- now last-modified) discord/channel-timeout) + ; Update interaction with timeout message + (discord/update-interaction-response token (discord/content-response "Request times out, please try again.")) + ; Move through the state machine to update cache side effecting new components + (do + (swap! discord/cache assoc-in [uuid :last-modified] now) + (process-event event interaction uuid)))))) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index ce8e1c6..9549501 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -11,6 +11,8 @@ (def poster-path "https://image.tmdb.org/t/p/w500") +(def status [:unknown :pending :processing :partially-available :available]) + (defn GET [endpoint & [params]] (utils/http-request :get @@ -83,31 +85,27 @@ (defn search-series [term] (search term "tv")) -(defn details [id media-type] - (a/go - (->> (a/> (a/ {:mediaType (:mediaType selection) @@ -123,9 +121,15 @@ (assoc s :description (:overview s)) (assoc s :remotePoster (str poster-path (:posterPath s))))) -(defn request [body user-id] +(defn post-process-selection [selection] + (a/go + (let [details (a/> (a/> ((search-fn request-type) request-term) - a/> (a/> (a/> (a/> (a/ Date: Sun, 19 Sep 2021 11:45:27 -0700 Subject: [PATCH 14/36] Renamed the backend --- src/doplarr/config.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doplarr/config.clj b/src/doplarr/config.clj index 3ad012c..558fb4b 100644 --- a/src/doplarr/config.clj +++ b/src/doplarr/config.clj @@ -16,7 +16,7 @@ ;; Default to overseerr if both are configured (defn backend [] (cond - (every? env overseerr-requirements) :proxied + (every? env overseerr-requirements) :overseerr (every? env direct-requirements) :direct :else nil)) From a2c161b59a91439132ca8d9e620eb19b7b1bb0d5 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Sun, 19 Sep 2021 12:52:20 -0700 Subject: [PATCH 15/36] Finished making sure direct connection works --- src/doplarr/interaction_state_machine.clj | 25 ++++++++++++++--------- src/doplarr/overseerr.clj | 7 +------ src/doplarr/radarr.clj | 10 +++++++++ src/doplarr/sonarr.clj | 16 +++++++++++++++ 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/doplarr/interaction_state_machine.clj b/src/doplarr/interaction_state_machine.clj index 773250d..1a84f69 100644 --- a/src/doplarr/interaction_state_machine.clj +++ b/src/doplarr/interaction_state_machine.clj @@ -22,21 +22,26 @@ :direct {:series sonarr/quality-profiles :movie radarr/quality-profiles}}) -(def process-selection-fn {:overseerr ovsr/post-process-selection - :direct #(a/go (identity %))}) +(def process-selection-fn {:overseerr {:series ovsr/post-process-selection + :movie ovsr/post-process-selection} + :direct {:series sonarr/post-process-series + :movie (fn [movie] (a/go movie))}}) (def request-selection-fn {:overseerr ovsr/selection-to-request :direct identity}) +(def account-id-fn {:overseerr #(a/go ((a/> (a/> (a/ {:mediaType (:mediaType selection) :mediaId (:id selection) diff --git a/src/doplarr/radarr.clj b/src/doplarr/radarr.clj index c1140da..3d5907b 100644 --- a/src/doplarr/radarr.clj +++ b/src/doplarr/radarr.clj @@ -52,3 +52,13 @@ :addOptions {:searchForMovie true}}) :content-type :json})) nil) + +(defn movie-status [movie & _] + (cond + (and (:hasFile movie) + (:isAvailable movie) + (:monitored movie)) :available + (and (not (:hasFile movie)) + (:isAvailable movie) + (:monitored movie)) :processing + :else nil)) diff --git a/src/doplarr/sonarr.clj b/src/doplarr/sonarr.clj index 6579571..9886e17 100644 --- a/src/doplarr/sonarr.clj +++ b/src/doplarr/sonarr.clj @@ -110,3 +110,19 @@ (if (= -1 season) (request-all series profile-id) (request-season series season profile-id))) + +(defn post-process-series [series] + (a/go + (if-let [id (:id series)] + (merge series (:body (a/> (:seasons series) + (filter (comp (partial = season) :seasonNumber)) + first)] + (when-let [stats (:statistics ssn)] + (when (:monitored ssn) + (cond + (> 100.0 (:percentOfEpisodes stats)) :processing + :else :available))))) From 7c74f8c06e8e04b5395659dba33cf11c97d8f665 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Sun, 19 Sep 2021 13:16:42 -0700 Subject: [PATCH 16/36] Added FAQ to readme and switched adoptopenjdk to adoptium --- .gitignore | 1 + README.md | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2ad4888..ff02445 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ pom.xml.asc .nrepl-port .cpcache/ .lsp/ +.clj-kondo/ diff --git a/README.md b/README.md index 6d5e948..d008015 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,13 @@ ### Caveats I wanted a clean app for the sole purpose of requesting movies/TV shows. -I personally didn't need Siri or Ombi integration - so those are missing in this -bot. If you need Ombi support, I suggest you check out Overseerr instead. +I personally didn't need Siri integration, support for old API versions, Ombi, +etc., so those features are missing here. +If you need Ombi support (for managing many people requesting), I suggest you check out Overseerr instead. There is only a boolean permission (role gated) for who has access to the bot, nothing fancy. -If any of these don't suit your fancy, check out [Requestrr](https://github.com/darkalfx/requestrr) +If any of these don't suit your fancy, check out +[Requestrr](https://github.com/darkalfx/requestrr) ### Screenshots @@ -33,11 +35,27 @@ If any of these don't suit your fancy, check out [Requestrr](https://github.com/ +### FAQ + +#### Will you support Lidarr/Readarr/\*arr + +Not yet. The idea is that one can work directly with the collection managers or +work through a request manager (Overseerr). As Overseerr doesn't support +collections managers other than radarr/sonarr and I want feature-parity, those +other managers will be left out until Overseerr supports them. + +#### Why are the commands greyed out? + +Due to how slash command permissions work in Discord, every user that intends to +use the bot must have the assigned role you created. That _includes_ the server +owner/admins. Make sure that you assigned the role to yourself and the role ID +you copied is correct. + ## Setup ### Java -If you are running without Docker, you need to have at least Java 11 installed, such as [adoptopenjdk](https://adoptopenjdk.net/) +If you are running without Docker, you need to have at least Java 11 installed, such as [adoptium](https://adoptium.net/) ### Discord From 3c322726b264cbca25e3fa7931480ed95c368d41 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Sun, 19 Sep 2021 16:21:35 -0700 Subject: [PATCH 17/36] Fixed bug in request transform fun kwargs --- src/doplarr/interaction_state_machine.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doplarr/interaction_state_machine.clj b/src/doplarr/interaction_state_machine.clj index 1a84f69..9677036 100644 --- a/src/doplarr/interaction_state_machine.clj +++ b/src/doplarr/interaction_state_machine.clj @@ -28,7 +28,7 @@ :movie (fn [movie] (a/go movie))}}) (def request-selection-fn {:overseerr ovsr/selection-to-request - :direct identity}) + :direct (fn [selection & _] selection)}) (def account-id-fn {:overseerr #(a/go ((a/ Date: Tue, 21 Sep 2021 10:50:05 -0700 Subject: [PATCH 18/36] Typo and take on account id --- src/doplarr/interaction_state_machine.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/doplarr/interaction_state_machine.clj b/src/doplarr/interaction_state_machine.clj index 9677036..894afb8 100644 --- a/src/doplarr/interaction_state_machine.clj +++ b/src/doplarr/interaction_state_machine.clj @@ -96,7 +96,7 @@ (a/go (let [{:keys [token selection season profile request-type profile-id is4k]} (get @discord/cache uuid) user-id (:user-id interaction) - backend-id ((account-id-fn @backend) user-id)] + backend-id (a/ (- now last-modified) discord/channel-timeout) ; Update interaction with timeout message - (discord/update-interaction-response token (discord/content-response "Request times out, please try again.")) + (discord/update-interaction-response token (discord/content-response "Request timed out, please try again.")) ; Move through the state machine to update cache side effecting new components (do (swap! discord/cache assoc-in [uuid :last-modified] now) From ecdacdf5bd3d5d95722fbe85beed1b9c5863e8e1 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 10:58:39 -0700 Subject: [PATCH 19/36] Return channels from radarr/sonarr - not nil --- src/doplarr/radarr.clj | 3 +-- src/doplarr/sonarr.clj | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/doplarr/radarr.clj b/src/doplarr/radarr.clj index 3d5907b..ae35c4c 100644 --- a/src/doplarr/radarr.clj +++ b/src/doplarr/radarr.clj @@ -50,8 +50,7 @@ :minimumAvailability "announced" :rootFolderPath (a/ Date: Tue, 21 Sep 2021 13:16:21 -0700 Subject: [PATCH 20/36] Added fn to check for partial seasons --- src/doplarr/overseerr.clj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index 7d14720..bca5fe1 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -128,3 +128,10 @@ :content-type :json :headers {"X-API-User" (str ovsr-id)}})) (then (constantly nil))))) + +(defn partial-seasons? [] + (a/go + (->> (a/> (:body %) + :partialRequestsEnabled)) + (else utils/fatal-error)))) From ae36e43d1e0e341099fd7c75d055c808b828d2c4 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 13:30:09 -0700 Subject: [PATCH 21/36] Finished transitioning radarr/sonarr to flow --- src/doplarr/radarr.clj | 45 ++++++++++++++++++-------------------- src/doplarr/sonarr.clj | 49 +++++++++++++++++++++--------------------- 2 files changed, 46 insertions(+), 48 deletions(-) diff --git a/src/doplarr/radarr.clj b/src/doplarr/radarr.clj index ae35c4c..ab5a66a 100644 --- a/src/doplarr/radarr.clj +++ b/src/doplarr/radarr.clj @@ -2,6 +2,7 @@ (:require [config.core :refer [env]] [doplarr.arr-utils :as utils] + [fmnoise.flow :as flow :refer [then else]] [clojure.core.async :as a])) (def base-url (delay (str (:radarr-url env) "/api/v3"))) @@ -22,35 +23,31 @@ @api-key params)) -(defn search [search-term] - (let [chan (a/promise-chan)] - (a/pipeline - 1 - chan - (map :body) - (GET "/movie/lookup" {:query-params {:term search-term}})) - chan)) +(defn search [term] + (a/go + (->> (a/> (a/> (:body %) + (map utils/quality-profile-data))) + (else utils/fatal-error)))) (defn request [movie & {:keys [profile-id]}] (a/go - (POST - "/movie" - {:form-params (merge movie - {:qualityProfileId profile-id - :monitored true - :minimumAvailability "announced" - :rootFolderPath (a/> (a/> (a/> (a/> (:body %) + (map utils/quality-profile-data))) + (else utils/fatal-error)))) (defn request-options [profile-id] (a/go @@ -74,10 +70,11 @@ (s/terminal-val profile-id)]) series) (merge series (a/> (a/> (a/> (a/> (:body %) + (merge series))) + (else utils/fatal-error)) series))) (defn season-status [series & {:keys [season]}] From 4b3e73e57297eba4066239732ed97115fb095111 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 13:52:29 -0700 Subject: [PATCH 22/36] Bypass season selection on overseerr parial season configuration --- src/doplarr/interaction_state_machine.clj | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/doplarr/interaction_state_machine.clj b/src/doplarr/interaction_state_machine.clj index 894afb8..c3dcee3 100644 --- a/src/doplarr/interaction_state_machine.clj +++ b/src/doplarr/interaction_state_machine.clj @@ -70,7 +70,13 @@ (into [])) selection (a/ Date: Tue, 21 Sep 2021 15:22:42 -0700 Subject: [PATCH 23/36] Fixes #16 --- src/doplarr/interaction_state_machine.clj | 1 + src/doplarr/radarr.clj | 5 ++- src/doplarr/sonarr.clj | 46 ++++++++++++++++------- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/doplarr/interaction_state_machine.clj b/src/doplarr/interaction_state_machine.clj index c3dcee3..95aad12 100644 --- a/src/doplarr/interaction_state_machine.clj +++ b/src/doplarr/interaction_state_machine.clj @@ -111,6 +111,7 @@ :available (discord/update-interaction-response token (discord/content-response "This is already available!")) (->> (a/> (a/> (a/> (a/> (a/> (a/> (a/> (a/ Date: Tue, 21 Sep 2021 15:52:08 -0700 Subject: [PATCH 24/36] Watches all episodes in a new season of an existing show --- src/doplarr/sonarr.clj | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/doplarr/sonarr.clj b/src/doplarr/sonarr.clj index 38dc5f1..64f7f52 100644 --- a/src/doplarr/sonarr.clj +++ b/src/doplarr/sonarr.clj @@ -103,8 +103,11 @@ [:seasons s/ALL (comp (partial = season) :seasonNumber) - :monitored - (s/terminal-val true)] + (s/multi-path + [:monitored + (s/terminal-val true)] + [:statistics + (s/terminal #(assoc % :episodeCount (:totalEpisodeCount %)))])] [:profileId (s/terminal-val profile-id)]) series) From 7ac5d9bf68b39941ddf2d481f8e568fc7a5e9ffb Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 16:09:32 -0700 Subject: [PATCH 25/36] Fixed NPE on season-status --- src/doplarr/overseerr.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index bca5fe1..f348e5b 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -95,7 +95,7 @@ (defn season-status [selection & {:keys [season is4k]}] (when-let [info (:mediaInfo selection)] - (when-let [[& seasons] (:seasons info)] + (when-let [seasons (seq (:seasons info))] (status (dec ((if is4k :status4k :status) (nth seasons (dec season)))))))) (defn movie-status [selection & {:keys [is4k]}] From 6b6b9c96b6057d5a3b65c98d1c27b185bd2843a9 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 17:25:14 -0700 Subject: [PATCH 26/36] Switching away from depstar to vanilla tools.build --- .github/workflows/main.yml | 4 ++-- Dockerfile | 2 +- build/build.clj | 31 +++++++++++++++++-------------- deps.edn | 5 ++--- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ed8e24a..1c57b08 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: cli: latest - name: Build bot - run: clojure -T:build uberjar + run: clojure -T:build uber shell: bash - name: Release tagged version @@ -37,7 +37,7 @@ jobs: with: token: ${{ secrets.RELEASE_TOKEN }} files: | - target/Doplarr.jar + target/doplarr.jar config.edn - name: Log in to the Container registry diff --git a/Dockerfile b/Dockerfile index 70dfa09..d124265 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN \ COPY . /app ENTRYPOINT ["/sbin/tini", "--"] -CMD ["java","-jar","/app/target/Doplarr.jar"] +CMD ["java","-jar","/app/target/doplarr.jar"] LABEL "maintainer"="Kiran Shila " LABEL "org.opencontainers.image.source"="https://github.com/kiranshila/Doplarr" diff --git a/build/build.clj b/build/build.clj index 270198e..24592b8 100644 --- a/build/build.clj +++ b/build/build.clj @@ -1,17 +1,20 @@ (ns build (:require - [clojure.tools.build.api :as b] - [hf.depstar :as depstar])) + [clojure.tools.build.api :as b])) -(defn uberjar [params] - (b/delete {:path "target"}) - (-> (merge {:jar "Doplarr.jar" - :aot true - :sync-pom true - :target-dir "target" - :main-class "doplarr.core"} - params) - (depstar/aot) - (depstar/pom) - (assoc :jar-type :uber) - (depstar/build))) +(def class-dir "target/classes") +(def basis (b/create-basis {:project "deps.edn"})) +(def uber-file (str "target/doplarr.jar")) + +(defn clean [_] + (b/delete {:path "target"})) + +(defn uber [_] + (clean nil) + (b/compile-clj {:basis basis + :src-dirs ["src"] + :class-dir class-dir}) + (b/uber {:class-dir class-dir + :uber-file uber-file + :main 'doplarr.core + :basis basis})) diff --git a/deps.edn b/deps.edn index 3c286fe..4dc37d9 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src" "resources"] - :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha1"} + :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha2"} org.clojure/core.cache {:mvn/version "1.0.217"} yogthos/config {:mvn/version "1.1.8"} com.rpl/specter {:mvn/version "1.1.3"} @@ -14,6 +14,5 @@ :jvm-opts ["-Dconfig=config.edn"] :aliases {:build {:extra-paths ["build"] - :deps {io.github.clojure/tools.build {:git/tag "v0.1.6" :git/sha "5636e61"} - com.github.seancorfield/depstar {:mvn/version "2.1.278"}} + :deps {io.github.clojure/tools.build {:git/tag "v0.5.1" :git/sha "21da7d4"}} :ns-default build}}} From e31a02916e35f3cd12666046fd90036034efff0b Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 17:51:08 -0700 Subject: [PATCH 27/36] Uh going back to 1.11-alpha1 --- deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.edn b/deps.edn index 4dc37d9..29cfd4b 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src" "resources"] - :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha2"} + :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha1"} org.clojure/core.cache {:mvn/version "1.0.217"} yogthos/config {:mvn/version "1.1.8"} com.rpl/specter {:mvn/version "1.1.3"} From d6eff5d95c96a03a614fb93d079b899471b62e3e Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 18:03:04 -0700 Subject: [PATCH 28/36] Back to alpha2, specify CLI version for github build --- .github/workflows/main.yml | 2 +- deps.edn | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1c57b08..14a8fe9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: - name: Install clojure tools uses: DeLaGuardo/setup-clojure@3.5 with: - cli: latest + cli: 1.10.3.981 - name: Build bot run: clojure -T:build uber diff --git a/deps.edn b/deps.edn index 29cfd4b..4dc37d9 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src" "resources"] - :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha1"} + :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha2"} org.clojure/core.cache {:mvn/version "1.0.217"} yogthos/config {:mvn/version "1.1.8"} com.rpl/specter {:mvn/version "1.1.3"} From f895303db8317531db6e184bc57260b7305a133d Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 18:04:36 -0700 Subject: [PATCH 29/36] Match CLI version from my machine --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14a8fe9..5f6733a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: - name: Install clojure tools uses: DeLaGuardo/setup-clojure@3.5 with: - cli: 1.10.3.981 + cli: 1.10.3.943 - name: Build bot run: clojure -T:build uber From b41266f969ecd137efaec0651facd00df00c0f42 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 18:22:32 -0700 Subject: [PATCH 30/36] Switch to Java 16, make docker context contain only jar --- .dockerignore | 6 ------ .github/workflows/main.yml | 10 ++++++---- Dockerfile => docker/Dockerfile | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) delete mode 100644 .dockerignore rename Dockerfile => docker/Dockerfile (88%) diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index a523df9..0000000 --- a/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -.github/ -logos/ -screenshots/ -.gitignore -LICENSE -README.md diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5f6733a..9eae5d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,16 +20,18 @@ jobs: uses: actions/setup-java@v2 with: distribution: "adopt" - java-version: "11" + java-version: "16" - name: Install clojure tools uses: DeLaGuardo/setup-clojure@3.5 with: cli: 1.10.3.943 - - name: Build bot - run: clojure -T:build uber + - name: Build bot and copy out jar for docker shell: bash + run: | + clojure -T:build uber + cp target/doplarr.jar docker - name: Release tagged version uses: softprops/action-gh-release@v1 @@ -56,7 +58,7 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v2 with: - context: . + context: docker push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/docker/Dockerfile similarity index 88% rename from Dockerfile rename to docker/Dockerfile index d124265..a6adf34 100644 --- a/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM adoptopenjdk/openjdk11:alpine-jre +FROM adoptopenjdk/openjdk16:alpine-jre WORKDIR /app From 7e25a5a493e83ba238eb87f8f109387e019221ed Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 18:25:54 -0700 Subject: [PATCH 31/36] Change cmd path for dockerfile --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a6adf34..2e4e308 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,7 +10,7 @@ RUN \ COPY . /app ENTRYPOINT ["/sbin/tini", "--"] -CMD ["java","-jar","/app/target/doplarr.jar"] +CMD ["java","-jar","/app/doplarr.jar"] LABEL "maintainer"="Kiran Shila " LABEL "org.opencontainers.image.source"="https://github.com/kiranshila/Doplarr" From 191d45d6aae7fdf8e7467a515f087460eea0cc39 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 18:31:46 -0700 Subject: [PATCH 32/36] Bump discljord to 1.3.0-snapshot --- deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.edn b/deps.edn index 4dc37d9..7cd610e 100644 --- a/deps.edn +++ b/deps.edn @@ -5,7 +5,7 @@ yogthos/config {:mvn/version "1.1.8"} com.rpl/specter {:mvn/version "1.1.3"} ch.qos.logback/logback-classic {:mvn/version "1.2.5"} - org.suskalo/discljord {:git/url "https://github.com/IGJoshua/discljord" :sha "a417b0d543c68820ce0633b31d7c98c6b328c857"} + org.suskalo/discljord {:mvn/version "1.3.0-SNAPSHOT"} org.clojure/core.async {:mvn/version "1.3.618"} cheshire/cheshire {:mvn/version "5.10.1"} fmnoise/flow {:mvn/version "4.1.0"} From f76e08e82035512e1561eccdaacab4983c441452 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Tue, 21 Sep 2021 18:39:58 -0700 Subject: [PATCH 33/36] Switch to sean's build tool --- .gitignore | 1 + build/build.clj | 20 ++++---------------- deps.edn | 3 ++- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index ff02445..8284308 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ pom.xml.asc .cpcache/ .lsp/ .clj-kondo/ +target/ diff --git a/build/build.clj b/build/build.clj index 24592b8..139e1ea 100644 --- a/build/build.clj +++ b/build/build.clj @@ -1,20 +1,8 @@ (ns build (:require - [clojure.tools.build.api :as b])) - -(def class-dir "target/classes") -(def basis (b/create-basis {:project "deps.edn"})) -(def uber-file (str "target/doplarr.jar")) - -(defn clean [_] - (b/delete {:path "target"})) + [org.corfield.build :as bb])) (defn uber [_] - (clean nil) - (b/compile-clj {:basis basis - :src-dirs ["src"] - :class-dir class-dir}) - (b/uber {:class-dir class-dir - :uber-file uber-file - :main 'doplarr.core - :basis basis})) + (bb/clean nil) + (bb/uber {:uber-file "target/doplarr.jar" + :main 'doplarr.core})) diff --git a/deps.edn b/deps.edn index 7cd610e..03caeba 100644 --- a/deps.edn +++ b/deps.edn @@ -14,5 +14,6 @@ :jvm-opts ["-Dconfig=config.edn"] :aliases {:build {:extra-paths ["build"] - :deps {io.github.clojure/tools.build {:git/tag "v0.5.1" :git/sha "21da7d4"}} + :deps {io.github.seancorfield/build-clj + {:git/tag "v0.3.1" :git/sha "996ddfa"}} :ns-default build}}} From 33a987055f204f9556eff4997a2000176724f9cb Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Wed, 22 Sep 2021 11:34:36 -0700 Subject: [PATCH 34/36] Back to alpha1, too many error messages confuses users --- deps.edn | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps.edn b/deps.edn index 03caeba..175a3de 100644 --- a/deps.edn +++ b/deps.edn @@ -1,6 +1,6 @@ {:paths ["src" "resources"] - :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha2"} + :deps {org.clojure/clojure {:mvn/version "1.11.0-alpha1"} org.clojure/core.cache {:mvn/version "1.0.217"} yogthos/config {:mvn/version "1.1.8"} com.rpl/specter {:mvn/version "1.1.3"} From c0609dcef3ba0a4207bcea984746efc002e01947 Mon Sep 17 00:00:00 2001 From: Kiran Shila Date: Wed, 22 Sep 2021 11:35:08 -0700 Subject: [PATCH 35/36] Switch ISM fns to hash-quoted so live coding does what I expect --- src/doplarr/interaction_state_machine.clj | 36 +++++++++++------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/doplarr/interaction_state_machine.clj b/src/doplarr/interaction_state_machine.clj index 95aad12..a77c69a 100644 --- a/src/doplarr/interaction_state_machine.clj +++ b/src/doplarr/interaction_state_machine.clj @@ -12,36 +12,36 @@ (def backend (delay (config/backend))) -(def search-fn {:overseerr {:series ovsr/search-series - :movie ovsr/search-movie} - :direct {:series sonarr/search - :movie radarr/search}}) +(def search-fn {:overseerr {:series #'ovsr/search-series + :movie #'ovsr/search-movie} + :direct {:series #'sonarr/search + :movie #'radarr/search}}) (def profiles-fn {:overseerr {:series #(a/go nil) :movie #(a/go nil)} - :direct {:series sonarr/quality-profiles - :movie radarr/quality-profiles}}) + :direct {:series #'sonarr/quality-profiles + :movie #'radarr/quality-profiles}}) -(def process-selection-fn {:overseerr {:series ovsr/post-process-selection - :movie ovsr/post-process-selection} - :direct {:series sonarr/post-process-series +(def process-selection-fn {:overseerr {:series #'ovsr/post-process-selection + :movie #'ovsr/post-process-selection} + :direct {:series #'sonarr/post-process-series :movie (fn [movie] (a/go movie))}}) -(def request-selection-fn {:overseerr ovsr/selection-to-request +(def request-selection-fn {:overseerr #'ovsr/selection-to-request :direct (fn [selection & _] selection)}) (def account-id-fn {:overseerr #(a/go ((a/ Date: Wed, 22 Sep 2021 11:35:48 -0700 Subject: [PATCH 36/36] Fixed bug in series status on new series requests --- src/doplarr/overseerr.clj | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/doplarr/overseerr.clj b/src/doplarr/overseerr.clj index f348e5b..83307ac 100644 --- a/src/doplarr/overseerr.clj +++ b/src/doplarr/overseerr.clj @@ -93,10 +93,16 @@ (then :body) (else utils/fatal-error))))) -(defn season-status [selection & {:keys [season is4k]}] +(defn series-status [selection & {:keys [is4k]}] (when-let [info (:mediaInfo selection)] - (when-let [seasons (seq (:seasons info))] - (status (dec ((if is4k :status4k :status) (nth seasons (dec season)))))))) + (status (dec ((if is4k :status4k :status) info))))) + +(defn season-status [selection & {:keys [season is4k]}] + (when-let [ss (series-status selection :is4k is4k)] + (if (= ss :partially-available) + (when-let [seasons (seq (:seasons (:mediaInfo selection)))] + (status (dec ((if is4k :status4k :status) (nth seasons (dec season)))))) + ss))) (defn movie-status [selection & {:keys [is4k]}] (when-let [info (:mediaInfo selection)]