157 lines
7.4 KiB
Common Lisp
157 lines
7.4 KiB
Common Lisp
;;;; seanut.lisp
|
|
|
|
(in-package #:seanut)
|
|
|
|
(defun prompt-and-download (domain auth root opts)
|
|
"prompt the user with y-or-n-p and download ITEM"
|
|
(labels ((fetch-user-id ()
|
|
;; fetches the user id for the current user
|
|
;; based on our auth string
|
|
(gethash "Id" (json-request (format-url domain "Users/Me")
|
|
:auth auth)))
|
|
|
|
(generate-filename (item)
|
|
;; generates a filename for ITEM
|
|
(make-pathname :name
|
|
(str:replace-all "/" "-"
|
|
(format nil "~A.~A"
|
|
(gethash "Name" item)
|
|
(if (string= (gethash "Type" item) "Season")
|
|
(first (str:split "," (gethash "Container" item)
|
|
:omit-nulls t))
|
|
(pathname-type (gethash "Path" item)))))))
|
|
|
|
(maybe-create-item-directory (item &key force)
|
|
;; create a directory, if ITEM should be a directory
|
|
;; if FORCE is non-nil, we create a folder regardless
|
|
(if (or force (gethash "IsFolder" item))
|
|
(ensure-directories-exist
|
|
(uiop:ensure-directory-pathname
|
|
(make-pathname :name (gethash "Name" item))))
|
|
#P"./"))
|
|
|
|
(download (item)
|
|
;; downloads ITEM
|
|
(when (getf opts :verbose)
|
|
(format t "Downloading ~A~%" (generate-filename item)))
|
|
(download-media (generate-filename item)
|
|
(format-url domain "Items/~A/Download"
|
|
(gethash "Id" item))
|
|
auth))
|
|
|
|
(run-query-for-media-type (item)
|
|
;; gets the children for ITEM. checks the type
|
|
;; and runs different queries for different types
|
|
(json-request
|
|
(cond
|
|
;; Handle Fetching TV Episodes
|
|
((or (string= (gethash "Type" item) "Season")
|
|
(and (string= (gethash "Type" item) "Series")
|
|
(getf opts :season)))
|
|
(format-url domain "Shows/~A/Episodes?fields=Path~@[&seasonId=~A~]~@[&season=~A~]"
|
|
(or (gethash "SeriesId" item)
|
|
(gethash "Id" item))
|
|
(unless (getf opts :season)
|
|
(gethash "Id" item))
|
|
(getf opts :season)))
|
|
|
|
;; Default Search Query
|
|
(t (format-url domain "Items?userId=~A&fields=Path,ChildCount&parentId=~A"
|
|
(fetch-user-id)
|
|
(gethash "Id" item))))
|
|
:auth auth))
|
|
|
|
(download-item-p (item)
|
|
;; checks the ITEM type and checks if we should download it
|
|
;; or check for and download its children
|
|
(macrolet ((media-type-cond (t-clause &rest nil-types)
|
|
`(cond
|
|
,@(loop :for type :in nil-types
|
|
:collect `((string= (gethash "Type" item) ,type) nil))
|
|
(t ,t-clause))))
|
|
(media-type-cond (zerop (gethash "ChildCount" item 0))
|
|
"MusicAlbum" "BoxSet" "Playlist" "MusicGenre"
|
|
"MusicArtist" "Series")))
|
|
|
|
(download-item-or-children (item)
|
|
;; check if we should download the item based on media type or child count
|
|
(if (download-item-p item)
|
|
;; if we should, we create a directory for it, change into the directory
|
|
;; and download the item
|
|
(download item)
|
|
|
|
;; if we shouldn't then we get all the children for the item,
|
|
;; maybe create a new subdirectory, and finally recurse
|
|
(let ((children (gethash "Items" (run-query-for-media-type item))))
|
|
(map 'vector #'(lambda (child)
|
|
(uiop:with-current-directory ((maybe-create-item-directory child))
|
|
(download-item-or-children child)))
|
|
children)))))
|
|
|
|
(when (or (getf opts :assume-yes)
|
|
(y-or-n-p "Download \"~A (~A)~@[ Season ~A~]\""
|
|
(gethash "Name" root)
|
|
(gethash "ProductionYear" root)
|
|
(getf opts :season)))
|
|
|
|
;; CD into our output directory
|
|
(uiop:with-current-directory ((getf opts :output #P"./"))
|
|
|
|
;; create our folder for the "root" item
|
|
;; then CD into it, and start downloading
|
|
(uiop:with-current-directory ((ensure-directories-exist (format nil "~A (~A)~@[ Season ~A~]/"
|
|
(gethash "Name" root)
|
|
(gethash "ProductionYear" root)
|
|
(getf opts :season))))
|
|
(download-item-or-children root))))))
|
|
|
|
|
|
(defun main ()
|
|
"binary entry point"
|
|
(handle-user-abort
|
|
(multiple-value-bind (opts args) (get-opts)
|
|
;; --help
|
|
(when (or (getf opts :help)
|
|
(and (every #'null args)
|
|
(every #'null opts)))
|
|
(opts:describe :usage-of "seanut"
|
|
:args "DOMAIN MEDIA-NAME"
|
|
:suffix (format nil *command-line-brief*
|
|
*valid-media-types*))
|
|
(uiop:quit 0))
|
|
|
|
;; --version
|
|
(when (getf opts :version)
|
|
(quit-with-message 0 "seanut v~A" (seanut-version)))
|
|
|
|
;; --username/--password/--quick-connect/--token checking
|
|
(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"))
|
|
|
|
;; --media-type
|
|
(unless (getf opts :media-type)
|
|
(quit-with-message 1 "Please specify media type to download.~%~A ~{~A~^, ~}"
|
|
"Supported media types are:"
|
|
*valid-media-types*))
|
|
|
|
;; DOMAIN SEARCH-TERM checking
|
|
(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))
|
|
(loop :for item :across results
|
|
:do (prompt-and-download domain authorization item opts))
|
|
(quit-with-message 0 "No results found for \"~A\"" search-term)))))
|
|
|
|
(error (e)
|
|
(quit-with-message 1 "error:~%~A" e))))
|