seanut/seanut.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))))