;;;; 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))))