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