first push
This commit is contained in:
commit
1e576d8443
|
@ -0,0 +1,16 @@
|
|||
define LISP_CMDS
|
||||
"(handler-case \
|
||||
(progn (ql:quickload :seanut) \
|
||||
(asdf:make :seanut)) \
|
||||
(error (e) \
|
||||
(format t \"~A~%\" e) \
|
||||
(uiop:quit 1)))"
|
||||
endef
|
||||
|
||||
.PHONY: clean all
|
||||
|
||||
all:
|
||||
ros --eval $(LISP_CMDS)
|
||||
|
||||
clean:
|
||||
rm -ri bin/
|
|
@ -0,0 +1,19 @@
|
|||
# seanut
|
||||
### _a. fox_
|
||||
|
||||
a command line utility to bulk download media from jellyfin servers (e.g., shows, albums, etc)
|
||||
|
||||
## Building
|
||||
|
||||
1. Install [roswell](https://github.com/roswell/roswell)
|
||||
2. `$ mkdir ~/common-lisp && git clone https://dev.focks.website/focks/seanut ~/common-lisp/seanut`
|
||||
3. `$ cd ~/common-lisp/seanut && make`
|
||||
|
||||
## Running
|
||||
|
||||
`$ ./seanut -t your_Cool&Token -m MusicAlbum -o ~/Downloads/Jellyfin/Media https://your.jellyfin.domain "My Cool Album"`
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
;;;; package.lisp
|
||||
|
||||
(defpackage #:seanut
|
||||
(:use #:cl #:with-user-abort)
|
||||
(:local-nicknames (:jzon :com.inuoe.jzon))
|
||||
|
||||
(:import-from :quri
|
||||
:url-encode
|
||||
:url-decode)
|
||||
(:import-from :babel
|
||||
:string-to-octets)
|
||||
(:import-from :ironclad
|
||||
:digest-sequence)
|
||||
(:import-from :unix-opts
|
||||
:get-opts
|
||||
:define-opts)
|
||||
(:import-from :com.inuoe.jzon
|
||||
:parse))
|
||||
|
||||
(in-package :seanut)
|
||||
|
||||
(defvar *authorization-format*
|
||||
"MediaBrowser Client=\"Seanut v~A\", Device=\"~A\", DeviceId=\"~A\", Version=\"~A\", Token=\"~A\"")
|
||||
|
||||
(defvar *valid-media-types*
|
||||
'("AggregateFolder" "Audio" "AudioBook" "Book"
|
||||
"BoxSet" "Movie" "MusicAlbum" "MusicArtist" "MusicGenre"
|
||||
"MusicVideo" "Playlist" "Season" "Series" "Trailer"))
|
|
@ -0,0 +1,20 @@
|
|||
;;;; seanut.asd
|
||||
|
||||
(asdf:defsystem #:seanut
|
||||
:description "command line utility to grab bulk media (e.g., full shows, full albums) from Jellyfin servers"
|
||||
:author "a. fox"
|
||||
:license "MIT"
|
||||
:version "0.0.1"
|
||||
:serial t
|
||||
:depends-on (#:dexador #:with-user-abort #:unix-opts
|
||||
#:com.inuoe.jzon #:babel #:ironclad #:quri)
|
||||
:components ((:file "package")
|
||||
(:file "util")
|
||||
(:file "seanut"))
|
||||
:entry-point "seanut::main"
|
||||
:build-operation "program-op"
|
||||
:build-pathname "bin/seanut")
|
||||
|
||||
#+sb-core-compression
|
||||
(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
|
||||
(uiop:dump-image (asdf:output-file o c) :executable t :compression t))
|
|
@ -0,0 +1,61 @@
|
|||
;;;; seanut.lisp
|
||||
|
||||
(in-package #:seanut)
|
||||
|
||||
(define-opts
|
||||
(:name :help
|
||||
:short #\h
|
||||
:long "help"
|
||||
:description "prints this help")
|
||||
(:name :version
|
||||
:short #\v
|
||||
:long "version"
|
||||
:description "prints the version")
|
||||
(:name :output
|
||||
:short #\o
|
||||
:long "output"
|
||||
:meta-var "DIR"
|
||||
:arg-parser #'uiop:ensure-directory-pathname
|
||||
:description "location to save downloaded media")
|
||||
(:name :media-type
|
||||
:short #\m
|
||||
:long "media-type"
|
||||
:meta-var "TYPE"
|
||||
:arg-parser #'validate-media-type
|
||||
:description "media type to base our query on")
|
||||
(:name :token
|
||||
:short #\t
|
||||
:long "token"
|
||||
:meta-var "TOKEN"
|
||||
:arg-parser #'identity
|
||||
:description "access token for specified jellyfin server")
|
||||
(:name :season-number
|
||||
:short #\s
|
||||
:long "season"
|
||||
:meta-var "SEASON"
|
||||
:arg-parser #'maybe-parse-integer
|
||||
:description "specify specific season to download, if downloading a show"))
|
||||
|
||||
(defun build-search-query ())
|
||||
|
||||
(defun download-media (name url header &optional destination)
|
||||
"downloads the media at URL, using HEADER as the authorization header.
|
||||
|
||||
if DESTINATION is non-nil, dumps media into that directory, otherwise it uses CWD"
|
||||
(let ((output (or destination #P"./")))
|
||||
(dexador:fetch url (merge-pathnames name output)
|
||||
:if-exists nil
|
||||
:headers `(("X-Emby-Authorization" . ,header)))))
|
||||
|
||||
(defun main ()
|
||||
"binary entry point"
|
||||
(multiple-value-bind (opts args) (get-opts)
|
||||
(when (getf opts :help)
|
||||
(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)))
|
||||
|
||||
(destructuring-bind (domain search-term) args)))
|
|
@ -0,0 +1,36 @@
|
|||
;;; util.lisp
|
||||
|
||||
(in-package :seanut)
|
||||
|
||||
(declaim (inline seanut-version generate-authorization))
|
||||
|
||||
(defun maybe-parse-integer (str)
|
||||
(or (parse-integer str :junk-allowed t) -1))
|
||||
|
||||
(defun string-to-keyword (str)
|
||||
(intern (string-upcase str) :keyword))
|
||||
|
||||
(defun validate-media-type (type)
|
||||
(car (member type *valid-media-types* :test #'string=)))
|
||||
|
||||
(defun seanut-version ()
|
||||
"gets the system version"
|
||||
#.(asdf:component-version (asdf:find-system :seanut)))
|
||||
|
||||
(defun md5-string (str)
|
||||
"returns the MD5 hash of STR"
|
||||
(format nil "~{~X~}"
|
||||
(coerce (digest-sequence 'ironclad:md5
|
||||
(string-to-octets str))
|
||||
'list)))
|
||||
|
||||
(defun generate-authorization (token)
|
||||
"generates a properly formatted authorization header"
|
||||
(format nil *authorization-format*
|
||||
(seanut-version) (uiop:hostname) (md5-string (uiop:hostname))
|
||||
(seanut-version) token))
|
||||
|
||||
(defmacro quit-with-message (code message &rest args)
|
||||
`(progn
|
||||
(format t ,message ,@args)
|
||||
(uiop:quit ,code)))
|
Loading…
Reference in New Issue