diff --git a/package.lisp b/package.lisp index e1e8a0b..5a65c55 100644 --- a/package.lisp +++ b/package.lisp @@ -21,9 +21,9 @@ (in-package :seanut) -(defvar *authorization-format* - "MediaBrowser Client=\"Seanut v~A\", Device=\"~A\", DeviceId=\"~A\", Version=\"~A\", Token=\"~A\"") +(defparameter *authorization-format* + "MediaBrowser Client=\"~A\", Device=\"~A\", DeviceId=\"~A\", Version=\"~A\", Token=\"~A\"") -(defvar *valid-media-types* +(defparameter *valid-media-types* '("Book" "BoxSet" "Movie" "MusicAlbum" "MusicArtist" "MusicGenre" "Playlist" "Season" "Series")) diff --git a/seanut.asd b/seanut.asd index 2535e8d..a60a39f 100644 --- a/seanut.asd +++ b/seanut.asd @@ -8,7 +8,7 @@ :serial t :depends-on (#:dexador #:with-user-abort #:unix-opts #:com.inuoe.jzon #:babel #:ironclad #:quri - #:alexandria) + #:alexandria #:str) :components ((:file "package") (:file "util") (:file "web") diff --git a/seanut.lisp b/seanut.lisp index ac18f02..75f702e 100644 --- a/seanut.lisp +++ b/seanut.lisp @@ -2,67 +2,93 @@ (in-package #:seanut) -(defun prompt-and-download (domain auth item opts &optional assume-yes) +(defun prompt-and-download (domain auth root opts &optional assume-yes) "prompt the user with y-or-n-p and download ITEM" - ;; debug print statements lol - (format t "~A~%" auth) - (y-or-n-p) + (labels ((fetch-user-id () + (gethash "Id" (json-request (format-url domain "Users/Me") + :auth auth))) - - (labels ((generate-root-name (&optional add-trailing) + (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" item) - (gethash "ProductionYear" item) + (gethash "Name" root) + (gethash "ProductionYear" root) (getf opts :season-number) add-trailing)) - (ensure-download-dir (&optional new-path) - (ensure-directories-exist - (merge-pathnames (or new-path "") - (merge-pathnames (generate-root-name 'add-slash) - (getf opts :output #P"./"))))) + (create-directory (dir) + (ensure-directories-exist (uiop:ensure-directory-pathname (make-pathname :name dir)))) - (download-item-or-children (item &optional parents) + (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 (if (string= (gethash "Type" item) "Season") - (json-request (format-url domain "Shows/~A/Episodes?fields=Path~@[&season=~A~]" - (gethash "Id" item) - (getf opts :season-number)) - :auth auth) - (json-request (format-url domain "Items?fields=Path&parentId=~A" - (gethash "Id" item)) - :auth auth)))) - (if (zerop (length children)) - ;; download single file - ;; to get the extension type i think we may need to include - ;; "Path" field in the api request, then use that to get the extension - (download-media (format nil "~A~A~{~A~^/~}~A.~A" - (getf opts :output #P"./") - (generate-root-name 'trailing) - parents (gethash "Name" item) - (pathname-type (gethash "Path" item))) - (format-url domain "Items/~A/Download" - (gethash "Id" item)) - auth) + (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) - ;; 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 - (progn - (ensure-download-dir (format nil "~A~A~{~A/~}~A" - (getf opts :output #P"./") - (generate-root-name 'trailing) - parents (gethash "Name" item))) - (loop :for child :in children - :do (apply #'download-item-or-children - `(,child (,@parents ,(gethash "Name" item)))))))))) + :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))) - (ensure-download-dir) - (download-item-or-children item)))) + + ;; 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 () @@ -94,7 +120,7 @@ (quit-with-message 1 "domain and/or media name not provided")) (destructuring-bind (domain search-term) args - (let* ((authorization (or (getf opts :token) + (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) diff --git a/web.lisp b/web.lisp index a6ca38f..88c97b7 100644 --- a/web.lisp +++ b/web.lisp @@ -3,13 +3,21 @@ (in-package :seanut) (eval-when (:compile-toplevel) - (declaim (inline json-request format-url generate-authorization download-media))) + (declaim (inline json-request format-url generate-authorization download-media + format-hostname))) + +(defun format-hostname () + (if (uiop:string-suffix-p (uiop:hostname) ".local") + (subseq (uiop:hostname) 0 (- (length (uiop:hostname)) 6)) + (uiop:hostname))) (defun generate-authorization (&optional token) "generates a properly formatted authorization header" - (format nil *authorization-format* - (seanut-version) (uiop:hostname) (md5-string (uiop:hostname)) - (seanut-version) (or token ""))) + (apply #'format `(nil ,*authorization-format* + ,@(mapcar #'url-encode + (list (concatenate 'string "Seanut " (seanut-version)) + (format-hostname) (string-downcase (md5-string (format-hostname))) + (seanut-version) (or token "")))))) (defun format-url (domain slug &rest args) "formats DOMAIN into a url, ensures we include the url scheme @@ -46,4 +54,4 @@ can probably be removed and the request can be made in-line" 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)))) + :headers `(("Authorization" . ,auth))))