From 96f6b242278c0b808e8d68789e29dd9f5850dcd8 Mon Sep 17 00:00:00 2001 From: "a. fox" Date: Fri, 17 Nov 2023 13:56:56 -0500 Subject: [PATCH] fixed error fetching results after authentication updated authorization format string ensured we url encode our authorization values removed ".local" from hostnames if it's present added str package as dependency fixed using deprecated authorization header --- package.lisp | 6 +-- seanut.asd | 2 +- seanut.lisp | 126 +++++++++++++++++++++++++++++++-------------------- web.lisp | 18 ++++++-- 4 files changed, 93 insertions(+), 59 deletions(-) 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))))