From 1e576d84432e0a90fb242b899d55de5ff6185598 Mon Sep 17 00:00:00 2001 From: "a. fox" Date: Wed, 8 Nov 2023 17:52:56 -0500 Subject: [PATCH] first push --- Makefile | 16 ++++++++++++++ README.md | 19 ++++++++++++++++ package.lisp | 28 ++++++++++++++++++++++++ seanut.asd | 20 +++++++++++++++++ seanut.lisp | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ util.lisp | 36 +++++++++++++++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 package.lisp create mode 100644 seanut.asd create mode 100644 seanut.lisp create mode 100644 util.lisp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4aadf45 --- /dev/null +++ b/Makefile @@ -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/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..049c6ec --- /dev/null +++ b/README.md @@ -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 + diff --git a/package.lisp b/package.lisp new file mode 100644 index 0000000..adf5454 --- /dev/null +++ b/package.lisp @@ -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")) diff --git a/seanut.asd b/seanut.asd new file mode 100644 index 0000000..d57c604 --- /dev/null +++ b/seanut.asd @@ -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)) diff --git a/seanut.lisp b/seanut.lisp new file mode 100644 index 0000000..a818e9c --- /dev/null +++ b/seanut.lisp @@ -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))) diff --git a/util.lisp b/util.lisp new file mode 100644 index 0000000..af80d21 --- /dev/null +++ b/util.lisp @@ -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)))