Spec-ing out everything and codifying interfaces

WIP
This commit is contained in:
Kiran Shila
2021-09-25 18:24:47 -07:00
parent b65a0bc67c
commit 3fff7f624c
11 changed files with 174 additions and 111 deletions

View File

@@ -5,11 +5,14 @@
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 {:mvn/version "1.3.0-SNAPSHOT"}
org.suskalo/discljord {:mvn/version "1.3.0-RC2"}
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"}}
hato/hato {:mvn/version "0.8.2"}
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"}
org.clojure/alpha.spec {:git/url "https://github.com/clojure/spec-alpha2.git"
:sha "c087ded910b3532a938b37e853df79fc3b9c48c1"}}
:jvm-opts ["-Dconfig=config.edn"]

View File

@@ -1,45 +0,0 @@
(ns doplarr.arr-utils
(: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 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
(deep-merge
{:method method
:url url
:as :json
:async? true
:headers {"X-API-Key" key}}
params)
put-close
put-close)
chan))
(defn rootfolder [base-url key]
(a/go
(->> (a/<! (http-request
:get
(str base-url "/rootfolder")
key))
(then (comp :path first :body))
(else fatal-error))))
(defn quality-profile-data [profile]
{:name (:name profile)
:id (:id profile)})

View File

@@ -1,4 +1,4 @@
(ns doplarr.overseerr
(ns doplarr.backends.overseerr.impl
(:require
[com.rpl.specter :as s]
[clojure.core.async :as a]

View File

@@ -0,0 +1,74 @@
(ns doplarr.backends.radarr.impl
(:require
[config.core :refer [env]]
[doplarr.utils :as utils]
[doplarr.backends.radarr.specs :as specs]
[clojure.core.async :as a]
[clojure.spec.alpha :as spec]
[doplarr.backends.specs :as bs]))
(def base-url (delay (str (:radarr-url env) "/api/v3")))
(def api-key (delay (:radarr-api env)))
(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))
(def rootfolder (delay (a/<!! (utils/request-and-process-body GET #(get (first %) "path") "/rootfolder"))))
(defn search [term]
(utils/request-and-process-body
GET
#(into [] (map utils/process-search-result %))
"/movie/lookup"
{:query-params {:term term}}))
(defn quality-profiles []
(utils/request-and-process-body
GET
#(into [] (map utils/process-quality-profile %))
"/qualityProfile"))
(defn status [movie & _]
(cond
(and (:has-file movie)
(:is-available movie)
(:monitored movie)) :available
(and (not (:has-file movie))
(:is-available movie)
(:monitored movie)) :processing
:else nil))
(defn request-payload [result quality-profile-id]
(merge result
{:quality-profile-id quality-profile-id
:monitored true
:rootFolderPath @rootfolder
:add-options {:search-for-movie true}}))
(spec/fdef request-payload :args (spec/cat :arg ::bs/result :arg ::bs/quality-profile-id))
(defn request [payload]
(utils/request-and-process-body
POST
(constantly nil)
"/movie"
{:form-params (utils/to-camel payload)
:content-type :json}))
(spec/fdef request :args (spec/cat :arg ::specs/payload))
(def payload {:title "Making 'The Matrix'"
:tmdb-id 684431
:quality-profile-id 1
:root-folder-path (a/<!! (rootfolder))})

View File

@@ -0,0 +1,9 @@
(ns doplarr.backends.radarr.specs
(:require
[clojure.spec.alpha :as spec]
[doplarr.backends.specs :as bs]))
(spec/def ::payload (spec/keys :req-un [::bs/title
::bs/tmdb-id
::bs/root-folder-path
::bs/quality-profile-id]))

View File

@@ -1,10 +1,10 @@
(ns doplarr.sonarr
(ns doplarr.backends.sonarr.impl
(:require
[com.rpl.specter :as s]
[clojure.core.async :as a]
[config.core :refer [env]]
[fmnoise.flow :as flow :refer [then else]]
[doplarr.arr-utils :as utils]))
[doplarr.utils :as utils]))
(def base-url (delay (str (:sonarr-url env) "/api/v3")))
(def api-key (delay (:sonarr-api env)))

View File

@@ -0,0 +1,2 @@
(ns doplarr.backends.sonarr.specs
(:require [clojure.spec.alpha :as spec]))

View File

@@ -0,0 +1,14 @@
(ns doplarr.backends.specs
(:require [clojure.spec.alpha :as spec]))
(spec/def ::title string?)
(spec/def ::year int?)
(spec/def ::id int?)
(spec/def ::tmdb-id int?)
(spec/def ::tvdb-id int?)
(spec/def ::root-folder-path string?)
(spec/def ::quality-profile-id (spec/and int? pos?))
(spec/def ::result (spec/keys
:req-un [::title ::year]
:opt-un [::id ::tvdb-id ::tmdb-id]))

View File

@@ -1,61 +0,0 @@
(ns doplarr.radarr
(: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")))
(def api-key (delay (:radarr-api env)))
(defn rootfolder [] (utils/rootfolder @base-url @api-key))
(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 search [term]
(a/go
(->> (a/<! (GET "/movie/lookup" {:query-params {:term term}}))
(then :body)
(else utils/fatal-error))))
(defn quality-profiles []
(a/go
(->> (a/<! (GET "/qualityProfile"))
(then #(->> (:body %)
(map utils/quality-profile-data)))
(else utils/fatal-error))))
(defn request [movie & {:keys [profile-id]}]
(a/go
(->> (a/<! (POST
"/movie"
{:form-params (merge movie
{:qualityProfileId profile-id
:monitored true
:minimumAvailability "announced"
:rootFolderPath (a/<! (rootfolder))
:addOptions {:searchForMovie true}})
:content-type :json}))
(then (constantly 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))

9
src/doplarr/specs.clj Normal file
View File

@@ -0,0 +1,9 @@
(ns doplarr.specs
(:require
[clojure.spec.alpha :as spec]))
(spec/def ::result-name string?)
(spec/def ::result-year int?)
(spec/def ::result-id int?)
(spec/def ::search-result (spec/keys :req []))

58
src/doplarr/utils.clj Normal file
View File

@@ -0,0 +1,58 @@
(ns doplarr.utils
(:require
[clojure.tools.logging :as log]
[camel-snake-kebab.core :as csk]
[camel-snake-kebab.extras :as cske]
[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 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
(deep-merge
{:method method
:url url
:as :json-string-keys
:coerce :always
:async? true
:headers {"X-API-Key" key}}
params)
put-close
put-close)
chan))
(defn from-camel [m]
(cske/transform-keys csk/->kebab-case-keyword m))
(defn to-camel [m]
(cske/transform-keys csk/->camelCaseString m))
(defn process-search-result [result]
(-> result
(select-keys ["title" "year" "id" "tvdbId" "tmdbId"])
from-camel))
(defn process-quality-profile [profile]
(->> (select-keys profile ["id" "name"])
from-camel))
(defn request-and-process-body [request-fn process-fn & request-args]
(a/go
(->> (a/<! (apply request-fn request-args))
(then #(process-fn (:body %)))
(else #(fatal-error (ex-info (str "Error on request from " request-fn)
{:args request-args
:exception (ex-data %)}))))))