├── system-index.txt ├── .gitignore ├── t └── cl-transmission.lisp ├── cl-transmission-test.asd ├── LICENSE ├── cl-transmission.asd ├── src ├── util.lisp ├── constants.lisp └── cl-transmission.lisp └── README.org /system-index.txt: -------------------------------------------------------------------------------- 1 | cl-transmission.asd 2 | cl-transmission-test.asd 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.fasl 2 | *.dx32fsl 3 | *.dx64fsl 4 | *.lx32fsl 5 | *.lx64fsl 6 | *.x86f 7 | *~ 8 | .#* 9 | .projectile -------------------------------------------------------------------------------- /t/cl-transmission.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | (defpackage cl-transmission-test 3 | (:use :cl 4 | :cl-transmission 5 | :prove)) 6 | (in-package :cl-transmission-test) 7 | 8 | ;; NOTE: To run this test file, execute `(asdf:test-system :cl-transmission)' in your Lisp. 9 | 10 | (plan nil) 11 | 12 | ;; blah blah blah. 13 | 14 | (finalize) 15 | -------------------------------------------------------------------------------- /cl-transmission-test.asd: -------------------------------------------------------------------------------- 1 | #| 2 | This file is a part of cl-transmission project. 3 | Copyright (c) 2017 Thomas Schaper (Thomas@libremail.nl) 4 | |# 5 | 6 | (defsystem "cl-transmission-test" 7 | :author "Thomas Schaper" 8 | :license "" 9 | :depends-on ("cl-transmission" 10 | "prove") 11 | :components ((:module "t" 12 | :components 13 | ((:test-file "cl-transmission")))) 14 | :description "Test system for cl-transmission" 15 | 16 | :defsystem-depends-on (:prove-asdf) 17 | :perform (test-op :after (op c) 18 | (funcall (intern #.(string :run-test-system) :prove-asdf) c) 19 | (asdf:clear-system c))) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thomas Schaper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cl-transmission.asd: -------------------------------------------------------------------------------- 1 | #| 2 | This file is a part of cl-transmission project. 3 | Copyright (c) 2017 Thomas Schaper (Thomas@libremail.nl) 4 | |# 5 | 6 | #| 7 | Author: Thomas Schaper (Thomas@libremail.nl) 8 | |# 9 | 10 | (asdf:defsystem "cl-transmission" 11 | :version "0.1" 12 | :author "Thomas Schaper" 13 | :license "MIT" 14 | :depends-on ("rutils" 15 | "drakma" 16 | "named-readtables" 17 | "cl-ppcre" 18 | "uiop" 19 | "jonathan") 20 | :components ((:module "src" 21 | :components 22 | ((:file "cl-transmission" :depends-on ("util" "constants")) 23 | (:file "constants" :depends-on ("util")) 24 | (:file "util")))) 25 | :description "" 26 | :long-description 27 | #.(with-open-file (stream (merge-pathnames 28 | #p"README.org" 29 | (or *load-pathname* *compile-file-pathname*)) 30 | :if-does-not-exist nil 31 | :direction :input) 32 | (when stream 33 | (let ((seq (make-array (file-length stream) 34 | :element-type 'character 35 | :fill-pointer t))) 36 | (setf (fill-pointer seq) (read-sequence seq stream)) 37 | seq))) 38 | :in-order-to ((test-op (test-op cl-transmission-test)))) 39 | -------------------------------------------------------------------------------- /src/util.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | 3 | (uiop:define-package cl-transmission.util 4 | (:use :cl :rutils.readtable) 5 | (:import-from :alexandria 6 | #:define-constant)) 7 | (in-package :cl-transmission.util) 8 | 9 | (named-readtables:in-readtable rutils.readtable:rutils-readtable) 10 | 11 | (defmacro define-export-macro (type) 12 | (let* ((name (intern (format nil "~A-EXPORT" (symbol-name type))))) 13 | `(progn 14 | (export ',name) 15 | (defmacro ,name (exported-name args &body body) 16 | `(progn 17 | (export ',exported-name) 18 | (,',type ,exported-name ,args ,@body)))))) 19 | 20 | (define-export-macro defclass) 21 | (define-export-macro defmacro) 22 | (define-export-macro defun) 23 | (define-export-macro defgeneric) 24 | (define-export-macro define-constant) 25 | (define-export-macro define-condition) 26 | 27 | (defmacro the-check (type val) 28 | (rutils:once-only (val) 29 | `(progn 30 | (check-type ,val ,type) 31 | (the ,type ,val)))) 32 | 33 | (defun-export make-keyword (str) 34 | (check-type str string) 35 | (intern str #.(find-package :keyword))) 36 | 37 | (defun-export string->keyword (string) 38 | (check-type string string) 39 | (the-check symbol 40 | (make-keyword 41 | (string-upcase 42 | (cl-ppcre:regex-replace-all "[A-Z]" string "-\\&"))))) 43 | 44 | (defun-export plist-to-hash-table (plist 45 | &rest 46 | rest 47 | &key 48 | (convert-key #'identity) 49 | (convert-value #'identity) 50 | &allow-other-keys) 51 | (remf rest :convert-key) 52 | (remf rest :convert-value) 53 | (loop :with ht = (apply #'make-hash-table rest) 54 | :for key :in plist :by #'cddr 55 | :for val :in (cdr plist) :by #'cddr 56 | :do (rutils:sethash (funcall convert-key key) 57 | ht 58 | (funcall convert-value val)) 59 | :finally (return ht))) 60 | 61 | (declaim (inline contains-key)) 62 | (defun-export contains-key (key hash-table) 63 | (rutils:2nd (gethash key hash-table))) 64 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * CL-Transmission 2 | ** Usage 3 | *** Loading 4 | Lets start by loading ~CL-TRANSMISSION~ and defining our connection. Connections 5 | are thread-safe in the way that you can use a one or more connection in multiple 6 | threads. 7 | #+begin_src lisp :exports both 8 | (ql:quickload :cl-transmission) 9 | 10 | (defvar *conn* (make-instance 'cl-transmission:transmission-connection 11 | :host "your.ip" ;; the host is "localhost" by default. 12 | :credentials '("libreman" "super-secret-password"))) 13 | #+end_src 14 | 15 | #+RESULTS: 16 | : *CONN* 17 | 18 | Please note that this package is not (yet) in Quicklisp so you will have to add 19 | it to your local projects to be able to load it. See the [[https://www.quicklisp.org/beta/faq.html][Quicklisp FAQ]] on how to 20 | do this. 21 | *** Searching 22 | So lets get some torrents. First lets get all torrents and of every torrent get 23 | the id, name and eta. This is done by using the 24 | ~CL-TRANSMISSION:TRANSMISSION-GET~ method. 25 | 26 | #+begin_src lisp :exports both 27 | (cl-transmission:transmission-get *conn* #(:name :id :eta) :strict t) 28 | #+end_src 29 | 30 | #+RESULTS: 31 | #+begin_example 32 | (# 33 | # 34 | #) 35 | NIL 36 | #+end_example 37 | 38 | As shown for every live torrent a hash-table is returned. So lets see how it is 39 | structured: 40 | 41 | #+begin_src lisp :exports both 42 | (alexandria:hash-table-plist 43 | (elt (cl-transmission:transmission-get *conn* #(:name :id :eta) :strict t) 44 | 0)) 45 | #+end_src 46 | 47 | #+RESULTS: 48 | | :NAME | debian-8.7.1-amd64-DVD-1.iso | :ID | 72 | :ETA | 1368 | 49 | 50 | So it simply contains the fields we specified. Searching is done by or passing a 51 | id, if you know it this is the fastest way, or mapping over the return value. 52 | 53 | *** Adding 54 | Adding torrents is done by the ~CL-TRANSMISSION:TRANSMISSION-ADD~ 55 | function. It accepts a =:FILENAME= argument which should be a filename 56 | to a torrent file or a magnet link, and =:METAINFO= which should be a 57 | base64-encoded torrent file. 58 | 59 | So lets say we want to add a nice debian torrent to seed: 60 | 61 | #+begin_src lisp :exports both 62 | (cl-transmission:transmission-add *conn* :filename "http://cdimage.debian.org/debian-cd/current/amd64/bt-dvd/debian-8.7.1-amd64-DVD-2.iso.torrent") 63 | #+end_src 64 | 65 | #+RESULTS: 66 | : # 67 | : :TORRENT-ADDED 68 | 69 | We get a hash-table of our new torrent with an ~ID~, ~NAME~ and ~HASH-STRING~, 70 | and the second return value is indicating this is a new torrent. So if we would 71 | add the same torrent again this would be ~:TORRENT-DUPLICATE~. 72 | 73 | *** Others 74 | At the moment the entire section 3 of the RPC spec is implemented. Simply see 75 | the exported method and their docstrings. Development is active and section 4 76 | will be implemented. 77 | ** Author 78 | + Thomas Schaper (Thomas@libremail.nl) 79 | ** Copyright 80 | Copyright (c) 2017 Thomas Schaper (Thomas@libremail.nl) 81 | -------------------------------------------------------------------------------- /src/constants.lisp: -------------------------------------------------------------------------------- 1 | (in-package :cl-user) 2 | 3 | (uiop:define-package cl-transmission.constants 4 | (:use #:cl #:cl-transmission.util) 5 | (:import-from :alexandria 6 | #:define-constant)) 7 | 8 | (in-package :cl-transmission.constants) 9 | (named-readtables:in-readtable rutils.readtable:rutils-readtable) 10 | 11 | (define-constant-export +default-optimizations+ '(optimize (debug 3)) 12 | :test #'equalp) 13 | 14 | (declaim #.+default-optimizations+) 15 | 16 | (define-constant-export +transmission-set-params+ 17 | #h(eql :bandwidth-priority "bandwidthPriority" 18 | :download-limit "downloadLimit" 19 | :download-limited "downloadLimited" 20 | :files-wanted "files-wanted" 21 | :files-unwanted "files-unwanted" 22 | :honors-session-limits "honorsSessionLimits" 23 | :ids "ids" 24 | :location "location" 25 | :peer-limit "peer-limit" 26 | :priority-high "priority-high" 27 | :priority-low "priority-low" 28 | :priority-normal "priority-normal" 29 | :queue-position "queuePosition" 30 | :seed-idle-limit "seedIdleLimit" 31 | :seed-idle-mode "seedIdleMode" 32 | :seed-ratio-limit "seedRatioLimit" 33 | :seed-ratio-mode "seedRatioMode" 34 | :tracker-add "trackerAdd" 35 | :tracker-remove "trackerRemove" 36 | :tracker-replace "trackerReplace" 37 | :upload-limit "uploadLimit" 38 | :upload-limited "uploadLimited") 39 | :test #'equalp) 40 | 41 | (define-constant-export +transmission-add-params+ 42 | #h(eql :cookies "cookies" 43 | :download-dir "download-dir" 44 | :filename "filename" 45 | :metainfo "metainfo" 46 | :paused "paused" 47 | :peer-limit "peer-limit" 48 | :bandwidthPriority "bandwidthPriority" 49 | :files-wanted "files-wanted" 50 | :files-unwanted "files-unwanted" 51 | :priority-high "priority-high" 52 | :priority-low "priority-low" 53 | :priority-normal "priority-normal") 54 | :test #'equalp) 55 | 56 | (define-constant-export +transmission-get-params+ 57 | #h(eql :activity-date "activityDate" 58 | :added-date "addedDate" 59 | :bandwidth-priority "bandwidthPriority" 60 | :comment "comment" 61 | :corrupt-ever "corruptEver" 62 | :creator "creator" 63 | :date-created "dateCreated" 64 | :desired-available "desiredAvailable" 65 | :done-date "doneDate" 66 | :download-dir "downloadDir" 67 | :downloaded-ever "downloadedEver" 68 | :download-limit "downloadLimit" 69 | :download-limited "downloadLimited" 70 | :error "error" 71 | :error-string "errorString" 72 | :eta "eta" 73 | :eta-idle "etaIdle" 74 | :files "files" 75 | :file-stats "fileStats" 76 | :hash-string "hashString" 77 | :have-unchecked "haveUnchecked" 78 | :have-valid "haveValid" 79 | :honors-session-limits "honorsSessionLimits" 80 | :id "id" 81 | :is-finished "isFinished" 82 | :is-private "isPrivate" 83 | :is-stalled "isStalled" 84 | :left-until-done "leftUntilDone" 85 | :magnet-link "magnetLink" 86 | :manual-announce-time "manualAnnounceTime" 87 | :max-connected-peers "maxConnectedPeers" 88 | :metadata-percent-complete "metadataPercentComplete" 89 | :name "name" 90 | :peer "peer" 91 | :peers "peers" 92 | :peers-connected "peersConnected" 93 | :peers-from "peersFrom" 94 | :peers-getting-from-us "peersGettingFromUs" 95 | :peers-sending-to-us "peersSendingToUs" 96 | :percent-done "percentDone" 97 | :pieces "pieces" 98 | :piece-count "pieceCount" 99 | :piece-size "pieceSize" 100 | :priorities "priorities" 101 | :queue-position "queuePosition" 102 | :rate-download "rateDownload" 103 | :rate-upload "rateUpload" 104 | :recheck-progress "recheckProgress" 105 | :seconds-downloading "secondsDownloading" 106 | :seconds-seeding "secondsSeeding" 107 | :seed-idle-limit "seedIdleLimit" 108 | :seed-idle-mode "seedIdleMode" 109 | :seed-ratio-limit "seedRatioLimit" 110 | :seed-ratio-mode "seedRatioMode" 111 | :size-when-done "sizeWhenDone" 112 | :start-date "startDate" 113 | :status "status" 114 | :trackers "trackers" 115 | :tracker-stats "trackerStats" 116 | :total-size "totalSize" 117 | :torrent-file "torrentFile" 118 | :uploaded-ever "uploadedEver" 119 | :upload-limit "uploadLimit" 120 | :upload-limited "uploadLimited" 121 | :upload-ratio "uploadRatio" 122 | :wanted "wanted" 123 | :webseeds "webseeds" 124 | :webseeds-sending-to-us "webseedsSendingToUs" 125 | :files "files" 126 | :file-stats "fileStats" 127 | :peers "peers" 128 | :peers-from "peersFrom" 129 | :pieces "pieces" 130 | :priorities "priorities" 131 | :trackers "trackers" 132 | :tracker-stats "trackerStats" 133 | :wanted "wanted" 134 | :webseeds "webseeds") 135 | :test #'equalp) 136 | -------------------------------------------------------------------------------- /src/cl-transmission.lisp: -------------------------------------------------------------------------------- 1 | ;;-*- mode: lisp -*- 2 | 3 | (in-package :cl-user) 4 | 5 | (uiop:define-package cl-transmission 6 | (:use #:cl 7 | #:rutils.readtable 8 | #:rutils.anaphora 9 | #:cl-transmission.util 10 | #:cl-transmission.constants) 11 | (:import-from :rutils 12 | #:sethash 13 | #:length= 14 | #:hash-table-from-plist)) 15 | (in-package :cl-transmission) 16 | 17 | (declaim #.+default-optimizations+) 18 | 19 | (named-readtables:in-readtable rutils.readtable:rutils-readtable) 20 | (eval-when (:compile-toplevel :load-toplevel :execute) 21 | (set-dispatch-macro-character 22 | #\# #\d (lambda (s c n) 23 | (declare (ignore c n)) 24 | (format nil (read s))))) 25 | 26 | (setf drakma:*drakma-default-external-format* :utf8) 27 | (pushnew '("application" . "json") drakma:*text-content-types*) 28 | 29 | (deftype port-number () 30 | '(integer 1 65535)) 31 | 32 | (defun keyword-all-p (keyword) 33 | (and (keywordp keyword) (eq keyword :all))) 34 | 35 | (defun valid-torrent-get-keys-p (item) 36 | (when (typep item 'sequence) 37 | (block nil 38 | (map 'nil 39 | ^(unless (contains-key % +transmission-get-params+) 40 | (return nil)) 41 | item) 42 | t))) 43 | 44 | (defun valid-torrent-add-keys-p (item) 45 | (and (typep item 'sequence) 46 | (block nil 47 | (let ((res nil)) 48 | (alexandria:doplist (key val item res) 49 | (when (or (eql key :filename) (eql key :metainfo)) 50 | (unless (and (typep val 'string) (not res)) 51 | (return nil)) 52 | (setf res t))))))) 53 | 54 | (defun no-duplicates-p (item) 55 | (when (typep item 'sequence) 56 | (block nil 57 | (let ((seen #h(equal))) 58 | (map 'nil 59 | ^(if (rutils:2nd (gethash % seen)) 60 | (return nil) 61 | (sethash % seen t)) 62 | item) 63 | t)))) 64 | 65 | (defclass-export transmission-connection () 66 | ((protocol 67 | :initarg :protocol 68 | :type string 69 | :initform "http" 70 | :documentation #d"The protocol to use to contact the server probably ~ 71 | \"http\" or \"https\".") 72 | (host 73 | :initarg :host 74 | :type string 75 | :initform "localhost" 76 | :documentation "The host of the transmission server.") 77 | (port 78 | :initarg :port 79 | :type port-number 80 | :initform 9091 81 | :documentation "The port of the transmission server.") 82 | (url 83 | :initarg :url 84 | :type string 85 | :initform "transmission/rpc" 86 | :documentation #d"The part of the url of the transmission server after the ~ 87 | port.") 88 | (credentials 89 | :initarg :credentials 90 | :type (or null list) 91 | :initform nil 92 | :documentation #d"he basic authentication for the transmission server. Use ~ 93 | NIL if no auth is needed.") 94 | (session-id 95 | :type string 96 | :initform "" 97 | :documentation #d"The current \"X-Transmission-Session-Id\" used by the ~ 98 | transmission server."))) 99 | 100 | (defmacro def-torrent-request (method lambda-list (&key ignore-keys-p) &body body) 101 | (let* ((name (intern (concatenate 'string "TRANSMISSION-" (symbol-name method)))) 102 | (method-string (concatenate 'string "torrent-" (string-downcase (symbol-name method)))) 103 | (documentation (if (and (stringp (car body)) (not (length= body 1))) 104 | (car body) 105 | nil)) 106 | (body (if documentation (cdr body) body)) 107 | (declarations (if (eql (caar body) 'declare) (car body) nil)) 108 | (body (if declarations (cdr body) body)) 109 | (method-list (cons '(conn transmission-connection) 110 | (if ignore-keys-p 111 | (loop :for item :in lambda-list 112 | :for key-seen = (eql item '&key) 113 | :then (or key-seen (eql item '&key)) 114 | :when (eql item '&key) 115 | :append (list '&rest 'all-keys '&key) :into res 116 | :when (or (not key-seen) 117 | (listp item)) 118 | :collect item :into res 119 | :finally (return (append res 120 | (if key-seen 121 | '(&allow-other-keys) 122 | '(&rest all-keys))))) 123 | lambda-list))) 124 | (lambda-list (loop :for item :in lambda-list 125 | :collect (if (listp item) 126 | (car item) 127 | item)))) 128 | `(defgeneric-export ,name ,(cons 'connection lambda-list) 129 | (:documentation ,documentation) 130 | (:method ,method-list 131 | ,declarations 132 | (let ((transmission-method-name ,method-string)) 133 | ,@body))))) 134 | 135 | (defmacro def-torrent-action-request (action &optional docstring) 136 | (check-type docstring (or null string)) 137 | `(def-torrent-request ,action (ids) (:ignore-keys-p nil) 138 | ,@(when docstring 139 | (list (concatenate 'string docstring " 140 | 141 | The torrents are specified by \"IDS\", which should be a list of torrent 142 | designators, a torrent designator is a torrent-id or a torrent sha1 string, or 143 | it should be the string \"recently-active\" (case sensitive). 144 | 145 | This function will return nothing of value."))) 146 | (check-type ids sequence) 147 | (transmission-request conn 148 | transmission-method-name 149 | #h(equal "ids" ids)) 150 | (values))) 151 | 152 | (defun get-transmission-host (conn) 153 | (with-slots (protocol host port url) conn 154 | (rutils:fmt "~A://~A:~A/~A" 155 | protocol 156 | host 157 | port 158 | url))) 159 | 160 | (defun update-session-id (conn headers) 161 | (with-slots (session-id-lock session-id) conn 162 | (setf session-id (cdr (assoc :x-transmission-session-id 163 | headers))))) 164 | 165 | (define-condition-export transmission-error (error) 166 | ((response :initarg :response :reader transmission-error-response)) 167 | (:documentation #d"The general error used when we could not complete the ~ 168 | request to the transmission server.") 169 | (:report (lambda (condition stream) 170 | (format stream 171 | "Transmission server signaled an error: \"~A\"." 172 | (transmission-error-response condition))))) 173 | 174 | (defun %transmission-request (conn method arguments) 175 | (check-type conn transmission-connection) 176 | (check-type method string) 177 | (check-type arguments (or hash-table list)) 178 | (flet ((get-res (url content) 179 | (drakma:http-request url 180 | :method :post 181 | :basic-authorization (slot-value conn 'credentials) 182 | :additional-headers `(("X-Transmission-Session-Id" . ,(slot-value conn 'session-id))) 183 | :content content))) 184 | (let* ((content-hash #h(equalp 185 | "method" method 186 | "arguments" arguments)) 187 | (content (jonathan:to-json content-hash)) 188 | (url (get-transmission-host conn))) 189 | (multiple-value-bind (res code headers) 190 | (get-res url content) 191 | (when (= code 409) 192 | (update-session-id conn headers) 193 | (multiple-value-bind (new-res new-code) 194 | (get-res url content) 195 | (setf code new-code) 196 | (setf res new-res))) 197 | (setf res (if (= code 200) 198 | (jonathan:parse res 199 | :as :hash-table 200 | :junk-allowed t 201 | :keyword-normalizer #'string->keyword 202 | :normalize-all t) 203 | #h(:result res))) 204 | (let* ((arguments (gethash :arguments res)) 205 | (error-string (gethash :result res))) 206 | (if (equal error-string "success") 207 | arguments 208 | (error 'transmission-error :response error-string))))))) 209 | 210 | (defun transmission-request (conn method arguments) 211 | (loop :thereis (with-simple-restart (retry-request "Retry the request to ~ 212 | transmission server with the same arguments") 213 | (%transmission-request conn method arguments)))) 214 | 215 | (def-torrent-action-request start 216 | "Start the given torrents by adding them to the appropriate queue.") 217 | (def-torrent-action-request start-now 218 | "Start the given torrents directly bypassing the queues.") 219 | (def-torrent-action-request stop 220 | "Stop (pause) the given torrents.") 221 | (def-torrent-action-request verify 222 | "Queue the given torrents for verification.") 223 | (def-torrent-action-request reannounce 224 | "Ask the tracker for more peers for the given torrents.") 225 | 226 | (def-torrent-request set (ids &key bandwidth-priority download-limit 227 | download-limited files-wanted files-unwanted 228 | honors-session-limits location peer-limit 229 | priority-high priority-low priority-normal 230 | queue-position seed-idle-limit seed-idle-mode 231 | seed-ratio-limit seed-ratio-mode tracker-add 232 | tracker-remove tracker-replace upload-limit 233 | upload-limited) 234 | (:ignore-keys-p t) 235 | "Set the specified key to the given value for the given \"IDS\". 236 | 237 | \"IDS\" should be a list of torrent designators, where a torrent designator is a 238 | torrent-id or a torrent sha1 string, or it should be \":ALL\" to set this value 239 | for all the torrents on the server. 240 | 241 | For the exact meaning of the keys see the rpc spec. Please note that leaving a 242 | key unspecified is not the same as passing an empty array. Also note that 243 | passing \"NIL\" will result in sending an empty array, not the boolean 244 | \"false\". To send the boolean \"false\" pass \":FALSE\" as keyword. 245 | 246 | This function will return nothing of value." 247 | (check-type ids (and (not null) (or list (satisfies keyword-all-p)))) 248 | (let* ((args (plist-to-hash-table 249 | all-keys 250 | :test 'equal 251 | :convert-key ^(gethash % +transmission-set-params+)))) 252 | (sethash (gethash :ids +transmission-set-params+) 253 | args 254 | (if (eql ids :all) #() ids)) 255 | (transmission-request conn 256 | transmission-method-name 257 | args) 258 | (values))) 259 | 260 | (def-torrent-request get (fields &key (ids :all) strict) (:ignore-keys-p nil) 261 | "Get the fields specified in \"FIELDS\" for the given \"IDS\". 262 | 263 | \"IDS\" should be a list of torrent designators, where a torrent designator is a 264 | torrent-id or a torrent sha1 string, or it should be \":ALL\" to set this value 265 | for all the torrents on the server. 266 | 267 | The specified fields should be a sequence of keywords, if \"STRICT\" is non NIL 268 | we will check if it does not contain any illegal keywords. All legal keywords 269 | are specified in \"CL-TRANSMISSION.CONSTANTS:+TRANSMISSION-GET-PARAMS+\" 270 | 271 | The first return value is a list of active torrents. The second return value is 272 | a list of removed torrents. Torrents are represented by a hash-table containing 273 | the specified fiels normalized as keywords." 274 | (check-type ids (or null sequence string (satisfies keyword-all-p))) 275 | (check-type fields sequence) 276 | (when strict 277 | (check-type fields (and (satisfies valid-torrent-get-keys-p) 278 | (satisfies no-duplicates-p)))) 279 | (let* ((string-fields (map 'vector 280 | ^(gethash % +transmission-get-params+) 281 | fields)) 282 | (string-fields (remove nil string-fields)) 283 | (args #h(equalp "fields" string-fields 284 | "ids" ids)) 285 | (_ (when (eql ids :all) (remhash "ids" args))) 286 | (res (transmission-request conn 287 | transmission-method-name 288 | args))) 289 | (declare (ignore _)) 290 | (values (gethash :torrents res) 291 | (gethash :removed res)))) 292 | 293 | (def-torrent-request add (&key cookies download-dir filename metainfo paused 294 | peer-limit bandwidth-priority files-wanted 295 | files-unwanted priority-high priority-low 296 | priority-normal) 297 | (:ignore-keys-p t) 298 | "Add a new torrent specified the given fields. 299 | 300 | The torrent is given in \"METAINFO\" or \"FILENAME\". \"METAINFO\" should be a 301 | base64-encoded torrent file while \"FILENAME\" should be a filename of torrent 302 | file or a magnet link. For the other options and more detailed descriptions see 303 | the rpc specifications. 304 | 305 | The first return value is a hash-table with fields for the id, \":ID\", name, 306 | \":NAME\" and hash string, \":HASH-STRING\", of the added torrent. The second 307 | return value is \":TORRENT-ADDED\" if a new torrent was added and 308 | \":TORRENT-DUPLICATE\" if no new torrent was added." 309 | (check-type all-keys (satisfies valid-torrent-add-keys-p)) 310 | (let* ((res (transmission-request conn 311 | transmission-method-name 312 | (plist-to-hash-table 313 | all-keys 314 | :test 'equal 315 | :convert-key ^(gethash % +transmission-add-params+))))) 316 | (values (car (rutils:hash-table-vals res)) 317 | (car (rutils:hash-table-keys res))))) 318 | 319 | (def-torrent-request remove (&key ids delete-local-data) (:ignore-keys-p nil) 320 | "Remove the torrents with the given \"IDS\" and maybe delete the local data. 321 | 322 | \"IDS\" should be a list of torrent designators, where a torrent designator is a 323 | torrent-id or a torrent sha1 string, or it should be the string 324 | \"recently-active\" (case sensitive). 325 | 326 | \"DELETE-LOCAL-DATA\" should parse to a json boolean. \"NIL\" will be converted 327 | to \":FALSE\" which will parse the false boolean in json. 328 | 329 | This function does not return anything useful. 330 | " 331 | (check-type ids (or sequence string)) 332 | (setf delete-local-data (or delete-local-data :false)) 333 | (transmission-request conn 334 | transmission-method-name 335 | #h(equal "ids" ids 336 | "delete-local-data" delete-local-data)) 337 | (values)) 338 | 339 | (def-torrent-request set-location (ids location &optional (move :false)) (:ignore-keys-p nil) 340 | "Move the \"IDS\" to the new \"LOCATION\" moving the old if \"MOVE\" is true. 341 | 342 | \"IDS\" should be a list of torrent designators, where a torrent designator is a 343 | torrent-id or a torrent sha1 string, or it should be the string 344 | \"recently-active\" (case sensitive). 345 | 346 | \"LOCATION\" is the new location of the specified torrents. It should parse to a 347 | json string by jonathan. 348 | 349 | \"MOVE\" should parse to a json boolean. \"NIL\" will be converted to \":FALSE\" 350 | which will parse the false boolean in json. 351 | 352 | This function does not return anything of value." 353 | (check-type ids (or sequence string)) 354 | (transmission-request conn 355 | transmission-method-name 356 | #h(equal "ids" ids 357 | "location" location 358 | "move" move)) 359 | (values)) 360 | 361 | (def-torrent-request rename-path (id path name) (:ignore-keys-p nil) 362 | "Rename the \"PATH\" of given torrent by \"ID\" to \"NAME\" 363 | 364 | The \"ID\" should be a torrent id, sha1 hash string or the string 365 | \"recently-active\" (case sensitive). 366 | 367 | The torrents \"PATH\" will be renamed to \"NAME\", some examples (modified from 368 | transmission.h): 369 | 370 | Consider a torrent, with id 0, which has two files with the names 371 | \"frobnitz-linux/checksum\" and \"frobnitz-linux/frobnitz.iso\". 372 | 373 | If we would call \"(TRANSMISSION-RENAME-PATH 0 \"frobnitz-linux\", \"foo\")\" then 374 | the \"frobnitz-linux\" folder will be renamed to \"foo\" in both torrents. 375 | However if we would call \"(TRANSMISSION-RENAME-PATH 0 376 | \"frobnitz-linux/checksum\", \"foo\")\" then only \"frobnitz-linux/checksum\" 377 | will be renamed to \"frobnitz-linux/foo\" while 378 | \"frobnitz-linux/frobnitz.iso\" will remain unchanged. 379 | 380 | This value does not return anything value." 381 | (check-type id (or integer string)) 382 | (when (string= id "recently-active") 383 | (setf id (list id))) 384 | (transmission-request conn 385 | transmission-method-name 386 | #h(equal "ids" id 387 | "path" path 388 | "name" name)) 389 | (values)) 390 | 391 | #| 392 | Local Variables: 393 | eval: (font-lock-add-keywords nil '(("(\\(\\(def-torrent-\\(action-\\)?request\\)\\)\\s \\(\\(?:\\s_\\|\\sw\\)+\\)" (1 font-lock-keyword-face) (4 font-lock-function-name-face)))) 394 | End: 395 | |# 396 | --------------------------------------------------------------------------------