reorganized things a bit
parent
303d2d242f
commit
a839ca60b0
@ -0,0 +1,54 @@
|
|||||||
|
;;; auth.lisp
|
||||||
|
|
||||||
|
(in-package :seanut)
|
||||||
|
|
||||||
|
(defun get-access-token (domain options)
|
||||||
|
(if (getf options :quick-connect-p)
|
||||||
|
|
||||||
|
;; go through the whole quick connect rigamarole
|
||||||
|
(quick-connect-dance domain)
|
||||||
|
|
||||||
|
;; authenticates the user via username and password
|
||||||
|
(gethash "AccessToken"
|
||||||
|
(json-request (format-url domain "Users/AuthenticateByName")
|
||||||
|
(generate-authorization "")
|
||||||
|
:method :post
|
||||||
|
:content `(("Username" . ,(getf options :username))
|
||||||
|
("Pw" . ,(getf options :password)))))))
|
||||||
|
|
||||||
|
(defun quick-connect-dance (domain)
|
||||||
|
(let* ((auth (generate-authorization ""))
|
||||||
|
(qc-session (handler-case
|
||||||
|
(json-request (format-url domain "QuickConnect/Initiate") auth)
|
||||||
|
(dex:http-request-unauthorized ()
|
||||||
|
(error "QuickConnect not enabled on this server.")))))
|
||||||
|
;; initiate quick connect session
|
||||||
|
;; display code to user
|
||||||
|
;; sleep 5 seconds
|
||||||
|
;; poll QuickConnect/Connect?secret=~A
|
||||||
|
;; until "Authenticated" is t or we've looped 20 times (~1min)
|
||||||
|
;; if we time out signal an error
|
||||||
|
;; else POST to Users/AuthenticateWithQuickConnect with "Secret"
|
||||||
|
;; return "AccessToken"
|
||||||
|
(format t "QuickConnect Code: ~A~%" (gethash "Code" qc-session))
|
||||||
|
(force-output)
|
||||||
|
(loop :with counter := 1
|
||||||
|
:with authed
|
||||||
|
|
||||||
|
:until (or authed (> counter 20))
|
||||||
|
:do
|
||||||
|
(sleep 5)
|
||||||
|
(let ((state (json-request (format-url domain "QuickConnect/Connect?secret=~A"
|
||||||
|
(gethash "Secret" qc-session))
|
||||||
|
auth)))
|
||||||
|
(setf authed (gethash "Authenticated" state)
|
||||||
|
counter (1+ counter)))
|
||||||
|
|
||||||
|
:finally
|
||||||
|
(when (> counter 20)
|
||||||
|
(error "QuickConnect session timed out.")))
|
||||||
|
(gethash "AccessToken"
|
||||||
|
(json-request (format-url domain "Users/AuthenticateWithQuickConnect") auth
|
||||||
|
:method :post
|
||||||
|
:content (jzon:stringify (alist-hash-table `(("Secret" . ,(gethash "Secret" qc-session)))))
|
||||||
|
:extra-headers '(("Content-Type" . "application/json"))))))
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
;;; command-line.lisp
|
||||||
|
|
||||||
|
(in-package :seanut)
|
||||||
|
|
||||||
|
(define-opts
|
||||||
|
(:name :help
|
||||||
|
:short #\h
|
||||||
|
:long "help"
|
||||||
|
:description "prints this help")
|
||||||
|
(:name :version
|
||||||
|
:short #\v
|
||||||
|
:long "version"
|
||||||
|
:description "prints the version")
|
||||||
|
(:name :assume-yes
|
||||||
|
:long "no-prompt"
|
||||||
|
:description "assumes yes for all download prompts")
|
||||||
|
(:name :quick-connect-p
|
||||||
|
:short #\q
|
||||||
|
:long "quick-connect"
|
||||||
|
:description "alternative login method to providing username/password - times out after ~1min")
|
||||||
|
(:name :output
|
||||||
|
:short #\o
|
||||||
|
:long "output"
|
||||||
|
:meta-var "DIR"
|
||||||
|
:arg-parser #'uiop:ensure-directory-pathname
|
||||||
|
:description "location to save downloaded media")
|
||||||
|
(:name :media-type
|
||||||
|
:short #\m
|
||||||
|
:long "media-type"
|
||||||
|
:meta-var "TYPE"
|
||||||
|
:arg-parser #'validate-media-type
|
||||||
|
:description "media type to base our query on")
|
||||||
|
(:name :username
|
||||||
|
:short #\u
|
||||||
|
:long "username"
|
||||||
|
:meta-var "USERNAME"
|
||||||
|
:arg-parser #'identity
|
||||||
|
:description "username for the jellyfin server")
|
||||||
|
(:name :password
|
||||||
|
:short #\p
|
||||||
|
:long "password"
|
||||||
|
:meta-var "PASSOWRD"
|
||||||
|
:arg-parser #'identity
|
||||||
|
:description "passowrd for the jellyfin server")
|
||||||
|
(:name :season-number
|
||||||
|
:short #\s
|
||||||
|
:long "season"
|
||||||
|
:meta-var "SEASON"
|
||||||
|
:arg-parser #'maybe-parse-integer
|
||||||
|
:description "specify specific season to download, if downloading a show"))
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
;;; web.lisp
|
||||||
|
|
||||||
|
(in-package :seanut)
|
||||||
|
|
||||||
|
(eval-when (:compile-toplevel)
|
||||||
|
(declaim (inline json-request format-url generate-authorization download-media)))
|
||||||
|
|
||||||
|
(defun generate-authorization (token)
|
||||||
|
"generates a properly formatted authorization header"
|
||||||
|
(format nil *authorization-format*
|
||||||
|
(seanut-version) (uiop:hostname) (md5-string (uiop:hostname))
|
||||||
|
(seanut-version) token))
|
||||||
|
|
||||||
|
(defun format-url (domain slug &rest args)
|
||||||
|
"formats DOMAIN into a url, ensures we include the url scheme
|
||||||
|
|
||||||
|
SLUG is a format-coded string that represents the path for the query
|
||||||
|
ARGS are the arguments for the SLUG format string"
|
||||||
|
(format nil "~:[https://~;~]~A/~A"
|
||||||
|
(uiop:string-prefix-p "https://" domain)
|
||||||
|
domain (apply #'format `(nil ,slug ,@args))))
|
||||||
|
|
||||||
|
(defun json-request (url auth &key (method :get) extra-headers content)
|
||||||
|
"makes a request to URL, using AUTH as the X-Emby-Authorization header and METHOD as the http method (defaults to get) and parses the returned value with jzon:parse
|
||||||
|
|
||||||
|
if EXTRA-HEADERS is non-nil, includes them in the headers alongside the X-Emby-Authorization one
|
||||||
|
if CONTENT is non-nil, passes that along to the request"
|
||||||
|
(parse (dex:request url :method method
|
||||||
|
:content content
|
||||||
|
:headers `(("X-Emby-Authorization" . ,auth)
|
||||||
|
,@extra-headers))))
|
||||||
|
|
||||||
|
(defun run-search-query (domain auth type name)
|
||||||
|
(gethash "Items"
|
||||||
|
(json-request (format-url domain "Items?fields=Path&includeItemTypes=~A&recursive=true&searchTerm=~A"
|
||||||
|
type name)
|
||||||
|
auth)))
|
||||||
|
|
||||||
|
(defun download-media (path url auth)
|
||||||
|
"downloads the media at URL, using HEADER as the authorization header.
|
||||||
|
|
||||||
|
if DESTINATION is non-nil, dumps media into that directory, otherwise it uses CWD"
|
||||||
|
(dex:fetch url path
|
||||||
|
:if-exists nil
|
||||||
|
:headers `(("X-Emby-Authorization" . ,auth))))
|
||||||
Loading…
Reference in New Issue