first push

This commit is contained in:
a. fox 2023-11-08 17:52:56 -05:00
commit 1e576d8443
6 changed files with 180 additions and 0 deletions

16
Makefile Normal file
View File

@ -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/

19
README.md Normal file
View File

@ -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

28
package.lisp Normal file
View File

@ -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"))

20
seanut.asd Normal file
View File

@ -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))

61
seanut.lisp Normal file
View File

@ -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)))

36
util.lisp Normal file
View File

@ -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)))