You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
7.0 KiB
Common Lisp
142 lines
7.0 KiB
Common Lisp
;;;; seanut.lisp
|
|
|
|
(in-package #:seanut)
|
|
|
|
(defun prompt-and-download (domain auth root opts &optional assume-yes)
|
|
"prompt the user with y-or-n-p and download ITEM"
|
|
(labels ((fetch-user-id ()
|
|
(gethash "Id" (json-request (format-url domain "Users/Me")
|
|
:auth auth)))
|
|
|
|
(generate-filename (item)
|
|
(make-pathname :name
|
|
(str:replace-all "/" "-"
|
|
(format nil "~A.~A"
|
|
(gethash "Name" item)
|
|
(if (string= (gethash "Type" item) "Season")
|
|
(subseq (gethash "Container" item) 0
|
|
(search "," (gethash "Container" item)))
|
|
(pathname-type (gethash "Path" item)))))))
|
|
|
|
(generate-root-name (&optional add-trailing)
|
|
(format nil "~A (~A)~@[ Season ~A~]~@[/~]"
|
|
(gethash "Name" root)
|
|
(gethash "ProductionYear" root)
|
|
(getf opts :season-number)
|
|
add-trailing))
|
|
|
|
(create-directory (dir)
|
|
(ensure-directories-exist (uiop:ensure-directory-pathname (make-pathname :name dir))))
|
|
|
|
(download-item-or-children (item)
|
|
;; PARENTS is a list of all parent names with FIRST being
|
|
;; the oldest grandparent (for building complete download path)
|
|
(let ((children (gethash "Items"
|
|
(if (string= (gethash "Type" item) "Season")
|
|
(json-request (format-url domain "Shows/~A/Episodes?fields=Path&seasonId=~A~@[&season=~A~]"
|
|
(gethash "SeriesId" item)
|
|
(gethash "Id" item)
|
|
(getf opts :season-number))
|
|
:auth auth)
|
|
(json-request (format-url domain "Items?userId=~A&fields=Path,ChildCount&parentId=~A"
|
|
(fetch-user-id)
|
|
(gethash "Id" item))
|
|
:auth auth)))))
|
|
;; DELETE: debug prints lmao
|
|
(loop :for child :across children
|
|
:do (format t "Name: ~A, Id: ~A~%"
|
|
(gethash "Name" child)
|
|
(gethash "Id" child)))
|
|
|
|
|
|
(loop :for child :across children
|
|
:if (zerop (gethash "ChildCount" child 0))
|
|
:do
|
|
;; download single file
|
|
(download-media (generate-filename child)
|
|
(format-url domain "Items/~A/Download"
|
|
(gethash "Id" child))
|
|
auth)
|
|
|
|
:else
|
|
:do
|
|
;; if the item has children we need to download them.
|
|
;; to accomplish this we get the list of children
|
|
;; and loop over them, recursing for each one
|
|
;; ensure that a directory exists for Parent
|
|
;; then recurse with children
|
|
(format t "Creating child directory in ~A and recursing: ~A~%"
|
|
(uiop:getcwd) (gethash "Name" child))
|
|
(uiop:with-current-directory ((create-directory (gethash "Name" child)))
|
|
(download-item-or-children child))))))
|
|
|
|
(format t "Name: ~A, Id: ~A~%"
|
|
(gethash "Name" root)
|
|
(gethash "Id" root))
|
|
(when (or assume-yes
|
|
(y-or-n-p "Download ~A" (generate-root-name)))
|
|
|
|
;; CD into our output directory
|
|
(uiop:with-current-directory ((getf opts :output #P"./"))
|
|
(let ((grandest-parent (format nil "~A (~A)~@[ Season ~A~]/"
|
|
(gethash "Name" root)
|
|
(gethash "ProductionYear" root)
|
|
(getf opts :season-number))))
|
|
|
|
;; create our folder for our current download
|
|
(ensure-directories-exist grandest-parent)
|
|
|
|
;; CD into it, then download the files
|
|
(uiop:with-current-directory (grandest-parent)
|
|
(download-item-or-children root)))))))
|
|
|
|
|
|
(defun main ()
|
|
"binary entry point"
|
|
(handle-user-abort
|
|
(multiple-value-bind (opts args) (get-opts)
|
|
(when (or (getf opts :help)
|
|
(and (every #'null args)
|
|
(every #'null opts)))
|
|
(opts:describe :usage-of "seanut"
|
|
:args "DOMAIN MEDIA-NAME")
|
|
(uiop:quit 0))
|
|
|
|
(when (getf opts :version)
|
|
(quit-with-message 0 "seanut v~A" (seanut-version)))
|
|
|
|
(unless (or (and (getf opts :username)
|
|
(getf opts :password))
|
|
(getf opts :quick-connect-p)
|
|
(getf opts :token))
|
|
(quit-with-message 1 "please provide an access token, username & password, or use quick connect"))
|
|
|
|
(unless (getf opts :media-type)
|
|
(quit-with-message 1 "Please specify media type to download.~%~A ~{~A~^, ~}"
|
|
"Supported media types are:"
|
|
*valid-media-types*))
|
|
|
|
(when (some #'null args)
|
|
(quit-with-message 1 "domain and/or media name not provided"))
|
|
|
|
(destructuring-bind (domain search-term) args
|
|
(let* ((authorization (or (and (getf opts :token) (generate-authorization (getf opts :token)))
|
|
(generate-authorization (get-access-token domain opts))))
|
|
(results (run-search-query domain authorization
|
|
(getf opts :media-type)
|
|
(url-encode search-term))))
|
|
(if (< 0 (length results))
|
|
;; FIXME: for some reason this access token is not "valid" enough to get
|
|
;; certain info? when we run the parentID search it craps out on us?
|
|
;; maybe its not something wrong with the token, but the auth string as a
|
|
;; whole? look into this more tomorrow
|
|
;;
|
|
;; after reading more
|
|
(loop :for item :across results
|
|
:do (prompt-and-download domain authorization
|
|
item (getf opts :assume-yes)))
|
|
(quit-with-message 0 "No results found for ~A" search-term)))))
|
|
|
|
(error (e)
|
|
(quit-with-message 1 "encountered error: ~A" e))))
|