transmission-0.12.2/0000755000175000017500000000000013764054547014176 5ustar dogslegdogslegtransmission-0.12.2/transmission.el0000644000175000017500000031023413764054547017254 0ustar dogslegdogsleg;;; transmission.el --- Interface to a Transmission session -*- lexical-binding: t -*- ;; Copyright (C) 2014-2020 Mark Oteiza ;; Author: Mark Oteiza ;; Version: 0.12.2 ;; Package-Requires: ((emacs "24.4") (let-alist "1.0.5")) ;; Keywords: comm, tools ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the GNU General Public License ;; as published by the Free Software Foundation; either version 3 ;; of the License, or (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;;; Commentary: ;; Interface to a Transmission session. ;; Originally based on the JSON RPC library written by Christopher ;; Wellons, available online at ;; ;; Entry points are the `transmission' and `transmission-add' ;; commands. A variety of commands are available for manipulating ;; torrents and their contents, many of which can be applied over ;; multiple items by selecting them with marks or within a region. ;; The menus for each context provide good exposure. ;; "M-x transmission RET" pops up a torrent list. One can add, ;; start/stop, verify, remove torrents, set speed limits, ratio ;; limits, bandwidth priorities, trackers, etc. Also, one can ;; navigate to the corresponding file list, torrent info, or peer info ;; contexts. In the file list, individual files can be toggled for ;; download, and their priorities set. ;; Customize-able are: the session address components, RPC ;; credentials, the display format of dates, file sizes and transfer ;; rates, pieces display, automatic refreshing of the torrent ;; list, etc. See the `transmission' customization group. ;; The design draws from a number of sources, including the command ;; line utility transmission-remote(1), the ncurses interface ;; transmission-remote-cli(1), and the rtorrent(1) client. These can ;; be found respectively at the following: ;; ;; ;; ;;; Code: (require 'auth-source) (require 'calc-bin) (require 'calc-ext) (require 'color) (require 'diary-lib) (require 'json) (require 'mailcap) (require 'tabulated-list) (require 'url-util) (eval-when-compile (cl-declaim (optimize (speed 3))) (require 'cl-lib) (require 'let-alist) (require 'subr-x)) (defgroup transmission nil "Interface to a Transmission session." :link '(url-link "https://github.com/transmission/transmission") :link '(url-link "https://transmissionbt.com/") :group 'external) (defcustom transmission-host "localhost" "Host name, IP address, or socket address of the Transmission session." :type 'string) (defcustom transmission-service 9091 "Port or name of the service for the Transmission session." :type '(choice (const :tag "Default" 9091) (string :tag "Service") (integer :tag "Port")) :link '(function-link make-network-process)) (defcustom transmission-rpc-path "/transmission/rpc" "Path to the Transmission session RPC interface." :type '(choice (const :tag "Default" "/transmission/rpc") (string :tag "Other path"))) (defcustom transmission-rpc-auth nil "Authentication (username, password, etc.) for the RPC interface. Its value is a specification of the type used in `auth-source-search'. If no password is set, `auth-sources' is searched using the username, `transmission-host', and `transmission-service'." :type '(choice (const :tag "None" nil) (plist :tag "Username/password" :options ((:username string) (:password string)))) :link '(info-link "(auth) Help for users") :link '(function-link auth-source-search)) (defcustom transmission-digit-delimiter "," "String used to delimit digits in numbers. The variable `calc-group-char' is bound to this in `transmission-group-digits'." :type '(choice (const :tag "Comma" ",") (const :tag "Full Stop" ".") (const :tag "None" nil) (string :tag "Other char")) :link '(variable-link calc-group-char) :link '(function-link transmission-group-digits)) (defcustom transmission-pieces-function #'transmission-format-pieces "Function used to show pieces of incomplete torrents. The function takes a string (bitfield) representing the torrent pieces and the number of pieces as arguments, and should return a string." :type '(radio (const :tag "None" nil) (function-item transmission-format-pieces) (function-item transmission-format-pieces-brief) (function :tag "Function"))) (defcustom transmission-trackers '() "List of tracker URLs. These are used for completion in `transmission-trackers-add' and `transmission-trackers-replace'." :type '(repeat (string :tag "URL"))) (defcustom transmission-units nil "The flavor of units used to display file sizes. See `file-size-human-readable'." :type '(choice (const :tag "Default" nil) (const :tag "SI" si) (const :tag "IEC" iec)) :link '(function-link file-size-human-readable)) (defcustom transmission-refresh-modes '() "List of major modes in which to refresh the buffer automatically." :type 'hook :options '(transmission-mode transmission-files-mode transmission-info-mode transmission-peers-mode)) (defcustom transmission-refresh-interval 2 "Period in seconds of the refresh timer." :type '(number :validate (lambda (w) (when (<= (widget-value w) 0) (widget-put w :error "Value must be positive") w)))) (defcustom transmission-time-format "%a %b %e %T %Y %z" "Format string used to display dates. See `format-time-string'." :type 'string :link '(function-link format-time-string)) (defcustom transmission-time-zone nil "Time zone of formatted dates. See `format-time-string'." :type '(choice (const :tag "Local time" nil) (const :tag "Universal Time (UTC)" t) (const :tag "System Wall Clock" wall) (string :tag "Time Zone Identifier")) :link '(info-link "(libc) TZ Variable") :link '(function-link format-time-string)) (defcustom transmission-add-history-variable 'transmission-add-history "History list to use for interactive prompts of `transmission-add'. Consider adding the value (`transmission-add-history' by default) to `savehist-additional-variables'." :type 'variable :link '(emacs-commentary-link "savehist")) (defcustom transmission-tracker-history-variable 'transmission-tracker-history "History list to use for interactive prompts of tracker commands. Consider adding the value (`transmission-tracker-history' by default) to `savehist-additional-variables'." :type 'variable :link '(emacs-commentary-link "savehist")) (defcustom transmission-torrent-functions '(transmission-ffap transmission-ffap-selection transmission-ffap-last-killed) "List of functions to use for guessing torrents for `transmission-add'. Each function should accept no arguments, and return a string or nil." :type 'hook :options '(transmission-ffap transmission-ffap-selection transmission-ffap-last-killed)) (defcustom transmission-files-command-functions '(mailcap-file-default-commands) "List of functions to use for guessing default applications. Each function should accept one argument, a list of file names, and return a list of strings or nil." :type 'hook :options '(mailcap-file-default-commands)) (defcustom transmission-geoip-function nil "Function used to translate an IP address into a location name. The function should accept an IP address and return a string or nil." :type '(radio (const :tag "None" nil) (function-item transmission-geoiplookup) (function :tag "Function"))) (defcustom transmission-geoip-use-cache nil "Whether to cache IP address/location name associations. If non-nil, associations are stored in `transmission-geoip-table'. Useful if `transmission-geoip-function' does not have its own caching built in or is otherwise slow." :type 'boolean) (defcustom transmission-turtle-lighter " turtle" "Lighter for `transmission-turtle-mode'." :type `(choice (const :tag "Default" " turtle") (const :tag "ASCII" " ,=,e") (const :tag "Emoji" ,(string ?\s #x1f422)) (string :tag "Some string")) :set (lambda (symbol value) (set-default symbol value) (when (fboundp 'transmission-turtle-poll) (transmission-turtle-poll))) :link '(info-link "(elisp) Defining Minor Modes")) (defconst transmission-schedules (eval-when-compile (pcase-let* ((`(,sun ,mon ,tues ,wed ,thurs ,fri ,sat) (mapcar (lambda (x) (lsh 1 x)) (number-sequence 0 6))) (weekday (logior mon tues wed thurs fri)) (weekend (logior sat sun)) (all (logior weekday weekend))) `((sun . ,sun) (mon . ,mon) (tues . ,tues) (wed . ,wed) (thurs . ,thurs) (fri . ,fri) (sat . ,sat) (weekday . ,weekday) (weekend . ,weekend) (all . ,all)))) "Alist of Transmission turtle mode schedules.") (defconst transmission-mode-alist '((session . 0) (torrent . 1) (unlimited . 2)) "Alist of threshold mode enumerations.") (defconst transmission-priority-alist '((low . -1) (normal . 0) (high . 1)) "Alist of names to priority values.") (defconst transmission-status-alist '((stopped . 0) (verifywait . 1) (verifying . 2) (downwait . 3) (downloading . 4) (seedwait . 5) (seeding . 6)) "Alist of possible Transmission torrent statuses.") (defconst transmission-draw-torrents-keys ["id" "name" "status" "eta" "error" "labels" "rateDownload" "rateUpload" "percentDone" "sizeWhenDone" "metadataPercentComplete" "uploadRatio"]) (defconst transmission-draw-files-keys ["name" "files" "fileStats" "downloadDir"]) (defconst transmission-draw-info-keys ["name" "hashString" "magnetLink" "labels" "activityDate" "addedDate" "dateCreated" "doneDate" "startDate" "peers" "pieces" "pieceCount" "pieceSize" "trackerStats" "peersConnected" "peersGettingFromUs" "peersFrom" "peersSendingToUs" "sizeWhenDone" "error" "errorString" "uploadRatio" "downloadedEver" "corruptEver" "haveValid" "totalSize" "percentDone" "seedRatioLimit" "seedRatioMode" "bandwidthPriority" "downloadDir" "uploadLimit" "uploadLimited" "downloadLimit" "downloadLimited" "honorsSessionLimits" "rateDownload" "rateUpload" "queuePosition"]) (defconst transmission-file-symbols '(:files-wanted :files-unwanted :priority-high :priority-low :priority-normal) "List of \"torrent-set\" method arguments for operating on files.") (defvar transmission-session-id nil "The \"X-Transmission-Session-Id\" header value.") (defvar transmission-add-history nil "Default history list for `transmission-add'.") (defvar transmission-tracker-history nil "Default history list for `transmission-trackers-add' and others.") (defvar-local transmission-torrent-vector nil "Vector of Transmission torrent data.") (defvar-local transmission-torrent-id nil "The Transmission torrent ID integer.") (defvar-local transmission-refresh-function nil "The name of the function used to redraw a buffer. Should accept the torrent ID as an argument, e.g. `transmission-torrent-id'.") (define-error 'transmission-conflict "Wrong or missing header \"X-Transmission-Session-Id\"") (define-error 'transmission-unauthorized "Unauthorized user. Check `transmission-rpc-auth'") (define-error 'transmission-wrong-rpc-path "Bad RPC path. Check `transmission-rpc-path'") (define-error 'transmission-failure "RPC Failure") (define-error 'transmission-misdirected "Unrecognized hostname. Check \"rpc-host-whitelist\"") (defvar transmission-timer nil "Timer for repeating `revert-buffer' in a visible Transmission buffer.") (defvar transmission-geoip-table (make-hash-table :test 'equal) "Table for storing associations between IP addresses and location names.") (defvar-local transmission-marked-ids nil "List of indices of the currently marked items.") (defvar transmission-network-process-pool nil "List of network processes connected to Transmission.") ;; JSON RPC (defun transmission--move-to-content () "Move the point to beginning of content after the headers." (setf (point) (point-min)) (re-search-forward "^\r?\n" nil t)) (defun transmission--content-finished-p () "Return non-nil if all of the content has arrived." (setf (point) (point-min)) (when (search-forward "Content-Length: " nil t) (let ((length (read (current-buffer)))) (and (transmission--move-to-content) (<= length (- (position-bytes (point-max)) (position-bytes (point)))))))) (defun transmission--status () "Check the HTTP status code. A 409 response from a Transmission session includes the \"X-Transmission-Session-Id\" header. If a 409 is received, update `transmission-session-id' and signal the error." (save-excursion (goto-char (point-min)) (forward-char 5) ; skip "HTTP/" (skip-chars-forward "0-9.") (let* ((buffer (current-buffer)) (status (read buffer))) (pcase status (200 (save-excursion (let (result) (when (and (search-forward "\"result\":") (not (equal "success" (setq result (json-read))))) (signal 'transmission-failure (list result)))))) ((or 301 404 405) (signal 'transmission-wrong-rpc-path (list status))) (401 (signal 'transmission-unauthorized (list status))) (403 (signal 'transmission-failure (list status))) (409 (when (search-forward "X-Transmission-Session-Id: ") (setq transmission-session-id (read buffer)) (signal 'transmission-conflict (list status)))) (421 (signal 'transmission-misdirected (list transmission-host))))))) (defun transmission--auth-source-secret (user) "Return the secret for USER at found in `auth-sources'. Unless otherwise specified in `transmission-rpc-auth', the host and port default to `transmission-host' and `transmission-service', respectively." (let ((spec (copy-sequence transmission-rpc-auth))) (unless (plist-get spec :host) (plist-put spec :host transmission-host)) (unless (plist-get spec :port) (plist-put spec :port transmission-service)) (apply #'auth-source-pick-first-password (nconc `(:user ,user) spec)))) (defun transmission--auth-string () "HTTP \"Authorization\" header value if `transmission-rpc-auth' is populated." (when transmission-rpc-auth (let* ((user (plist-get transmission-rpc-auth :username)) (pass (and user (or (plist-get transmission-rpc-auth :password) (transmission--auth-source-secret user))))) (concat "Basic " (base64-encode-string (concat user ":" pass) t))))) (defun transmission-http-post (process content) "Send to PROCESS an HTTP POST request containing CONTENT." (with-current-buffer (process-buffer process) (erase-buffer)) (let ((headers (list (cons "X-Transmission-Session-Id" transmission-session-id) (cons "Host" transmission-host) ; CVE-2018-5702 (cons "Content-length" (string-bytes content))))) (let ((auth (transmission--auth-string))) (when auth (push (cons "Authorization" auth) headers))) (with-temp-buffer (insert (concat "POST " transmission-rpc-path " HTTP/1.1\r\n")) (dolist (elt headers) (insert (format "%s: %s\r\n" (car elt) (cdr elt)))) (insert "\r\n" content) (process-send-region process (point-min) (point-max))))) (defun transmission-wait (process) "Wait to receive HTTP response from PROCESS. Return JSON object parsed from content." (with-current-buffer (process-buffer process) (while (and (not (transmission--content-finished-p)) (process-live-p process)) (accept-process-output process 1)) (transmission--status) (transmission--move-to-content) (search-forward "\"arguments\":" nil t) (json-read))) (defun transmission-send (process content) "Send PROCESS string CONTENT and wait for response synchronously." (transmission-http-post process content) (transmission-wait process)) (defun transmission-process-sentinel (process _message) "Sentinel for PROCESS made by `transmission-make-network-process'." (setq transmission-network-process-pool (delq process transmission-network-process-pool)) (when (buffer-live-p (process-buffer process)) (kill-buffer (process-buffer process)))) (defun transmission-make-network-process () "Return a network client process connected to a Transmission daemon. When creating a new connection, the address is determined by the custom variables `transmission-host' and `transmission-service'." (let ((socket (when (file-name-absolute-p transmission-host) (expand-file-name transmission-host))) buffer process) (unwind-protect (prog1 (setq buffer (generate-new-buffer " *transmission*") process (make-network-process :name "transmission" :buffer buffer :host (when (null socket) transmission-host) :service (or socket transmission-service) :family (when socket 'local) :noquery t :coding 'binary :filter-multibyte nil)) (setq buffer nil process nil)) (when (process-live-p process) (kill-process process)) (when (buffer-live-p buffer) (kill-buffer buffer))))) (defun transmission-get-network-process () "Return a network client process connected to a Transmission daemon. Returns a stopped process in `transmission-network-process-pool' or, if none is found, establishes a new connection and adds it to the pool." (or (cl-loop for process in transmission-network-process-pool when (process-command process) return (continue-process process)) (let ((process (transmission-make-network-process))) (push process transmission-network-process-pool) process))) (defun transmission-request (method &optional arguments tag) "Send a request to Transmission and return a JSON object. The JSON is the \"arguments\" object decoded Transmission's response. METHOD is a string. ARGUMENTS is a plist having keys corresponding to METHOD. TAG is an integer and ignored. Details regarding the Transmission RPC can be found here: " (let ((process (transmission-get-network-process)) (content (json-encode `(:method ,method :arguments ,arguments :tag ,tag)))) (set-process-plist process nil) (set-process-filter process nil) (set-process-sentinel process nil) (unwind-protect (condition-case err (transmission-send process content) (transmission-conflict (transmission-send process content)) (transmission-failure (message "%s" (cdr err)))) (if (process-live-p process) (stop-process process) (setq transmission-network-process-pool (delq process transmission-network-process-pool)) (kill-buffer (process-buffer process)))))) ;; Asynchronous calls (defun transmission-process-callback (process) "Call PROCESS's callback if it has one." (let ((callback (process-get process :callback))) (when callback (transmission--move-to-content) (search-forward "\"arguments\":" nil t) (run-at-time 0 nil callback (json-read))))) (defun transmission-process-filter (process text) "Handle PROCESS's output TEXT and trigger handlers." (internal-default-process-filter process text) (when (buffer-live-p (process-buffer process)) (with-current-buffer (process-buffer process) (when (transmission--content-finished-p) (condition-case e (progn (transmission--status) (transmission-process-callback process) (stop-process process)) (transmission-conflict (transmission-http-post process (process-get process :request))) (transmission-failure (message "%s" (cdr e))) (error (stop-process process) (signal (car e) (cdr e)))))))) (defun transmission-request-async (callback method &optional arguments tag) "Send a request to Transmission asynchronously. CALLBACK accepts one argument, the response \"arguments\" JSON object. METHOD, ARGUMENTS, and TAG are the same as in `transmission-request'." (let ((process (transmission-get-network-process)) (content (json-encode `(:method ,method :arguments ,arguments :tag ,tag)))) (set-process-filter process #'transmission-process-filter) (set-process-sentinel process #'transmission-process-sentinel) (process-put process :request content) (process-put process :callback callback) (transmission-http-post process content) process)) ;; Response parsing (defun transmission-torrents (response) "Return the \"torrents\" array in RESPONSE, otherwise nil. Each element is an alist with keys corresponding to the elements of \"fields\" in the arguments of the \"torrent-get\" request. If the array is empty or not found, return nil." (let ((obj (cdr (assq 'torrents response)))) (and (vectorp obj) (< 0 (length obj)) obj))) ;; Timer management (defun transmission-timer-revert () "Revert the buffer or cancel `transmission-timer'." (if (and (memq major-mode transmission-refresh-modes) (not (or (bound-and-true-p isearch-mode) (buffer-narrowed-p) (use-region-p)))) (revert-buffer) (cancel-timer transmission-timer))) (defun transmission-timer-run () "Run the timer `transmission-timer'." (when transmission-timer (cancel-timer transmission-timer)) (setq transmission-timer (run-at-time t transmission-refresh-interval #'transmission-timer-revert))) (defun transmission-timer-check () "Check if current buffer should run a refresh timer." (when (memq major-mode transmission-refresh-modes) (transmission-timer-run))) ;; Other (defun transmission-refs (sequence key) "Return a list of the values of KEY in each element of SEQUENCE." (mapcar (lambda (x) (cdr (assq key x))) sequence)) (defun transmission-size (bytes) "Return string showing size BYTES in human-readable form." (file-size-human-readable bytes transmission-units)) (defun transmission-percent (have total) "Return the percentage of HAVE by TOTAL." (if (zerop total) 0 (/ (* 100.0 have) total))) (defun transmission-slice (str k) "Slice STRING into K strings of somewhat equal size. The result can have no more elements than STRING. \n(fn STRING K)" (let ((len (length str))) (let ((quotient (/ len k)) (remainder (% len k)) (i 0) slice result) (while (and (/= 0 (setq len (length str))) (< i k)) (setq slice (if (< i remainder) (1+ quotient) quotient)) (push (substring str 0 (min slice len)) result) (setq str (substring str (min slice len) len)) (cl-incf i)) (nreverse result)))) (defun transmission-text-property-all (beg end prop) "Return a list of non-nil values of a text property PROP between BEG and END. If none are found, return nil." (let (res pos) (save-excursion (goto-char beg) (while (> end (point)) (push (get-text-property (point) prop) res) (setq pos (text-property-not-all (point) end prop (car-safe res))) (goto-char (or pos end)))) (nreverse (delq nil res)))) (defun transmission-eta (seconds percent) "Return a string showing SECONDS in human-readable form; otherwise some other estimate indicated by SECONDS and PERCENT." (if (<= seconds 0) (cond ((= percent 1) "Done") ((char-displayable-p ?∞) (eval-when-compile (char-to-string ?∞))) (t "Inf")) (let* ((minute 60.0) (hour 3600.0) (day 86400.0) (month (* 29.53 day)) (year (* 365.25 day))) (apply #'format "%.0f%s" (cond ((> minute seconds) (list seconds "s")) ((> hour seconds) (list (/ seconds minute) "m")) ((> day seconds) (list (/ seconds hour) "h")) ((> month seconds) (list (/ seconds day) "d")) ((> year seconds) (list (/ seconds month) "mo")) (t (list (/ seconds year) "y"))))))) (defun transmission-when (seconds) "The `transmission-eta' of time between `current-time' and SECONDS." (if (<= seconds 0) "never" (let ((secs (- seconds (float-time (current-time))))) (format (if (< secs 0) "%s ago" "in %s") (transmission-eta (abs secs) nil))))) (defun transmission-rate (bytes) "Return a rate in units kilobytes per second. The rate is calculated from BYTES according to `transmission-units'." (/ bytes (if (eq 'iec transmission-units) 1024 1000))) (defun transmission-throttle-torrent (ids limit n) "Set transfer speed limit for IDS. LIMIT is a symbol; either uploadLimit or downloadLimit. N is the desired threshold. A negative value of N means to disable the limit." (cl-assert (memq limit '(uploadLimit downloadLimit))) (let* ((limit (intern (concat ":" (symbol-name limit)))) (limited (intern (concat (symbol-name limit) "ed"))) (arguments `(:ids ,ids ,@(if (< n 0) `(,limited :json-false) `(,limited t ,limit ,n))))) (transmission-request-async nil "torrent-set" arguments))) (defun transmission-set-torrent-speed-limit (ids d) "Set speed limit of torrents IDS. Direction D should be a symbol, either \"up\" or \"down\"." (cl-assert (memq d '(up down))) (let* ((str (concat (symbol-name d) "loadLimit")) (limit (intern str)) (limited (intern (concat str "ed")))) (if (cdr ids) (let ((prompt (concat "Set torrents' " (symbol-name d) "load limit: "))) (transmission-throttle-torrent ids limit (read-number prompt))) (transmission-request-async (lambda (response) (let* ((torrent (elt (transmission-torrents response) 0)) (n (cdr (assq limit torrent))) (throttle (eq t (cdr (assq limited torrent)))) (prompt (concat "Set torrent's " (symbol-name d) "load limit (" (if throttle (format "%d kB/s" n) "disabled") "): "))) (transmission-throttle-torrent ids limit (read-number prompt)))) "torrent-get" `(:ids ,ids :fields [,str ,(concat str "ed")]))))) (defun transmission-torrent-honors-speed-limits-p () "Return non-nil if torrent honors session speed limits, otherwise nil." (let ((torrent (elt transmission-torrent-vector 0))) (eq t (cdr (assq 'honorsSessionLimits torrent))))) (defun transmission-prompt-speed-limit (upload) "Make a prompt to set transfer speed limit. If UPLOAD is non-nil, make a prompt for upload rate, otherwise for download rate." (let-alist (transmission-request "session-get") (let ((limit (if upload .speed-limit-up .speed-limit-down)) (enabled (eq t (if upload .speed-limit-up-enabled .speed-limit-down-enabled)))) (list (read-number (concat "Set global " (if upload "up" "down") "load limit (" (if enabled (format "%d kB/s" limit) "disabled") "): ")))))) (defun transmission-prompt-ratio-limit () "Make a prompt to set global seed ratio limit." (let-alist (transmission-request "session-get") (let ((limit .seedRatioLimit) (enabled (eq t .seedRatioLimited))) (list (read-number (concat "Set global seed ratio limit (" (if enabled (format "%.1f" limit) "disabled") "): ")))))) (defun transmission-read-strings (prompt &optional collection history filter) "Read strings until an input is blank, with optional completion. PROMPT, COLLECTION, and HISTORY are the same as in `completing-read'. FILTER is a predicate that prevents adding failing input to HISTORY. Returns a list of non-blank inputs." (let ((history-add-new-input (null history)) res entry) (catch :finished (while t (setq entry (if (not collection) (read-string prompt nil history) (completing-read prompt collection nil nil nil history))) (if (and (not (string-empty-p entry)) (not (string-blank-p entry))) (progn (when (and history (or (null filter) (funcall filter entry))) (add-to-history history entry)) (push entry res) (when (consp collection) (setq collection (delete entry collection)))) (throw :finished (nreverse res))))))) (defun transmission-read-time (prompt) "Read an expression for time, prompting with string PROMPT. Uses `diary-entry-time' to parse user input. Returns minutes from midnight, otherwise nil." (let ((hhmm (diary-entry-time (read-string prompt)))) (when (>= hhmm 0) (+ (% hhmm 100) (* 60 (/ hhmm 100)))))) (defun transmission-format-minutes (minutes) "Return a formatted string from MINUTES from midnight." (format-time-string "%H:%M" (seconds-to-time (* 60 (+ 300 minutes))))) (defun transmission-n->days (n) "Return days corresponding to bitfield N. Days are the keys of `transmission-schedules'." (cond ((let ((cell (rassq n transmission-schedules))) (when cell (list (car cell))))) ((let (res) (pcase-dolist (`(,k . ,v) transmission-schedules) (unless (zerop (logand n v)) (push k res) (cl-decf n v))) (nreverse res))))) (defun transmission-levi-civita (a b c) "Return Levi-Civita symbol value for three numbers A, B, C." (cond ((or (< a b c) (< b c a) (< c a b)) 1) ((or (< c b a) (< a c b) (< b a c)) -1) ((or (= a b) (= b c) (= c a)) 0))) (defun transmission-turtle-when (beg end &optional now) "Calculate the time in seconds until the next schedule change. BEG END are minutes after midnight of schedules start and end. NOW is a time, defaulting to `current-time'." (let* ((time (or now (current-time))) (hours (string-to-number (format-time-string "%H" time))) (minutes (+ (* 60 hours) (string-to-number (format-time-string "%M" time))))) (pcase (transmission-levi-civita minutes beg end) (1 (* 60 (if (> beg minutes) (- beg minutes) (+ beg minutes)))) (-1 (* 60 (if (> end minutes) (- end minutes) (+ end minutes)))) ;; FIXME this should probably just return 0 because of inaccuracy (0 (* 60 (or (and (= minutes beg) end) (and (= minutes end) beg))))))) (defun transmission-tracker-url-p (str) "Return non-nil if STR is not just a number." (let ((match (string-match "[^[:blank:]]" str))) (when match (null (<= ?0 (aref str match) ?9))))) (defun transmission-tracker-stats (id) "Return the \"trackerStats\" array for torrent id ID." (let* ((arguments `(:ids ,id :fields ["trackerStats"])) (response (transmission-request "torrent-get" arguments))) (cdr (assq 'trackerStats (elt (transmission-torrents response) 0))))) (defun transmission-unique-announce-urls () "Return a list of unique announce URLs from all current torrents." (let ((response (transmission-request "torrent-get" '(:fields ["trackers"]))) torrents trackers res) (dotimes (i (length (setq torrents (transmission-torrents response)))) (dotimes (j (length (setq trackers (cdr (assq 'trackers (aref torrents i)))))) (cl-pushnew (cdr (assq 'announce (aref trackers j))) res :test #'equal))) res)) (defun transmission-btih-p (string) "Return non-nil if STRING is a BitTorrent info hash, otherwise nil." (and string (string-match (rx bos (= 40 xdigit) eos) string) string)) (defun transmission-directory-name-p (name) "Return non-nil if NAME ends with a directory separator character." (let ((len (length name)) (last ?.)) (if (> len 0) (setq last (aref name (1- len)))) (or (= last ?/) (and (memq system-type '(windows-nt ms-dos)) (= last ?\\))))) (defun transmission-ffap () "Return a file name, URL, or info hash at point, otherwise nil." (or (get-text-property (point) 'shr-url) (get-text-property (point) :nt-link) (let ((fn (run-hook-with-args-until-success 'file-name-at-point-functions))) (unless (transmission-directory-name-p fn) fn)) (url-get-url-at-point) (transmission-btih-p (thing-at-point 'word)))) (defun transmission-ffap-string (string) "Apply `transmission-ffap' to the beginning of STRING." (when string (with-temp-buffer (insert string) (goto-char (point-min)) (transmission-ffap)))) (defun transmission-ffap-last-killed () "Apply `transmission-ffap' to the most recent `kill-ring' entry." (transmission-ffap-string (car kill-ring))) (defun transmission-ffap-selection () "Apply `transmission-ffap' to the graphical selection." (transmission-ffap-string (with-no-warnings (x-get-selection)))) (defun transmission-files-do (action) "Apply ACTION to files in `transmission-files-mode' buffers." (cl-assert (memq action transmission-file-symbols)) (let ((id transmission-torrent-id) (prop 'tabulated-list-id) indices) (setq indices (or transmission-marked-ids (if (null (use-region-p)) (list (cdr (assq 'index (get-text-property (point) prop)))) (transmission-refs (transmission-text-property-all (region-beginning) (region-end) prop) 'index)))) (if (and id indices) (let ((arguments (list :ids id action indices))) (transmission-request-async nil "torrent-set" arguments)) (user-error "No files selected or at point")))) (defun transmission-files-file-at-point () "Return the absolute path of the torrent file at point, or nil. If the file named \"foo\" does not exist, try \"foo.part\" before returning." (let* ((dir (cdr (assq 'downloadDir (elt transmission-torrent-vector 0)))) (base (or (and dir (cdr (assq 'name (tabulated-list-get-id)))) (user-error "No file at point"))) (filename (and base (expand-file-name base dir)))) (or (file-exists-p filename) (let ((part (concat filename ".part"))) (and (file-exists-p part) (setq filename part)))) (if filename (abbreviate-file-name filename) (user-error "File does not exist")))) (defun transmission-files-index (torrent) "Return a list derived from the \"files\" and \"fileStats\" arrays in TORRENT. The two are spliced together with indices for each file, sorted by file name." (let* ((alist (elt torrent 0)) (files (cdr (assq 'files alist))) (stats (cdr (assq 'fileStats alist))) (n (length files)) (res (make-vector n 0))) (dotimes (i n) (aset res i (append (aref files i) (aref stats i) (list (cons 'index i))))) res)) (defun transmission-files-prefix (files) "Return a string that is a prefix of every filename in FILES, otherwise nil." (when (< 0 (length files)) (let* ((filename (cdr (assq 'name (aref files 0)))) (index (and (stringp filename) (string-match "/" filename))) (dir (and index (substring filename 0 (match-end 0))))) (when (and dir (cl-loop for file across files always (string-prefix-p dir (cdr (assq 'name file))))) dir)))) (defun transmission-geoiplookup (ip) "Return country name associated with IP using geoiplookup(1)." (let ((program (if (string-match-p ":" ip) "geoiplookup6" "geoiplookup"))) (when (executable-find program) (with-temp-buffer (call-process program nil t nil ip) (car (last (split-string (buffer-string) ": " t "[ \t\r\n]*"))))))) (defun transmission-geoip-retrieve (ip) "Retrieve value of IP in `transmission-geoip-table'. If IP is not a key, add it with the value from `transmission-geoip-function'. If `transmission-geoip-function' has changed, reset `transmission-geoip-table'." (let ((fun transmission-geoip-function) (cache transmission-geoip-table)) (when (functionp fun) (if (not transmission-geoip-use-cache) (funcall fun ip) (if (eq fun (get 'transmission-geoip-table :fn)) (or (gethash ip cache) (setf (gethash ip cache) (funcall fun ip))) (setq cache (make-hash-table :test 'equal)) (put 'transmission-geoip-table :fn fun) (setf (gethash ip cache) (funcall fun ip))))))) (defun transmission-time (seconds) "Format a time string, given SECONDS from the epoch." (if (= 0 seconds) "Never" (format-time-string transmission-time-format (seconds-to-time seconds) transmission-time-zone))) (defun transmission-hamming-weight (byte) "Calculate the Hamming weight of BYTE." (setq byte (- byte (logand (lsh byte -1) #x55555555))) (setq byte (+ (logand byte #x33333333) (logand (lsh byte -2) #x33333333))) (lsh (* (logand (+ byte (lsh byte -4)) #x0f0f0f0f) #x01010101) -24)) (defun transmission-count-bits (bytearray) "Calculate sum of Hamming weight of each byte in BYTEARRAY." (cl-loop for x across bytearray sum (transmission-hamming-weight x))) (defun transmission-byte->string (byte) "Format integer BYTE into a string." (let* ((calc-number-radix 2) (string (math-format-binary byte))) (concat (make-string (- 8 (length string)) ?0) string))) (defun transmission-ratio->glyph (ratio) "Return a single-char string representing RATIO." (char-to-string (cond ((= 0 ratio) #x20) ((< ratio 0.333) #x2591) ((< ratio 0.667) #x2592) ((< ratio 1) #x2593) ((= 1 ratio) #x2588)))) (defun transmission-ratio->256 (ratio) "Return a grey font-locked single-space string according to RATIO. Uses color names for the 256 color palette." (let ((n (if (= 1 ratio) 231 (+ 236 (* 19 ratio))))) (propertize " " 'font-lock-face `(:background ,(format "color-%d" n))))) (defun transmission-ratio->grey (ratio) "Return a grey font-locked single-space string according to RATIO." (let ((l (+ 0.2 (* 0.8 ratio)))) (propertize " " 'font-lock-face `(:background ,(color-rgb-to-hex l l l)) 'help-echo (format "%.2f" ratio)))) (defun transmission-torrent-seed-ratio (mode tlimit) "String showing a torrent's seed ratio limit. MODE is which seed ratio to use; TLIMIT is the torrent-level limit." (pcase mode (0 "session limit") (1 (format "%.2f (torrent-specific limit)" tlimit)) (2 "unlimited"))) (defun transmission-group-digits (n) "Group digits of positive number N with `transmission-digit-delimiter'." (if (< n 10000) (number-to-string n) (let ((calc-group-char transmission-digit-delimiter)) (math-group-float (number-to-string n))))) (defun transmission-plural (n s) "Return a pluralized string expressing quantity N of thing S. Done in the spirit of `dired-plural-s'." (let ((m (if (= -1 n) 0 n))) (concat (transmission-group-digits m) " " s (when (/= m 1) "s")))) (defun transmission-format-size (bytes) "Format size BYTES into a more readable string." (format "%s (%s bytes)" (transmission-size bytes) (transmission-group-digits bytes))) (defun transmission-toggle-mark-at-point () "Toggle mark of item at point. Registers the change in `transmission-marked-ids'." (let* ((eid (tabulated-list-get-id)) (id (cdr (or (assq 'id eid) (assq 'index eid))))) (if (memq id transmission-marked-ids) (progn (setq transmission-marked-ids (delete id transmission-marked-ids)) (tabulated-list-put-tag " ")) (push id transmission-marked-ids) (tabulated-list-put-tag ">")) (set-buffer-modified-p nil))) (defun transmission-move-to-file-name () "Move to the beginning of the filename on the current line." (let* ((eol (line-end-position)) (change (next-single-property-change (point) 'transmission-name nil eol))) (when (and change (< change eol)) (goto-char change)))) (defun transmission-file-name-matcher (limit) (let ((beg (next-single-property-change (point) 'transmission-name nil limit))) (when (and beg (< beg limit)) (goto-char beg) (let ((end (next-single-property-change (point) 'transmission-name nil limit))) (when (and end (<= end limit)) (set-match-data (list beg end)) (goto-char end)))))) (defmacro transmission-interactive (&rest spec) "Specify interactive use of a function. The symbol `ids' is bound to a list of torrent IDs marked, at point or in region, otherwise a `user-error' is signalled." (declare (debug t)) (let ((region (make-symbol "region")) (marked (make-symbol "marked")) (torrent (make-symbol "torrent"))) `(interactive (let ((,torrent transmission-torrent-id) ,marked ,region ids) (setq ids (or (and ,torrent (list ,torrent)) (setq ,marked transmission-marked-ids))) (when (null ids) (if (setq ,region (use-region-p)) (setq ids (cl-loop for x in (transmission-text-property-all (region-beginning) (region-end) 'tabulated-list-id) collect (cdr (assq 'id x)))) (let ((value (get-text-property (point) 'tabulated-list-id))) (when value (setq ids (list (cdr (assq 'id value)))))))) (if (null ids) (user-error "No torrent selected") ,@(cl-labels ((expand (form x) (cond ((atom form) form) ((and (listp form) (memq (car form) '(read-number y-or-n-p yes-or-no-p completing-read transmission-read-strings))) (pcase form (`(read-number ,prompt . ,rest) `(read-number (concat ,prompt ,x) ,@rest)) (`(y-or-n-p ,prompt) `(y-or-n-p (concat ,prompt ,x))) (`(yes-or-no-p ,prompt) `(yes-or-no-p (concat ,prompt ,x))) (`(completing-read ,prompt . ,rest) `(completing-read (concat ,prompt ,x) ,@rest)) (`(transmission-read-strings ,prompt . ,rest) `(transmission-read-strings (concat ,prompt ,x) ,@rest)))) ((or (listp form) (null form)) (mapcar (lambda (subexp) (expand subexp x)) form)) (t (error "Bad syntax: %S" form))))) (expand spec `(cond (,marked (format "[%d marked] " (length ,marked))) (,region (format "[%d in region] " (length ids))))))))))) (defun transmission-collect-hook (hook &rest args) "Run HOOK with ARGS and return a list of non-nil results from its elements." (let (res) (cl-flet ((collect (fun &rest args) (let ((val (apply fun args))) (when val (cl-pushnew val res :test #'equal)) nil))) (apply #'run-hook-wrapped hook #'collect args) (nreverse res)))) (defmacro transmission-with-window-maybe (window &rest body) "If WINDOW is non-nil, execute BODY with WINDOW current. Otherwise, just execute BODY." (declare (indent 1) (debug t)) `(if (null ,window) (progn ,@body) (with-selected-window ,window ,@body))) (defun transmission-window->state (window) "Return a list containing some state of WINDOW. A simplification of `window-state-get', the list associates WINDOW with `window-start' and the line/column coordinates of `point'." (transmission-with-window-maybe window (save-restriction (widen) (list window (window-start) (line-number-at-pos) (current-column))))) (defun transmission-restore-state (state) "Set `window-start' and `window-point' according to STATE." (pcase-let ((`(,window ,start ,line ,column) state)) (transmission-with-window-maybe window (goto-char (point-min)) (goto-char (point-at-bol line)) (move-to-column column) (setf (window-start) start)))) (defmacro transmission-with-saved-state (&rest body) "Execute BODY, restoring window position, point, and mark." (declare (indent 0) (debug t)) (let ((old-states (make-symbol "old-states")) (old-mark (make-symbol "old-mark")) (old-mark-active (make-symbol "old-mark-active"))) `(let* ((,old-states (or (mapcar #'transmission-window->state (get-buffer-window-list nil nil t)) (list (transmission-window->state nil)))) (,old-mark (if (not (region-active-p)) (mark) (let ((beg (region-beginning))) (if (= (window-point) beg) (region-end) beg)))) (,old-mark-active mark-active)) ,@body (mapc #'transmission-restore-state ,old-states) (and ,old-mark (set-mark ,old-mark)) (unless ,old-mark-active (deactivate-mark))))) ;; Interactive ;;;###autoload (defun transmission-add (torrent &optional directory) "Add TORRENT by filename, URL, magnet link, or info hash. When called with a prefix, prompt for DIRECTORY." (interactive (let* ((f (transmission-collect-hook 'transmission-torrent-functions)) (def (mapcar #'file-relative-name f)) (prompt (concat "Add torrent" (if def (format " [%s]" (car def))) ": ")) (history-add-new-input nil) (file-name-history (symbol-value transmission-add-history-variable)) (input (read-file-name prompt nil def))) (add-to-history transmission-add-history-variable input) (list input (if current-prefix-arg (read-directory-name "Target directory: "))))) (transmission-request-async (lambda (response) (let-alist response (or (and .torrent-added.name (message "Added %s" .torrent-added.name)) (and .torrent-duplicate.name (message "Already added %s" .torrent-duplicate.name))))) "torrent-add" (append (if (and (file-readable-p torrent) (not (file-directory-p torrent))) `(:metainfo ,(with-temp-buffer (insert-file-contents-literally torrent) (base64-encode-string (buffer-string) t))) (setq torrent (string-trim torrent)) `(:filename ,(if (transmission-btih-p torrent) (concat "magnet:?xt=urn:btih:" torrent) torrent))) (when directory (list :download-dir (expand-file-name directory)))))) (defun transmission-free (directory) "Show in the echo area how much free space is in DIRECTORY." (interactive (list (read-directory-name "Directory: " nil nil t))) (transmission-request-async (lambda (response) (let-alist response (message "%s free in %s" (transmission-format-size .size-bytes) (abbreviate-file-name .path)))) "free-space" (list :path (expand-file-name directory)))) (defun transmission-stats () "Message some information about the session." (interactive) (transmission-request-async (lambda (response) (let-alist response (message (concat "%d kB/s down, %d kB/s up; %d/%d torrents active; " "%s received, %s sent; uptime %s") (transmission-rate .downloadSpeed) (transmission-rate .uploadSpeed) .activeTorrentCount .torrentCount (transmission-size .current-stats.downloadedBytes) (transmission-size .current-stats.uploadedBytes) (transmission-eta .current-stats.secondsActive nil)))) "session-stats")) (defun transmission-move (ids location) "Move torrent at point, marked, or in region to a new LOCATION." (transmission-interactive (let* ((dir (read-directory-name "New directory: ")) (prompt (format "Move torrent%s to %s? " (if (cdr ids) "s" "") dir))) (if (y-or-n-p prompt) (list ids dir) '(nil nil)))) (when ids (let ((arguments (list :ids ids :move t :location (expand-file-name location)))) (transmission-request-async nil "torrent-set-location" arguments)))) (defun transmission-reannounce (ids) "Reannounce torrent at point, marked, or in region." (transmission-interactive (list ids)) (when ids (transmission-request-async nil "torrent-reannounce" (list :ids ids)))) (defun transmission-remove (ids &optional unlink) "Prompt to remove torrent at point or torrents marked or in region. When called with a prefix UNLINK, also unlink torrent data on disk." (transmission-interactive (if (yes-or-no-p (concat "Remove " (and current-prefix-arg "and unlink ") "torrent" (and (< 1 (length ids)) "s") "? ")) (progn (setq deactivate-mark t transmission-marked-ids nil) (list ids current-prefix-arg)) '(nil nil))) (when ids (let ((arguments `(:ids ,ids :delete-local-data ,(and unlink t)))) (transmission-request-async nil "torrent-remove" arguments)))) (defun transmission-delete (ids) "Prompt to delete (unlink) torrent at point or torrents marked or in region." (transmission-interactive (let* ((prompt (concat "Delete torrent" (and (< 1 (length ids)) "s") "? "))) (list (and (yes-or-no-p prompt) (setq transmission-marked-ids nil deactivate-mark t) ids)))) (when ids (transmission-request-async nil "torrent-remove" `(:ids ,ids :delete-local-data t)))) (defun transmission-set-bandwidth-priority (ids priority) "Set bandwidth priority of torrent(s) at point, in region, or marked." (transmission-interactive (let* ((prompt "Set bandwidth priority: ") (priority (completing-read prompt transmission-priority-alist nil t)) (number (cdr (assq (intern priority) transmission-priority-alist)))) (list (when number ids) number))) (when ids (let ((arguments `(:ids ,ids :bandwidthPriority ,priority))) (transmission-request-async nil "torrent-set" arguments)))) (defun transmission-set-download (limit) "Set global download speed LIMIT in kB/s." (interactive (transmission-prompt-speed-limit nil)) (let ((arguments (if (<= limit 0) '(:speed-limit-down-enabled :json-false) `(:speed-limit-down-enabled t :speed-limit-down ,limit)))) (transmission-request-async nil "session-set" arguments))) (defun transmission-set-upload (limit) "Set global upload speed LIMIT in kB/s." (interactive (transmission-prompt-speed-limit t)) (let ((arguments (if (< limit 0) '(:speed-limit-up-enabled :json-false) `(:speed-limit-up-enabled t :speed-limit-up ,limit)))) (transmission-request-async nil "session-set" arguments))) (defun transmission-set-ratio (limit) "Set global seed ratio LIMIT." (interactive (transmission-prompt-ratio-limit)) (let ((arguments (if (< limit 0) '(:seedRatioLimited :json-false) `(:seedRatioLimited t :seedRatioLimit ,limit)))) (transmission-request-async nil "session-set" arguments))) (defun transmission-set-torrent-download (ids) "Set download limit of selected torrent(s) in kB/s." (transmission-interactive (list ids)) (transmission-set-torrent-speed-limit ids 'down)) (defun transmission-set-torrent-upload (ids) "Set upload limit of selected torrent(s) in kB/s." (transmission-interactive (list ids)) (transmission-set-torrent-speed-limit ids 'up)) (defun transmission-set-torrent-ratio (ids mode limit) "Set seed ratio limit of selected torrent(s)." (transmission-interactive (let* ((prompt (concat "Set torrent" (if (cdr ids) "s'" "'s") " ratio mode: ")) (mode (completing-read prompt transmission-mode-alist nil t)) (n (cdr (assq (intern mode) transmission-mode-alist)))) (list ids n (when (= n 1) (read-number "Set torrent ratio limit: "))))) (when ids (let ((arguments `(:ids ,ids :seedRatioMode ,mode))) (when limit (setq arguments (append arguments `(:seedRatioLimit ,limit)))) (transmission-request-async nil "torrent-set" arguments)))) (defun transmission-toggle-limits (ids) "Toggle whether selected torrent(s) honor session speed limits." (transmission-interactive (list ids)) (when ids (transmission-request-async (lambda (response) (let* ((torrents (transmission-torrents response)) (honor (pcase (cdr (assq 'honorsSessionLimits (elt torrents 0))) (:json-false t) (_ :json-false)))) (transmission-request-async nil "torrent-set" `(:ids ,ids :honorsSessionLimits ,honor)))) "torrent-get" `(:ids ,ids :fields ["honorsSessionLimits"])))) (defun transmission-toggle (ids) "Toggle selected torrent(s) between started and stopped." (transmission-interactive (list ids)) (when ids (transmission-request-async (lambda (response) (let* ((torrents (transmission-torrents response)) (status (and (< 0 (length torrents)) (cdr (assq 'status (elt torrents 0))))) (method (and status (if (zerop status) "torrent-start" "torrent-stop")))) (when method (transmission-request-async nil method (list :ids ids))))) "torrent-get" (list :ids ids :fields ["status"])))) (defun transmission-trackers-add (ids urls) "Add announce URLs to selected torrent or torrents." (transmission-interactive (let* ((trackers (transmission-refs (transmission-tracker-stats ids) 'announce)) (urls (or (transmission-read-strings "Add announce URLs: " (cl-loop for url in (append transmission-trackers (transmission-unique-announce-urls)) unless (member url trackers) collect url) transmission-tracker-history-variable #'transmission-tracker-url-p) (user-error "No trackers to add")))) (list ids ;; Don't add trackers that are already there (cl-loop for url in urls unless (member url trackers) collect url)))) (transmission-request-async (lambda (_) (message "Added %s" (mapconcat #'identity urls ", "))) "torrent-set" (list :ids ids :trackerAdd urls))) (defun transmission-trackers-remove () "Remove trackers from torrent at point by ID or announce URL." (interactive) (let* ((id (or transmission-torrent-id (user-error "No torrent selected"))) (array (or (transmission-tracker-stats id) (user-error "No trackers to remove"))) (prompt (format "Remove tracker (%d trackers): " (length array))) (trackers (cl-loop for x across array collect (cons (cdr (assq 'announce x)) (cdr (assq 'id x))))) (completion-extra-properties `(:annotation-function (lambda (x) (format " ID# %d" (cdr (assoc x ',trackers)))))) (urls (or (transmission-read-strings prompt trackers transmission-tracker-history-variable #'transmission-tracker-url-p) (user-error "No trackers selected for removal"))) (tids (cl-loop for alist across array if (or (member (cdr (assq 'announce alist)) urls) (member (number-to-string (cdr (assq 'id alist))) urls)) collect (cdr (assq 'id alist)))) (arguments (list :ids id :trackerRemove tids))) (transmission-request-async (lambda (_) (message "Removed %s" (mapconcat #'identity urls ", "))) "torrent-set" arguments))) (defun transmission-trackers-replace () "Replace tracker by ID or announce URL." (interactive) (let* ((id (or transmission-torrent-id (user-error "No torrent selected"))) (trackers (or (cl-loop for x across (transmission-tracker-stats id) collect (cons (cdr (assq 'announce x)) (cdr (assq 'id x)))) (user-error "No trackers to replace"))) (prompt (format "Replace tracker (%d trackers): " (length trackers))) (tid (or (let* ((completion-extra-properties `(:annotation-function (lambda (x) (format " ID# %d" (cdr (assoc x ',trackers)))))) (tracker (completing-read prompt trackers))) (cl-loop for cell in trackers if (member tracker (list (car cell) (number-to-string (cdr cell)))) return (cdr cell))) (user-error "No tracker selected for substitution"))) (replacement (completing-read "Replacement tracker? " (append transmission-trackers (transmission-unique-announce-urls)) nil nil nil transmission-tracker-history-variable)) (arguments (list :ids id :trackerReplace (vector tid replacement)))) (transmission-request-async (lambda (_) (message "Replaced #%d with %s" tid replacement)) "torrent-set" arguments))) (defun transmission-turtle-set-days (days &optional disable) "Set DAYS on which turtle mode will be active. DAYS is a bitfield, the associations of which are in `transmission-schedules'. Empty input or non-positive DAYS makes no change to the schedule. With a prefix argument, disable turtle mode schedule." (interactive (let-alist (transmission-request "session-get") (let* ((alist transmission-schedules) (prompt (format "Days %s%s: " (or (transmission-n->days .alt-speed-time-day) "(none)") (if (eq t .alt-speed-time-enabled) "" " [disabled]"))) (names (transmission-read-strings prompt alist)) (bits 0)) (dolist (name names) (cl-callf logior bits (cdr (assq (intern name) alist)))) (list bits current-prefix-arg)))) (let ((arguments (append `(:alt-speed-time-enabled ,(if disable json-false t)) (when (> days 0) `(:alt-speed-time-day ,days))))) (transmission-request-async #'transmission-turtle-poll "session-set" arguments))) (defun transmission-turtle-set-times (begin end) "Set BEGIN and END times for turtle mode. See `transmission-read-time' for details on time input." (interactive (let-alist (transmission-request "session-get") (let* ((begs (transmission-format-minutes .alt-speed-time-begin)) (ends (transmission-format-minutes .alt-speed-time-end)) (start (or (transmission-read-time (format "Begin (%s): " begs)) .alt-speed-time-begin)) (stop (or (transmission-read-time (format "End (%s): " ends)) .alt-speed-time-end))) (when (and (= start .alt-speed-time-begin) (= stop .alt-speed-time-end)) (user-error "No change in schedule")) (if (y-or-n-p (format "Set active time from %s to %s? " (transmission-format-minutes start) (transmission-format-minutes stop))) (list start stop) '(nil nil))))) (when (or begin end) (let ((arguments (append (when begin (list :alt-speed-time-begin begin)) (when end (list :alt-speed-time-end end))))) (transmission-request-async #'transmission-turtle-poll "session-set" arguments)))) (defun transmission-turtle-set-speeds (up down) "Set UP and DOWN speed limits (kB/s) for turtle mode." (interactive (let-alist (transmission-request "session-get") (let ((p1 (format "Set turtle upload limit (%d kB/s): " .alt-speed-up)) (p2 (format "Set turtle download limit (%d kB/s): " .alt-speed-down))) (list (read-number p1) (read-number p2))))) (let ((arguments (append (when down (list :alt-speed-down down)) (when up (list :alt-speed-up up))))) (transmission-request-async #'transmission-turtle-poll "session-set" arguments))) (defun transmission-turtle-status () "Message details about turtle mode configuration." (interactive) (transmission-request-async (lambda (response) (let-alist response (message "%sabled; %d kB/s down, %d kB/s up; schedule %sabled, %s-%s, %s" (if (eq .alt-speed-enabled t) "En" "Dis") .alt-speed-down .alt-speed-up (if (eq .alt-speed-time-enabled t) "en" "dis") (transmission-format-minutes .alt-speed-time-begin) (transmission-format-minutes .alt-speed-time-end) (let ((bits (transmission-n->days .alt-speed-time-day))) (if (null bits) "never" (mapconcat #'symbol-name bits " ")))))) "session-get")) (defun transmission-verify (ids) "Verify torrent at point, in region, or marked." (transmission-interactive (if (y-or-n-p (concat "Verify torrent" (when (cdr ids) "s") "? ")) (list ids) '(nil))) (when ids (transmission-request-async nil "torrent-verify" (list :ids ids)))) (defun transmission-queue-move-top (ids) "Move torrent(s)--at point, in region, or marked--to the top of the queue." (transmission-interactive (if (y-or-n-p (concat "Queue torrent" (when (cdr ids) "s") " first? ")) (list ids) '(nil))) (when ids (transmission-request-async nil "queue-move-top" (list :ids ids)))) (defun transmission-queue-move-bottom (ids) "Move torrent(s)--at point, in region, or marked--to the bottom of the queue." (transmission-interactive (if (y-or-n-p (concat "Queue torrent" (when (cdr ids) "s") " last? ")) (list ids) '(nil))) (when ids (transmission-request-async nil "queue-move-bottom" (list :ids ids)))) (defun transmission-queue-move-up (ids) "Move torrent(s)--at point, in region, or marked--up in the queue." (transmission-interactive (if (y-or-n-p (concat "Raise torrent" (when (cdr ids) "s") " in the queue? ")) (list ids) '(nil))) (when ids (transmission-request-async nil "queue-move-up" (list :ids ids)))) (defun transmission-queue-move-down (ids) "Move torrent(s)--at point, in region, or marked--down in the queue." (transmission-interactive (if (y-or-n-p (concat "Lower torrent" (when (cdr ids) "s") " in the queue? ")) (list ids) '(nil))) (when ids (transmission-request-async nil "queue-move-down" (list :ids ids)))) (defun transmission-quit () "Quit and bury the buffer." (interactive) (let ((cur (current-buffer))) (if (cl-loop for list in (window-prev-buffers) never (eq cur (car list))) (quit-window) (if (one-window-p) (bury-buffer) (delete-window))))) (defun transmission-files-unwant () "Mark file(s)--at point, in region, or marked--as unwanted." (interactive) (transmission-files-do :files-unwanted)) (defun transmission-files-want () "Mark file(s)--at point, in region, or marked--as wanted." (interactive) (transmission-files-do :files-wanted)) (defun transmission-files-priority (priority) "Set bandwidth PRIORITY on file(s) at point, in region, or marked." (interactive (list (completing-read "Set priority: " transmission-priority-alist nil t))) (transmission-files-do (intern (concat ":priority-" priority)))) (defun transmission-files-command (command file) "Run a command COMMAND on the FILE at point." (interactive (let* ((fap (run-hook-with-args-until-success 'file-name-at-point-functions)) (fn (replace-regexp-in-string "\\.part\\'" "" fap)) (def (let ((lists (transmission-collect-hook 'transmission-files-command-functions (list fn)))) (delete-dups (apply #'append lists)))) (prompt (and fap (concat "! on " (file-name-nondirectory fap) (when def (format " (default %s)" (car def))) ": "))) (input (read-shell-command prompt nil nil def t))) (if fap (list (if (string-empty-p input) (or (car def) "") input) fap) (user-error "File does not exist")))) (let ((args (nconc (split-string-and-unquote command) (list (expand-file-name file))))) (apply #'start-process (car args) nil args))) (defun transmission-copy-file (file newname &optional ok-if-already-exists) "Copy the file at point to another location. FILE, NEWNAME, and OK-IF-ALREADY-EXISTS are the same as in `copy-file'." (interactive (let* ((f (transmission-files-file-at-point)) (prompt (format "Copy %s to: " (file-name-nondirectory f))) (def (when (bound-and-true-p dired-dwim-target) (buffer-local-value 'default-directory (window-buffer (next-window))))) (new (read-file-name prompt nil def))) (list f new 0))) (copy-file file newname ok-if-already-exists t t t) (message "Copied %s" (file-name-nondirectory file))) (defun transmission-find-file () "Visit the file at point with `find-file-read-only'." (interactive) (find-file-read-only (transmission-files-file-at-point))) (defun transmission-find-file-other-window () "Visit the file at point in another window." (interactive) (find-file-read-only-other-window (transmission-files-file-at-point))) (defun transmission-display-file () "Display the file at point in another window." (interactive) (let ((buf (find-file-noselect (transmission-files-file-at-point)))) (with-current-buffer buf (read-only-mode 1)) (display-buffer buf t))) (defun transmission-view-file () "Examine the file at point in view mode." (interactive) (view-file (transmission-files-file-at-point))) (defun transmission-browse-url-of-file () "Browse file at point in a WWW browser." (interactive) (browse-url-of-file (expand-file-name (transmission-files-file-at-point)))) (defun transmission-copy-filename-as-kill (&optional arg) "Copy name of file at point into the kill ring. With a prefix argument, use the absolute file name." (interactive "P") (let* ((fn (transmission-files-file-at-point)) (str (if arg fn (file-name-nondirectory fn)))) (if (eq last-command 'kill-region) (kill-append str nil) (kill-new str)) (message "%S" str))) (defun transmission-copy-magnet () "Copy magnet link of current torrent." (interactive) (let ((magnet (cdr (assq 'magnetLink (elt transmission-torrent-vector 0))))) (when magnet (kill-new magnet) (message "Copied %s" magnet)))) (defun transmission-toggle-mark (arg) "Toggle mark of item(s) at point. If the region is active, toggle the mark on all items in the region. Otherwise, with a prefix arg, mark files on the next ARG lines." (interactive "p") (if (use-region-p) (save-excursion (save-restriction (narrow-to-region (region-beginning) (region-end)) (goto-char (point-min)) (while (not (eobp)) (transmission-toggle-mark-at-point) (forward-line)))) (while (and (> arg 0) (not (eobp))) (cl-decf arg) (transmission-toggle-mark-at-point) (forward-line 1)) (while (and (< arg 0) (not (bobp))) (cl-incf arg) (forward-line -1) (transmission-toggle-mark-at-point)))) (defun transmission-unmark-all () "Remove mark from all items." (interactive) (let ((inhibit-read-only t) len n) (when (> (setq n (setq len (length transmission-marked-ids))) 0) (save-excursion (save-restriction (widen) (goto-char (point-min)) (catch :eobp (while (> n 0) (when (= (following-char) ?>) (save-excursion (forward-char) (insert-and-inherit ?\s)) (delete-region (point) (1+ (point))) (cl-decf n)) (when (not (zerop (forward-line))) (throw :eobp nil)))))) (setq transmission-marked-ids nil) (set-buffer-modified-p nil) (message "%s removed" (transmission-plural len "mark"))))) (defun transmission-invert-marks () "Toggle mark on all items." (interactive) (let ((inhibit-read-only t) ids tag key) (when (setq key (cl-ecase major-mode (transmission-mode 'id) (transmission-files-mode 'index))) (save-excursion (save-restriction (widen) (goto-char (point-min)) (catch :eobp (while t (when (setq tag (car (memq (following-char) '(?> ?\s)))) (save-excursion (forward-char) (insert-and-inherit (if (= tag ?>) ?\s ?>))) (delete-region (point) (1+ (point))) (when (= tag ?\s) (push (cdr (assq key (tabulated-list-get-id))) ids))) (when (not (zerop (forward-line))) (throw :eobp nil)))))) (setq transmission-marked-ids ids) (set-buffer-modified-p nil)))) ;; Turtle mode (defvar transmission-turtle-poll-callback (let (timer enabled next lighter) (lambda (response) (let-alist response (setq enabled (eq t .alt-speed-enabled)) (setq next (transmission-turtle-when .alt-speed-time-begin .alt-speed-time-end)) (set-default 'transmission-turtle-mode enabled) (setq lighter (if enabled (concat transmission-turtle-lighter (format ":%d/%d" .alt-speed-down .alt-speed-up)) nil)) (transmission-register-turtle-mode lighter) (when timer (cancel-timer timer)) (setq timer (run-at-time next nil #'transmission-turtle-poll))))) "Closure checking turtle mode status and marshaling a timer.") (defun transmission-turtle-poll (&rest _args) "Initiate `transmission-turtle-poll-callback' timer function." (transmission-request-async transmission-turtle-poll-callback "session-get" '(:fields ["alt-speed-enabled" "alt-speed-down" "alt-speed-up" "alt-speed-time-begin" "alt-speed-time-end"]))) (defvar transmission-turtle-mode-lighter nil "Lighter for `transmission-turtle-mode'.") (define-minor-mode transmission-turtle-mode "Toggle alternative speed limits (turtle mode). Indicates on the mode-line the down/up speed limits in kB/s." :group 'transmission :global t :lighter transmission-turtle-mode-lighter (transmission-request-async #'transmission-turtle-poll "session-set" `(:alt-speed-enabled ,(or transmission-turtle-mode json-false)))) (defun transmission-register-turtle-mode (lighter) "Add LIGHTER to buffers with a transmission-* major mode." (dolist (buf (buffer-list)) (with-current-buffer buf (when (string-prefix-p "transmission" (symbol-name major-mode)) (setq-local transmission-turtle-mode-lighter lighter))))) ;; Formatting (defun transmission-format-status (status up down) "Return a propertized string describing torrent status. STATUS is a value in `transmission-status-alist'. UP and DOWN are transmission rates." (let ((state (symbol-name (car (rassq status transmission-status-alist)))) (idle (propertize "idle" 'font-lock-face 'shadow)) (uploading (propertize "uploading" 'font-lock-face 'font-lock-constant-face))) (pcase status (0 (propertize state 'font-lock-face 'warning)) ((or 1 3 5) (propertize state 'font-lock-face '(bold shadow))) (2 (propertize state 'font-lock-face 'font-lock-function-name-face)) (4 (if (> down 0) (propertize state 'font-lock-face 'highlight) (if (> up 0) uploading idle))) (6 (if (> up 0) (propertize state 'font-lock-face 'success) idle)) (_ state)))) (defun transmission-format-pieces (pieces count) "Format into a string the bitfield PIECES holding COUNT boolean flags." (let* ((bytes (base64-decode-string pieces)) (bits (mapconcat #'transmission-byte->string bytes ""))) (cl-flet ((string-partition (s n) (let (res middle last) (while (not (zerop (setq last (length s)))) (setq middle (min n last)) (push (substring s 0 middle) res) (setq s (substring s middle last))) (nreverse res)))) (string-join (string-partition (substring bits 0 count) 72) "\n")))) (defun transmission-format-pieces-brief (pieces count) "Format pieces into a one-line greyscale representation. PIECES and COUNT are the same as in `transmission-format-pieces'." (let* ((bytes (base64-decode-string pieces)) (slices (transmission-slice bytes 72)) (ratios (cl-loop for bv in slices with div = nil do (cl-decf count (setq div (min count (* 8 (length bv))))) collect (/ (transmission-count-bits bv) (float div))))) (mapconcat (pcase (display-color-cells) ((pred (< 256)) #'transmission-ratio->grey) (256 #'transmission-ratio->256) (_ #'transmission-ratio->glyph)) ratios ""))) (defun transmission-format-pieces-internal (pieces count size) "Format piece data into a string. PIECES and COUNT are the same as in `transmission-format-pieces'. SIZE is the file size in bytes of a single piece." (let ((have (cl-loop for b across (base64-decode-string pieces) sum (transmission-hamming-weight b)))) (concat "Piece count: " (transmission-group-digits have) " / " (transmission-group-digits count) " (" (format "%.1f" (transmission-percent have count)) "%) * " (transmission-format-size size) " each" (when (and (functionp transmission-pieces-function) (/= have 0) (< have count)) (let ((str (funcall transmission-pieces-function pieces count))) (concat "\nPieces:\n\n" str)))))) (defun transmission-format-peers (peers origins connected sending receiving) "Format peer information into a string. PEERS is an array of peer-specific data. ORIGINS is an alist giving counts of peers from different swarms. CONNECTED, SENDING, RECEIVING are numbers." (cl-macrolet ((accumulate (array key) `(cl-loop for alist across ,array if (eq t (cdr (assq ,key alist))) sum 1))) (if (zerop connected) "Peers: none connected\n" (concat (format "Peers: %d connected, uploading to %d, downloading from %d" connected sending receiving) (format " (%d unchoked, %d interested)\n" (- connected (accumulate peers 'clientIsChoked)) (accumulate peers 'peerIsInterested)) (format "Peer origins: %s\n" (string-join (cl-loop with x = 0 for cell in origins for src across ["cache" "DHT" "incoming" "LPD" "LTEP" "PEX" "tracker(s)"] if (not (zerop (setq x (cdr cell)))) collect (format "%d from %s" x src)) ", ")))))) (defun transmission-format-tracker (tracker) "Format alist TRACKER into a string of tracker info." (let-alist tracker (let* ((label (format "Tracker %d" .id)) (col (length label)) (fill (propertize (make-string col ?\s) 'display `(space :align-to ,col))) (result (unless (member .lastAnnounceResult '("Success" "")) (concat "\n" fill ": " (propertize .lastAnnounceResult 'font-lock-face 'warning))))) (format (concat label ": %s (Tier %d)\n" fill ": %s %s. Announcing %s\n" fill ": %s, %s, %s %s. Scraping %s" result) .announce .tier (transmission-plural .lastAnnouncePeerCount "peer") (transmission-when .lastAnnounceTime) (transmission-when .nextAnnounceTime) (transmission-plural .seederCount "seeder") (transmission-plural .leecherCount "leecher") (transmission-plural .downloadCount "download") (transmission-when .lastScrapeTime) (transmission-when .nextScrapeTime))))) (defun transmission-format-trackers (trackers) "Format tracker information into a string. TRACKERS should be the \"trackerStats\" array." (if (zerop (length trackers)) "Trackers: none\n" (concat (mapconcat #'transmission-format-tracker trackers "\n") "\n"))) (defun transmission-format-speed-limit (speed limit limited) "Format speed limit data into a string. SPEED and LIMIT are rates in bytes per second. LIMITED, if t, indicates that the speed limit is enabled." (cond ((not (eq limited t)) (format "%d kB/s" (transmission-rate speed))) (t (format "%d / %d kB/s" (transmission-rate speed) limit)))) (defun transmission-format-limits (session rx tx rx-lim tx-lim rx-thr tx-thr) "Format download and upload rate and limits into a string." (concat (transmission-format-speed-limit rx rx-lim rx-thr) " down, " (transmission-format-speed-limit tx tx-lim tx-thr) " up" (when (eq session t) ", session limited"))) ;; Drawing (defun transmission-tabulated-list-format (&optional _arg _noconfirm) "Initialize tabulated-list header or update `tabulated-list-format'." (let ((idx (cl-loop for format across tabulated-list-format if (plist-get (nthcdr 3 format) :transmission-size) return format)) (col (if (eq 'iec transmission-units) 9 7))) (if (= (cadr idx) col) (or header-line-format (tabulated-list-init-header)) (setf (cadr idx) col) (tabulated-list-init-header)))) (defmacro transmission-do-entries (seq &rest body) "Map over SEQ to generate a new value of `tabulated-list-entries'. Each form in BODY is a column descriptor." (declare (indent 1) (debug t)) (let ((res (make-symbol "res"))) `(let (,res) (mapc (lambda (x) (let-alist x (push (list x (vector ,@body)) ,res))) ,seq) (setq tabulated-list-entries (nreverse ,res))))) (defun transmission-draw-torrents (_id) (let* ((arguments `(:fields ,transmission-draw-torrents-keys)) (response (transmission-request "torrent-get" arguments))) (setq transmission-torrent-vector (transmission-torrents response))) (transmission-do-entries transmission-torrent-vector (transmission-eta .eta .percentDone) (transmission-size .sizeWhenDone) (format "%d%%" (* 100 (if (= 1 .metadataPercentComplete) .percentDone .metadataPercentComplete))) (format "%d" (transmission-rate .rateDownload)) (format "%d" (transmission-rate .rateUpload)) (format "%.1f" (if (> .uploadRatio 0) .uploadRatio 0)) (if (not (zerop .error)) (propertize "error" 'font-lock-face 'error) (transmission-format-status .status .rateUpload .rateDownload)) (concat (propertize .name 'transmission-name t) (mapconcat (lambda (l) (concat " " (propertize l 'font-lock-face 'font-lock-constant-face))) .labels ""))) (tabulated-list-print)) (defun transmission-draw-files (id) (let* ((arguments `(:ids ,id :fields ,transmission-draw-files-keys)) (response (transmission-request "torrent-get" arguments))) (setq transmission-torrent-vector (transmission-torrents response))) (let* ((files (transmission-files-index transmission-torrent-vector)) (prefix (transmission-files-prefix files))) (transmission-do-entries files (format "%d%%" (transmission-percent .bytesCompleted .length)) (symbol-name (car (rassq .priority transmission-priority-alist))) (if (eq .wanted :json-false) "no" "yes") (transmission-size .length) (propertize (if prefix (string-remove-prefix prefix .name) .name) 'transmission-name t))) (tabulated-list-print)) (defmacro transmission-insert-each-when (&rest body) "Insert each non-nil form in BODY sequentially on its own line." (declare (indent 0) (debug t)) (let ((tmp (make-symbol "tmp"))) (cl-loop for form in body collect `(when (setq ,tmp ,form) (insert ,tmp "\n")) into res finally return `(let (,tmp) ,@res)))) (defun transmission-draw-info (id) (let* ((arguments `(:ids ,id :fields ,transmission-draw-info-keys)) (response (transmission-request "torrent-get" arguments))) (setq transmission-torrent-vector (transmission-torrents response))) (erase-buffer) (let-alist (elt transmission-torrent-vector 0) (transmission-insert-each-when (format "ID: %d" id) (concat "Name: " .name) (concat "Hash: " .hashString) (concat "Magnet: " (propertize .magnetLink 'font-lock-face 'link)) (if (zerop (length .labels)) "" (concat "Labels: " (mapconcat #'identity .labels ", ") "\n")) (concat "Location: " (abbreviate-file-name .downloadDir)) (let* ((percent (* 100 .percentDone)) (fmt (if (zerop (mod percent 1)) "%d" "%.2f"))) (concat "Percent done: " (format fmt percent) "%")) (format "Bandwidth priority: %s" (car (rassq .bandwidthPriority transmission-priority-alist))) (format "Queue position: %d" .queuePosition) (concat "Speed: " (transmission-format-limits .honorsSessionLimits .rateDownload .rateUpload .downloadLimit .uploadLimit .downloadLimited .uploadLimited)) (format "Ratio: %.3f / %s" (if (= .uploadRatio -1) 0 .uploadRatio) (transmission-torrent-seed-ratio .seedRatioMode .seedRatioLimit)) (unless (zerop .error) (concat "Error: " (propertize .errorString 'font-lock-face 'error))) (transmission-format-peers .peers .peersFrom .peersConnected .peersGettingFromUs .peersSendingToUs) (concat "Date created: " (transmission-time .dateCreated)) (concat "Date added: " (transmission-time .addedDate)) (concat "Date finished: " (transmission-time .doneDate)) (concat "Latest Activity: " (transmission-time .activityDate) "\n") (transmission-format-trackers .trackerStats) (concat "Wanted: " (transmission-format-size .sizeWhenDone)) (concat "Downloaded: " (transmission-format-size .downloadedEver)) (concat "Verified: " (transmission-format-size .haveValid)) (unless (zerop .corruptEver) (concat "Corrupt: " (transmission-format-size .corruptEver))) (concat "Total size: " (transmission-format-size .totalSize)) (transmission-format-pieces-internal .pieces .pieceCount .pieceSize)))) (defun transmission-draw-peers (id) (let* ((arguments `(:ids ,id :fields ["peers"])) (response (transmission-request "torrent-get" arguments))) (setq transmission-torrent-vector (transmission-torrents response))) (transmission-do-entries (cdr (assq 'peers (elt transmission-torrent-vector 0))) .address .flagStr (format "%d%%" (transmission-percent .progress 1.0)) (format "%d" (transmission-rate .rateToClient)) (format "%d" (transmission-rate .rateToPeer)) .clientName (or (transmission-geoip-retrieve .address) "")) (tabulated-list-print)) (defun transmission-draw () "Draw the buffer with new contents via `transmission-refresh-function'." (with-silent-modifications (funcall transmission-refresh-function transmission-torrent-id))) (defun transmission-refresh (&optional _arg _noconfirm) "Refresh the current buffer, restoring window position, point, and mark. Also run the timer for timer object `transmission-timer'." (transmission-with-saved-state (run-hooks 'before-revert-hook) (transmission-draw) (run-hooks 'after-revert-hook)) (transmission-timer-check)) (defmacro transmission-context (mode) "Switch to a context buffer of major mode MODE." (declare (debug (symbolp))) (cl-assert (string-suffix-p "-mode" (symbol-name mode))) (let ((name (make-symbol "name"))) `(let ((id (or transmission-torrent-id (cdr (assq 'id (tabulated-list-get-id))))) (,name ,(format "*%s*" (string-remove-suffix "-mode" (symbol-name mode))))) (if (not id) (user-error "No torrent selected") (let ((buffer (or (get-buffer ,name) (generate-new-buffer ,name)))) (transmission-turtle-poll) (with-current-buffer buffer (let ((old-id (or transmission-torrent-id (cdr (assq 'id (tabulated-list-get-id)))))) (unless (eq major-mode ',mode) (funcall #',mode)) (if (and old-id (eq old-id id)) (revert-buffer) (setq transmission-torrent-id id) (setq transmission-marked-ids nil) (transmission-draw) (goto-char (point-min))))) (pop-to-buffer-same-window buffer)))))) (defun transmission-print-torrent (id cols) "Insert a torrent entry at point using `tabulated-list-print-entry'. Put the mark tag in the padding area of the current line if the current torrent is marked. ID is a Lisp object identifying the entry to print, and COLS is a vector of column descriptors." (tabulated-list-print-entry id cols) (let* ((key (cl-ecase major-mode (transmission-mode 'id) (transmission-files-mode 'index))) (item-id (cdr (assq key id)))) (when (memq item-id transmission-marked-ids) (save-excursion (forward-line -1) (tabulated-list-put-tag ">"))))) ;; Major mode definitions (defmacro define-transmission-predicate (name test &rest body) "Define transmission-NAME as a function. The function is to be used as a `sort' predicate for `tabulated-list-format'. The definition is (lambda (a b) (TEST ...)) where the body is constructed from TEST, BODY and the `tabulated-list-id' tagged as `<>'." (declare (indent 2) (debug (symbolp function-form body))) (let ((a (make-symbol "a")) (b (make-symbol "b"))) (cl-labels ((cut (form x) (cond ((eq form '<>) (list 'car x)) ((atom form) form) ((or (listp form) (null form)) (mapcar (lambda (subexp) (cut subexp x)) form))))) `(defun ,(intern (concat "transmission-" (symbol-name name))) (,a ,b) (,test ,(cut (macroexp-progn body) a) ,(cut (macroexp-progn body) b)))))) (define-transmission-predicate download>? > (cdr (assq 'rateToClient <>))) (define-transmission-predicate upload>? > (cdr (assq 'rateToPeer <>))) (define-transmission-predicate size>? > (cdr (assq 'length <>))) (define-transmission-predicate size-when-done>? > (cdr (assq 'sizeWhenDone <>))) (define-transmission-predicate percent-done>? > (cdr (assq 'percentDone <>))) (define-transmission-predicate ratio>? > (cdr (assq 'uploadRatio <>))) (define-transmission-predicate progress>? > (cdr (assq 'progress <>))) (define-transmission-predicate eta>=? >= (let-alist <> (if (>= .eta 0) .eta (- 1.0 .percentDone)))) (define-transmission-predicate file-have>? > (let-alist <> (/ (* 1.0 .bytesCompleted) .length))) (defvar transmission-peers-mode-map (let ((map (make-sparse-keymap))) (define-key map "i" 'transmission-info) map) "Keymap used in `transmission-peers-mode' buffers.") (easy-menu-define transmission-peers-mode-menu transmission-peers-mode-map "Menu used in `transmission-peers-mode' buffers." '("Transmission-Peers" ["View Torrent Files" transmission-files] ["View Torrent Info" transmission-info] "--" ["Refresh" revert-buffer] ["Quit" quit-window])) (define-derived-mode transmission-peers-mode tabulated-list-mode "Transmission-Peers" "Major mode for viewing peer information. See the \"--peer-info\" option in transmission-remote(1) or https://github.com/transmission/transmission/wiki/Peer-Status-Text for explanation of the peer flags." :group 'transmission (setq-local line-move-visual nil) (setq tabulated-list-format [("Address" 15 nil) ("Flags" 6 t) ("Has" 4 transmission-progress>? :right-align t) ("Down" 4 transmission-download>? :right-align t) ("Up" 3 transmission-upload>? :right-align t :pad-right 2) ("Client" 20 t) ("Location" 0 t)]) (tabulated-list-init-header) (setq transmission-refresh-function #'transmission-draw-peers) (add-hook 'post-command-hook #'transmission-timer-check nil t) (setq-local revert-buffer-function #'transmission-refresh)) (defun transmission-peers () "Open a `transmission-peers-mode' buffer for torrent at point." (interactive) (transmission-context transmission-peers-mode)) (defvar transmission-info-font-lock-keywords (eval-when-compile `((,(rx bol (group (*? nonl) ":") (* blank) (group (* nonl)) eol) (1 'font-lock-type-face) (2 'font-lock-keyword-face)))) "Default expressions to highlight in `transmission-info-mode' buffers.") (defvar transmission-info-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "RET") 'transmission-files) (define-key map "p" 'previous-line) (define-key map "n" 'next-line) (define-key map "a" 'transmission-trackers-add) (define-key map "c" 'transmission-copy-magnet) (define-key map "d" 'transmission-set-torrent-download) (define-key map "e" 'transmission-peers) (define-key map "l" 'transmission-set-torrent-ratio) (define-key map "r" 'transmission-trackers-remove) (define-key map "u" 'transmission-set-torrent-upload) (define-key map "y" 'transmission-set-bandwidth-priority) map) "Keymap used in `transmission-info-mode' buffers.") (easy-menu-define transmission-info-mode-menu transmission-info-mode-map "Menu used in `transmission-info-mode' buffers." '("Transmission-Info" ["Add Tracker URLs" transmission-trackers-add] ["Remove Trackers" transmission-trackers-remove] ["Replace Tracker" transmission-trackers-replace] ["Copy Magnet Link" transmission-copy-magnet] ["Move Torrent" transmission-move] ["Reannounce Torrent" transmission-reannounce] ["Set Bandwidth Priority" transmission-set-bandwidth-priority] ("Set Torrent Limits" ["Honor Session Speed Limits" transmission-toggle-limits :help "Toggle whether torrent honors session limits." :style toggle :selected (transmission-torrent-honors-speed-limits-p)] ["Set Torrent Download Limit" transmission-set-torrent-download] ["Set Torrent Upload Limit" transmission-set-torrent-upload] ["Set Torrent Seed Ratio Limit" transmission-set-torrent-ratio]) ("Set Torrent Queue Position" ["Move To Top" transmission-queue-move-top] ["Move To Bottom" transmission-queue-move-bottom] ["Move Up" transmission-queue-move-up] ["Move Down" transmission-queue-move-down]) ["Verify Torrent" transmission-verify] "--" ["View Torrent Files" transmission-files] ["View Torrent Peers" transmission-peers] "--" ["Refresh" revert-buffer] ["Quit" quit-window])) (define-derived-mode transmission-info-mode special-mode "Transmission-Info" "Major mode for viewing and manipulating torrent attributes." :group 'transmission (setq buffer-undo-list t) (setq font-lock-defaults '(transmission-info-font-lock-keywords t)) (setq transmission-refresh-function #'transmission-draw-info) (add-hook 'post-command-hook #'transmission-timer-check nil t) (setq-local revert-buffer-function #'transmission-refresh)) (defun transmission-info () "Open a `transmission-info-mode' buffer for torrent at point." (interactive) (transmission-context transmission-info-mode)) (defvar transmission-files-font-lock-keywords '(("^[>]" (".+" (transmission-move-to-file-name) nil (0 'warning)))) "Default expressions to highlight in `transmission-files-mode'.") (defvar transmission-files-mode-map (let ((map (make-sparse-keymap))) (define-key map (kbd "RET") 'transmission-find-file) (define-key map "o" 'transmission-find-file-other-window) (define-key map (kbd "C-o") 'transmission-display-file) (define-key map "^" 'quit-window) (define-key map "!" 'transmission-files-command) (define-key map "&" 'transmission-files-command) (define-key map "X" 'transmission-files-command) (define-key map "W" 'transmission-browse-url-of-file) (define-key map "C" 'transmission-copy-file) (define-key map "e" 'transmission-peers) (define-key map "i" 'transmission-info) (define-key map "m" 'transmission-toggle-mark) (define-key map "t" 'transmission-invert-marks) (define-key map "u" 'transmission-files-unwant) (define-key map "U" 'transmission-unmark-all) (define-key map "v" 'transmission-view-file) (define-key map "w" 'transmission-files-want) (define-key map "y" 'transmission-files-priority) map) "Keymap used in `transmission-files-mode' buffers.") (easy-menu-define transmission-files-mode-menu transmission-files-mode-map "Menu used in `transmission-files-mode' buffers." '("Transmission-Files" ["Run Command On File" transmission-files-command] ["Visit File" transmission-find-file "Switch to a read-only buffer visiting file at point"] ["Visit File In Other Window" transmission-find-file-other-window] ["Display File" transmission-display-file "Display a read-only buffer visiting file at point"] ["Visit File In View Mode" transmission-view-file] ["Open File In WWW Browser" transmission-browse-url-of-file] ["Copy File Name" transmission-copy-filename-as-kill] "--" ["Unwant Files" transmission-files-unwant :help "Tell Transmission not to download files at point or in region"] ["Want Files" transmission-files-want :help "Tell Transmission to download files at point or in region"] ["Set Files' Bandwidth Priority" transmission-files-priority] "--" ["Toggle Mark" transmission-toggle-mark] ["Unmark All" transmission-unmark-all] ["Invert Marks" transmission-invert-marks] "--" ["View Torrent Info" transmission-info] ["View Torrent Peers" transmission-peers] "--" ["Refresh" revert-buffer] ["Quit" quit-window])) (define-derived-mode transmission-files-mode tabulated-list-mode "Transmission-Files" "Major mode for a torrent's file list." :group 'transmission (setq-local line-move-visual nil) (setq tabulated-list-format [("Have" 4 transmission-file-have>? :right-align t) ("Priority" 8 t) ("Want" 4 t :right-align t) ("Size" 9 transmission-size>? :right-align t :transmission-size t) ("Name" 0 t)]) (setq tabulated-list-padding 1) (transmission-tabulated-list-format) (setq-local file-name-at-point-functions #'transmission-files-file-at-point) (setq transmission-refresh-function #'transmission-draw-files) (setq tabulated-list-printer #'transmission-print-torrent) (setq-local revert-buffer-function #'transmission-refresh) (setq-local font-lock-defaults '(transmission-files-font-lock-keywords t)) (add-hook 'post-command-hook #'transmission-timer-check nil t) (add-hook 'before-revert-hook #'transmission-tabulated-list-format nil t)) (defun transmission-files () "Open a `transmission-files-mode' buffer for torrent at point." (interactive) (transmission-context transmission-files-mode)) (defvar transmission-font-lock-keywords '(("^[>]" (transmission-file-name-matcher nil nil (0 'warning)))) "Default expressions to highlight in `transmission-mode'.") (defvar transmission-mode-map (let ((map (make-sparse-keymap))) (define-key map "!" 'transmission-files) (define-key map (kbd "RET") 'transmission-files) (define-key map "a" 'transmission-add) (define-key map "d" 'transmission-set-download) (define-key map "e" 'transmission-peers) (define-key map "i" 'transmission-info) (define-key map "k" 'transmission-trackers-add) (define-key map "l" 'transmission-set-ratio) (define-key map "m" 'transmission-toggle-mark) (define-key map "r" 'transmission-remove) (define-key map "D" 'transmission-delete) (define-key map "s" 'transmission-toggle) (define-key map "t" 'transmission-invert-marks) (define-key map "u" 'transmission-set-upload) (define-key map "v" 'transmission-verify) (define-key map "q" 'transmission-quit) (define-key map "y" 'transmission-set-bandwidth-priority) (define-key map "U" 'transmission-unmark-all) map) "Keymap used in `transmission-mode' buffers.") (easy-menu-define transmission-mode-menu transmission-mode-map "Menu used in `transmission-mode' buffers." '("Transmission" ["Add Torrent" transmission-add] ["Start/Stop Torrent" transmission-toggle :help "Toggle pause on torrents at point or in region"] ["Set Bandwidth Priority" transmission-set-bandwidth-priority] ["Add Tracker URLs" transmission-trackers-add] ("Set Global/Session Limits" ["Set Global Download Limit" transmission-set-download] ["Set Global Upload Limit" transmission-set-upload] ["Set Global Seed Ratio Limit" transmission-set-ratio]) ("Set Torrent Limits" ["Toggle Torrent Speed Limits" transmission-toggle-limits :help "Toggle whether torrent honors session limits."] ["Set Torrent Download Limit" transmission-set-torrent-download] ["Set Torrent Upload Limit" transmission-set-torrent-upload] ["Set Torrent Seed Ratio Limit" transmission-set-torrent-ratio]) ["Move Torrent" transmission-move] ["Remove Torrent" transmission-remove] ["Delete Torrent" transmission-delete :help "Delete torrent contents from disk."] ["Reannounce Torrent" transmission-reannounce] ("Set Torrent Queue Position" ["Move To Top" transmission-queue-move-top] ["Move To Bottom" transmission-queue-move-bottom] ["Move Up" transmission-queue-move-up] ["Move Down" transmission-queue-move-down]) ["Verify Torrent" transmission-verify] "--" ["Toggle Mark" transmission-toggle-mark] ["Unmark All" transmission-unmark-all] ["Invert Marks" transmission-invert-marks :help "Toggle mark on all items"] "--" ["Query Free Space" transmission-free] ["Session Statistics" transmission-stats] ("Turtle Mode" :help "Set and schedule alternative speed limits" ["Turtle Mode Status" transmission-turtle-status] ["Toggle Turtle Mode" transmission-turtle-mode] ["Set Active Days" transmission-turtle-set-days] ["Set Active Time Span" transmission-turtle-set-times] ["Set Turtle Speed Limits" transmission-turtle-set-speeds]) "--" ["View Torrent Files" transmission-files] ["View Torrent Info" transmission-info] ["View Torrent Peers" transmission-peers] "--" ["Refresh" revert-buffer] ["Quit" transmission-quit])) (define-derived-mode transmission-mode tabulated-list-mode "Transmission" "Major mode for the list of torrents in a Transmission session. See https://github.com/transmission/transmission for more information about Transmission." :group 'transmission (setq-local line-move-visual nil) (setq tabulated-list-format [("ETA" 4 transmission-eta>=? :right-align t) ("Size" 9 transmission-size-when-done>? :right-align t :transmission-size t) ("Have" 4 transmission-percent-done>? :right-align t) ("Down" 4 nil :right-align t) ("Up" 3 nil :right-align t) ("Ratio" 5 transmission-ratio>? :right-align t) ("Status" 11 t) ("Name" 0 t)]) (setq tabulated-list-padding 1) (transmission-tabulated-list-format) (setq transmission-refresh-function #'transmission-draw-torrents) (setq tabulated-list-printer #'transmission-print-torrent) (setq-local revert-buffer-function #'transmission-refresh) (setq-local font-lock-defaults '(transmission-font-lock-keywords t)) (add-hook 'post-command-hook #'transmission-timer-check nil t) (add-hook 'before-revert-hook #'transmission-tabulated-list-format nil t)) ;;;###autoload (defun transmission () "Open a `transmission-mode' buffer." (interactive) (let* ((name "*transmission*") (buffer (or (get-buffer name) (generate-new-buffer name)))) (transmission-turtle-poll) (unless (eq buffer (current-buffer)) (with-current-buffer buffer (unless (eq major-mode 'transmission-mode) (condition-case e (progn (transmission-mode) (transmission-draw) (goto-char (point-min))) (error (kill-buffer buffer) (signal (car e) (cdr e)))))) (switch-to-buffer-other-window buffer)))) (provide 'transmission) ;;; transmission.el ends here transmission-0.12.2/README.org0000644000175000017500000000224413764054547015646 0ustar dogslegdogsleg#+TITLE: transmission.el #+STARTUP: showall An interface to a Transmission session for GNU Emacs. See the commentary in =transmission.el=. #+CAPTION: Screenshot of transmission.el in action. The typeface is Inconsolata. #+NAME: fig:example [[./example.png]] * Installation Available as the =transmission= package on MELPA . One can install as a package with #+BEGIN_SRC M-x package-install-file RET /path/to/transmission.el RET #+END_SRC Otherwise, for installing into one's =load-path=, a system install, etc., do the usual #+BEGIN_SRC make make install #+END_SRC * Setup The customizables =transmission-host=, =transmission-service=, and =transmission-rpc-path= are used for the URL to communicate with a Transmission RPC server. The defaults reflect the URL . Also, there is =transmission-rpc-auth= for setting a username and password. Setting a password is optional; Emacs' auth-source facility can search =auth-sources= for an associated password. To see the rest of the customizable variables, visit the =transmission= customization group with #+BEGIN_SRC M-x customize-group RET transmission RET #+END_SRC transmission-0.12.2/Makefile0000644000175000017500000000134513764054547015641 0ustar dogslegdogslegVERSION = $(shell git describe --tags) SRC = transmission.el DISTFILES = Makefile $(SRC) LICENSE NEWS README.org PREFIX = /usr/local datarootdir := $(PREFIX)/share emacsdir := $(datarootdir)/emacs/site-lisp EMACS = emacs # EMACSFLAGS = -f package-initialize all: $(SRC:.el=.elc) clean: $(RM) $(SRC:.el=.elc) dist: clean mkdir transmission-$(VERSION) cp -r $(DISTFILES) transmission-$(VERSION) tar czf transmission-$(VERSION).tar.gz transmission-$(VERSION) rm -rf transmission-$(VERSION) install: $(SRC:=.gz) install -d $(DESTDIR)$(emacsdir) install -m644 $(SRC:=.gz) $(SRC:.el=.elc) -t $(DESTDIR)$(emacsdir) .el.elc: $(EMACS) -batch $(EMACSFLAGS) -f batch-byte-compile $< %.gz: % gzip -k $< .PHONY: all clean dist install transmission-0.12.2/example.png0000644000175000017500000022314013764054547016341 0ustar dogslegdogslegPNG  IHDR(9k,sBITO&IDATx읇TUOyuuu׎ֵݵb;(R-Q@ PBIL&ef~'w89333aE|~ ɽ;s~ӦP( B7π߈вS( BHE&fL25%^ BP(J BP(x) BP(LVU:uΧmP7q[=,uU[+^kǺ)ׯ}?j=* B8W%\| {qo~,׀ps})cFhWP$C]Q_]KqgխxQ+RP( ʼnL'XCV 15h9tگsZF~wRgq+Fǵ. BP(J13Sߑ7:Mtl֥BP( %^I _3>֡*)|} fo6bj̻"Z BP(x%1d6ƚH|9wЄe-#ǞQgmڵ˞W7/BP(J⧽?AމEUo 5JqSM|9uK 7,.za֥BP( %^I,*[խ/o<]{+uP( BWc69*lLQSmM]Zx) BPuLz)uy08>vKBP(JXQ;ڽk}o;XMt4ұ4r&n BP(N`҃\\ϖe˸8? U޳zf{eJܞ]wǦ[VjdԝUݥP( &^(|g5rxKO>jh^+/z eXk[gM]Y?̺PUkQP( N2}QxP( BKBP( /BP( %^X BP(xi) BP(R( BPP( BKBP( qM@uK[3vh<Śƃo4N~9u h(ױ<~yΛk!$-~45 baE퓚5ȷ׏; /reGŞ>IJ9%l:rsBP(r%^JWD;cX2M+^U?վ%kᤣo ms&%x-{i_rz?Bo`H-77R6ye\ȓj! 9 #7G9jMȨkx:쑆8_1jw`rS)S{θ!iZviXPuT lu)l:rĢ1=xBP(r'^\_&^ۨemHhsg׬pe̅xa!^"rKqNUSս(76-_;hjJ4@'\U,h1%9o5ν%l>[Mi瞂.ÙZTS\-,.a֑;VuBPꋫܚ%^2`fd呉D%^+(]:4fqy/0мԪiqC)&]Xo6RN/t]\ ^Y.vֽ B5 7{Qī^8BwkU*>?;h#e-{j6>;iOk"5CЎ;|.)i߂f>F3ί0Jר s6B= BhP=)oxk4lvp >I|I\i "hsK{L_Asa,PVjU{Icܪ^^HB=FUn] t 4s-ObU2 Ӗ'U0똇Y7q4q76 iVTIc̈́зP4ݤMmоePS~MúבUw B ^Xs CZ/& W;3zu8|߯{ޛg~rMqz-UY_ip;*4Z$8H5^@0$4bi͵FT\xa @h=ەgLaeS#* ] kW0lMh %) fBcҴB,}%rpNKTE Usm,HAqVax yd'.,na_Ck蓼Byj%Ka(EOZjZmz`U(_ {Z7/w:zlcxi<&g/_81F<@R'$3ӂS6Duf3!^p ;,vnfJ ]<Ҁ@-ל*{%c+0T0%Ip$y Tr0F]pE-xs[%ƒ;ΰq`aea"o()J;,};SYֱr#^mFck >BP[) 꿬?n38p̞.ċ|~U< L}?B^&4q3  ]7=j_.Kc\/V Jo%*x %)\zd4axA/ڮrG6__P(xu"^,Gny/xGCR2yYu;@Y%lNnmQ<>0ɖ<V#ίF*4̺oq9ِ0  N'&Aqھ H@JsqV@2!WQ$?)͓/r خ'4B*d՞]sC x!B% ̟I!ƒ98KX\3s`Ͷ#[O@ipGu.9olXFΜI;:$xB&@ ϐn\r~ލ&;DwJ.|ͧkPxҞeD[%_ٶc}9zBPu/NٛycVLtlRq\qBN/NXr80.##F3!h]̛:Jsy_ۚ00GGumB-NOήvpS4ksUeo>e%1B{WV]?2=GP(xeG*/<{w{q΋ wsRѤQ x[œ}Ћ9 {qtה_>}qmX`kК>cYf#&!]%9cws}"M'o`ئv1 %]#+Z0#e:rhwv;FR"oYY-)iڲX2zOf}7#{(lA:2obmUBvXV9ǛOYɩrLsLl):JCnr/ |*78GbU(J:WRz\xHkԧϒ]KVxy˧ 8mӆ>ݣoTiag|rxAxۋޞ[:Ov/-cnɖuM+/}NmD}|2yuA{"T][1hNS=~.-IOxpǙF9(oD#vLk6yBH8@b?:HGBtVg~ք#.)^ޤH\ώ3Iz"au!`C;M3^TEѧbb#&?Tnu!>=c.9ە\VěOYɩrLsLS*6WPϐە\GriYeS99 BW'] PP[/9p[ /4/@jxtj\OzK9Ue[3E0P ʼeDr;î W%F#>Q?泞jd!G-` ZH}ey߷?Q'ײۤ'zdlLp^1, a|C'z܄͖]"'C<ºMV,VUO Q;ri #Li|<͊J*ͧT9g%+K#= L~=TP(Bu͗\9%:> za :kaw{G,ؾx,_on.6 8?sbYV>:f&(cP,l$lx8@pa6+gXb'dkHHج[;;(WVa_ldF#>[GK]ĝ?N!81J u`!L\XҞa6"MOȯXGŗ븎zy9ە\VěOYɩ |3 hWbɥ!gO9{BP\7{&SOأ֏{/vHؙ )+ 31SgD̰AS˵!H% ß=5Uv;t8wr=~؃qӐ&cuB|@n/llTKuKQÑ_lQ u)۞tݓ_XOW4"̨-l`9o6|˛'9pށ^|+Ω]ڹ׿I,_L/Wk.OK#?kNb3oyڕ\VěOY֠4=/Xy7??3+]匬뿾=t׾3v Kw/Z_؄zamyDSlfm}6wŮcGڼ׸y<u{-=ˮowTj8nώ=YC64FM/MmF"xeZ//c}J.? 9~A; {mNj#}F쾤ې-y'\z1h>1GUOZp~yAh{eg|ᦞEyGxGϬdT5WաfNί@W~_6] eOy\SLJxa+`-viW |#rhe"^BؼruNqB8+!^KP{61ī@w/{QJcG!c^[st:E&kȟ+ 45Ǿ]-y'l`9 W&͆9:p`\Vi'`tԞe%ʥ!WƁdW<*x)g2'mW9#|/GBE#k7WG=_f.9D6w>BŹ/!RiGvדo^uY/4=2 hs ӗ!==ܒẻGqErѱ#^TǃM֡RԴD[s356+nٞm4rN<6yw3GGu4㇃}jȬʥXCS̿]e[VSVr,'+a)|؞GCY @(# 9es:BIRKk){9wUW6Y%lUTY/|Ͼk]*V:O|zC9B/8Gz/_o]4ˆ]6x&R|~zCSqxy|ӾNp/h-ҽq4!Ws8r}( z꤄[yw >>y̬?AFCwšhQ7,`vغV1A~Yye>nƼ[4YŸCXw]ĸ|镏nP`ؚ(g|Y: y};@ze%1i36U` !g>#" ԵT<ܿ,ށRKvTjNm94cxQid0A.iL>Be_7^ԛ뱩cW5rKYk g4dvCsüm~4[=ٺsܝ,~gnǧ>~Gl;lХ.muV>9I[uC ;o,xOזjI-cnɖx6T ._O77iajq;p&tMC{?$ud:Kˠl+oT<ߤuv_zC0Ơ-;ôi7e)|o+u6t2 $|̼ڽH +G-~ 0%HAFϪxa岒KDB0>6sǒd4Gl/z,Ң RE*o#PZ6 ;FwOW|//2fD }}Ž;Y *xl -$1*״hs;-oe=ۤҘr)fT<7,ЍLhȤ}-Nin!\h)&7o, qe8cPx3=fr,_:'s1UNY`^5bSh?LDjv! gd jDeq"9|Νxa6R|֛΢ըr^y-Ԝ^V$7W㦲b7";̔O/3im;v0S~XB~ nB*r7Sfط4TnWu$ܞ^"2WΑC!Ud=ތ (/;iGר5>yĂΨH$}{P.+TN5*1La%^0[=xG8A4RNnjI24%6†h|Q`C~0^ {^1;RX5_^Ӆ^xyi5#rgĠJ:ps^8b7&^iۏfd~49q6HZ!#ԧkӬLrY`a5ҁ9DJ33e rbCI;eBץ{g`&hAO c,xrt)gtwKxߨp1bt 7FͶ^ۈ1;ޞnA}@<0"Z@Lm/vyQx*_:bI?".ikr$+Dz-ޜJNc%^ʱҖО]zY\H=%ʢOs/E2u7qڐ lpH4Kh.٘Уz [.:>c_^[sJL|ih|L2-;xe*+e߶U'ekK29󎣃9㒣LJ.ͭT9خ:KChr/s i# C؂ "+E[/WvrX͊2Xw魸 ,le Bti Oq+"mؼ=e`V-D9mO|bXP8tx >``j7!X#ٝL`al1h`v7Zp/{w.$^>iKˉ3f/ǍiL7a]uA;O@3i\MVn.lK&h=Nm^X*<8A]i\4]Upʥ!7PO9e#_˪ ͪT9gSܮ:K#= E.WȑCN)!2ۊ бѐy!E*yGmiGemfӵ+m~T Dt[7Q>+m ,l\[l'&%nB>l Nxr˙]gS{+TP C-C'zK,7XR$51e %I(Blq_Oٞ+DiS44>n{_Αܮ3|JNU`s&^r(4۳\ =Zۅfxd cϴ!w|L˱G \Vr`Xh[<%{%kUAㅋՕ#d"]‡^" nYb=#/ݻՎ[2 U{,$8{XmښۤI8d _J.xpf A٧t0o iBr'W<+nNxY;%hR,̧wK2gbWmVٳ9 k9vfə^`=A W9'GT+xW#w\,`ЅxeJXpV 'X`Ť, *g%-F!^9&r=tdF#\LH?Sڷ&ZBXʇ5 Sn4Ggn #tazf'\`AkM \r~:×3s$+7S4g%+Ґ۳\B9z(я;U/ A{l/CXuN,pIm} ߬ePa岒SMRH ?i5A@xǩYoֳ a\ş>vτ{ vS!^'k!VAݠP. *F},h3-ba?#e妪Nox[]BIX@3l:|ń!1Tcs$^#voKe7hy| "<Ơ/(QȖ!n C ChvrpP&|!.a4)N0CG&z>!G>zrVKJ:b~? iE0KFG3Q3d\$,aGo`XMLbԆwJD}P6>%C-Spʊ!:re8&,' -־Rh;4NarەPVěgY |3 lWBܞ?P.r$P dE9 jk? 1SlFN!U>XAl鼟PVrx $K"X:oN2DI L+9<=`QXHXkƆ*O' EoCc *GG#) BE s1xJg ^lToebWt\>3&tՑA BP:+ԣܺ"bږipdMr}zx8a 0 BP(:FC/F˅حcnuD-FK/K/ܯx) BP KP( BBP( /%^ BP(J BP(x) BP(RP( BKP( B/B읇T?yrA, (PrzyNjzvT:KB HhJB dzaǝLfϙ}')g/@ !^@ Bx @ @ %K @@ /@ !^B@ x @ @ %@ K @/@ !^@ B@ BZ -"DñC A['^M/EWxUKx=96IHPϬ=%yglzWǴ.:S>?3q!A#^O=]cva`SMie>8ݗ=zbmȬk˟q- Tq<1uvgxeu+-kЀ#:]@ Ie !z~=le^~`9hgM{]'":cx8o}@ .v΂8mln\}g隇<D p! xVG_8&$CB A>RSfR}w{M}Dp搾e#k}&uYg#, z!7A p[.WɰKSG]lTem%7uG-޻_=3oJ7a!(4m\"vRYv~^/>֯|S/˿Y-F=ᾔ,@ hp(u!Rg\onsU;O턯@Vj7)i,"(Zk4H {~P$2e(>O[r˗w*G3?^={l녯O9Ouc3ScO}xQފG?J?l'W)\O?kh&j4Z<!@ h+NzWf7:/ xA\Pxjԏy]e۪K$pB;)CqHY˓AFla-3r#N3k袡JG !O=e'Wiht+|KuW=2@ h ئc8vܡ)&%XHÊiŜ"^& S攡8h"q4q*ѷ}/wUMP%2dgEOhVYN.XK)s% jv(_D]5{ˆ]ț@ h+h00q=x4aE0RVTGť fg֓p3`A.سzuաU}czywQN7/Zɮ*5}WxAb5lx!iCnM/|i=Q"^ޠVB=V!We;AM[B8PlȻ7mέ20@ hO ry6Oi/Au_ȧ)!^/}L*;5{1b|29`/nwzIk\K1ukMl7ƼK~Zߥ{!B&C DNN#ʃ+(_xd#/d]c_O|7Nvż]eo-}w٩) 'Չ6dq%K C[:!Πf"^!Nb,hGk5eDM9Ym5w=llUTW< Q \8lv+ϥhDlkīU6x @:+;AQA>? ˁUZ_RA{SV@_2 Fc뎮[w{boT\zGCZoI-l#Ae|JkHYQ+mn#^Hշ3w̴Y-r#dQGoUpk֪]LCq",D=dR6)jM6`y]ejkm MeZ=rzpF7Ϳ96硂.m;suկz\'"R_;ֆuq̿˹?=1ĥ>)$K mxYNw*9\|PV}VrdC冷yxh d'K-}˿|녯O+N9OaA~Y>4Z<`Alk% CʠQ2+{X4婼>ZyPjI F_-XD"<;~x+=89s Ic7%:9G<%՗۵jZŝWxՈ #?~#.(O aޠW=#\D2aHY %֔6ij:BT H#)K4tPvH!}L^lR6F*տA/w\pK CZDwuA׹fg?[ q.{m%^S.;_h櫉HU'v9KI`WS\~%B6^ 6.`Q"h`M`-ZF0R^w5gESނ #ES|#g=gdœ¹Ifћ6 ^cAE鑮dH,4 9"B,ɥ*SNY]?ߥ*d(T;c@f^Ts]͂'!ĺM  ,1YE #5`u⺬6ꭎrObRB@&@I·CV:R[)/8Qox #WZÇB/C }+θܲI$d38MJ(AHC82N9em]w8K_1IT6^39ɏ0LrhU6ʟx7/MW8}$K mxM*߯zTgg_CICY"ӕ?Sۧ9r\$zjLSV@>wRߜ8iva.=BpKhf' 唵@ѦaVo-Fni¾/3 ^۾aVL4rڕ%6J8!=lh*0R짳IX+r7Vtޅ Ao~6a`oLSV`$Z3m $^iM6ollTϞx% 5$Y/mq6L4/|NmE0_E6^h=M]';"x  /tXNn+*A8bUXw[(l/ٱGi :Y#տx]GbU @VŤ|o-}݋9!^HC$qĹ_+l#C{xӘCH+<4]?4͵,!"7P>Kl*w/Ԃy)7IyFUۉ#KXwwrݰخFMuQKqqߜD -Gԁ عkC+ ^\ŝ=#*6 ?C³8?~-<<:X_dHY uO1K_*?]EżwYZG`X0w Veʆf7P&.=\c, xF˶D\B|ѣ'^p/݁ 16?jU1% wz@f@ K xurA{ٿ@ %īY"^B.y':Jib@ x jG w&Q25_W !^BX!'S@ %K @@ /@ \HJ-h-hy@ \Bċ;VǛ8e[n37l0Bbx5?8xeޜ9%p(=۝39?\l OW7⧞MZD@ ex቞05m*qx``kxE%p@rO(ya]{ Ϧ,o j!֗=>{$KߔZԣ}D)`M ϺND|@ hQF ihd7DGf{ Ϧ,o?;0uAwc#SABM:\%V):G#2q@I&5DP-_l3j&4GyKM ^r7j˼!6@.Дeʞcn&"5H/p:..3hs1Rʻq ~ShDKЎcM/jҬDf'?3&suvhEZ]ǜdRK%)/4`NuRHzDt2ku녯H6VnDjJVi/h\g즱֓e;_jGh,l#uiF5TŢx7 jRW!dlĚP|@SY15ǞIZB{F-:υĈ#,·M[^ʬ!!^?~CWVU 5R1OXZkV0;u,سzrաUqQfN[ݪ xC'g?V[x,qvD שV9`& p%bn>9Ϲ\5nc [ chFvJOm(@jw\_|Ř鮟©=] ,z۬8⵸b1lIX~Y[K1JgêFdMcnz`(tӰ(TP9v!)[+7rMi,W0 |=]_[Uz-}܍fWߪ7\%6v?%/>B\RX7J@BHf~{ !^zG\7'S2HAIK^Pۿ饶eA/Ez: @8 3,DAq ŅwT~\_zE,ȋD5ÊQ ?ȾuהmSR昱cF!^< AOVaȅ L!IxR7@5pI8_~ge<;AS6^qc0 ^;LyH߰ \GҲw4B+{8#Ľbf3V,فsG&^MO; ŏhv]t8/s]NbC{>Z[`qM,/:H -s.پqxapmN&{'5”yxv!"iy2+6ܮc|oΉ JGhֳJ{c"^!hx]%!4A}}k=3vlu"v'1gQByqv_ C9T`~uw[wZ&-1!?&y=:+b*"(YZ;qc^+G#4Y%qLd/XHK=l$ƈ:GӍFChsdI9ٱR0`p>vF[!vZB Koohc_+*pl4:ױ7yO}a_RR^x W~o'^U2G1O!^2'^`YjFS5{no};+ѣyw\EC0 ،b"txռbnꅁ%h4YBKo týxjӇeXVH fϭ6_6Ł ֮% ŘK,NnU/{2CDII}ѥ1DͮEe)w5Wq~&KCЅkwC#^S% ֝۳U2G٬t)GJg^/w]|onsU;O|uFtvW۹G8|k즱O9u۸7+]T'(‡\L^[c2zΑ~oh=Xtb@( =4/Rh߹cEA{fNCG " ``ϔiU*d{3[gw-ܸǘ+jv~" 4u*Vɐs"fU3/Z/bz*5JlTY\ͭgjC@E+~K'Y޺K -x|eY{e҇M2!ojM5#`& yޟR2s7ӱS7+(~pPƸ$F%w,*ec=)Y6,POrrN7'Y &)U:bsf h}8?CLqxv1a;=JxcCZs{J(1 S؞ D9 3GO `B⾶\\ͭq3JU^&&'C 2ѳ뛙d㊟Z0ey f,l5֤!Ϩ?F%YK [B)IZNFPxuGէ?&aGT%f>Oގ {}gi2d*ۿ鱚x5ͼ@[s2d0Y#lV c8>iʥ?,> C-h"^C mf \h*K¹܆÷cG_^-3Y)2]w-8CM5N @UiQQ2g=]2$ؿG>( r^л2RV{BÍ$6e&^sWYȌuxkV~7#=櫅Y-& -O56Ǜ^jLI5i395!2m8@޳є|i 3REH(R+=ҋ4X׮04dgd8WN%{}3>)-nN.Lmanɬ8x3Q5h.q0|UћF'աX=4!nX7,֎VpKu/id 6u۴(mL@9%BHD맭W Wg|.$z[Ug x1e58Y* zl@E5*VהUd˫k&Cb\IÈ>L]^# )WLZ2OH0bQl_f}d"^j$e]i/%Qdfa6vzs%kkyRX2@qttEC+9U2Gtb~6V`5fӘ̮*5}Z>Z{Ys/c8B!AWXϦk&aƌ{Y*ʌxd+hys\.#d3$^|3jUהwLnP>uY& @n5ق)˛B}X0P)SzAQkC7"j#{BuN~^=PS5{}xdn$m='J+0Rҝ( H%[ϐq?L}V5CC r6V) r 6QItU:C72֜+6Qj5e'e/z[h#9^ 4uL; ֚d;5⚢1 螯jL |8ö#Kւ^bd1RDCM'>CtAOF/6KՁF\/(P{V8Pt̢`sph+sgJ9R|,٭cU*V:H1<3q8:XYO]]*r#)<:ѴLu{VL۠﷎4;gީo&X1ZXEvU:sߞr3OY-fLnrfFw5#R8R><0"6%< Ab3-l,r{l!*#l gy_."^9}M)L̝*KBxr¡~u 0n?gl) )˫ޅ1l~XydYg#]O3Z\ޔH92k_sMbQmF^|ï6$^XSWlCn (7mO N;/Dqr|[_I &ؠX_H}QkAҝ;M.$kz$ VY9Wj݃ cdVc)sL4cn[4O"eciO0RLtk=C0b\J e.Y~8 $^d557F[ F?Kit-h(o Xȸ}5i3y)-`n772#^ȒxA["xgO6.Lؖج5W)gol&^GЏ3Hzrh=6qJr${6s%dɘv&Hc& %IGmtъ@b9 P\EBiL;Vϩ8m\+0}@;']oB8j!^IhLC Nxe ,[BVi_A ,0b/. /KpIxB5F ;41/t@܅/B@ x @ %@ Kр1H+H=_ ӂ{do}ՏW/,/Zጼdlst _x(&'{˂eҵbqnx۳::Ƀ}ag {l1#<L4&s{~[o xE*|EolGsĹ.d=pjE E="J"z:}Ef. ç|x/"~6瀔 ?621+ tE}LD-L#1Jl/Fj6խ_∗ZrOYGM;b@o?%,c,[A 7ܪ8P.iJq9Y=A^,+Ed3[sə=Jzn '"I(L!^A'^֐AȺTs|P)%cU6bW̄qb?IJ'_ZpPpR@c˃)pȹ#ߐ*r䝄m2`\;jEf? DPa3j[ىjjk7eѣX@ ˲(*LFo82ІEo҇Ț{=fabr/PEV'^Vhd2ݷV'^fԩ*x0.ȥxu0" W[gR(K h7kïMJj}n ¼ldO2a푵Kuk}3Gx;7 Y8o{Z{lc)/XtӘmD7zs?;Xsp}?HV\ex%{Pe]gv{ޔB [0OV~tҗ ^ݯkq ?hoW">h \OO zNd!<Cr4sѸ56_8`-'H$"+  q8"[SL;Gj]jG":$FeX o{[xnbWbZ6[J]zYD*[ G"T89cAj G== G+tTzNT=ZF1VlS+E5}έN+bVG䊦!W+_f9!.X +]ꀈ>ȾYM!: 1;/(;W=] O ]45 ?TZ)#J$O/o?Z 3/ޫ\}#6PnUkt//3SšVBG.yC HMk?8نuzb̪PcH<ӺQKZ?X䘅է0DⅢD-{ӵ;a"Me5v[k}$($^Mܴzm@&=7#Uc۪gdQȇ͕hH=YAЂ`ċ._Jolʅoe*\ZNPn:Q\ N~O9OcX0D>v*zJ0,t''NFH͔$O`ߜLW] YQT&pYA@$ J$CP`fީh{fꮮ:]UsNUN{˙wG*?R*6ʅuc4E_CPh~T*Ǚaf{ k\ DRH c|xx)t5/>/"8Q/u.l]߿WZArELe#k!s?+rVXETǶ o7cy j\(ʖJ^ܾKHŮ{.S@Qe^9=jͣ9Uӣ^KzEZ}<8aŹr1l9$O>Wk TF 6;B\+@m$gڃBP馴#Jq՛%ޟ5=C(mT<5K*R䃜uO+Mƹy:5jh`ᣱ]BR+8c""^4Hbsb+d0\8\\n6a&g|n,ToQ*jĻ(`T &`FѡƳ qfN!5.62FF D x%^ KzG1V.jefsHZLhA;)^YW⥱"{BM%BFY(02glHV@ hi/tިy[ jh|71'^ >Wp3s $dvQ uB._VV|-ET* S7ag8?s&^X&/m8mYJa`Af-"•\ctώ ]s33!^1 ^߄S&^}/&z,'QwFsHt9Q „;w58zy5~xy? UEڒRԛ>Ėx56&H.B]tԳ|tolC Ǜx+!^4?B4#<5ػ/g6 -?J1KU ~P/ERnzB5V"i 7eE)K;W0}!@!^1 ^_|Y뎑w7On4͙S/e`"p8bbo}AלqV阦PPJ+d=#^g֯Ϛ Yv/#`9au1Ŕ";*XF`j0yeUj,)b:54֓z)NJrY0@.iI4@W `Qv79qQ- ~WpRUtP}6hͰGl ;Z$ċtj͞5^5/ur4|&mQ##fL3*WUǒ[q1βzxu?R1%XMf-n?RX戔~e0 ;{⊨>sHJ1@*) o W$YP6 GKR}ra~$*,B( 0j['Scjj&O<&u}J=BQU065,xo'{X?b9*: PfW9E^>1_Io!_!P'JyLj] l2YY *yt"_A x%cbWzbulW.bX6҆2 9$n|@ LSؤX2HY gViCW!^@ B@ B@ x @ %@ K @@ @@ /@ !^B@ x @ @ @ %@ K@ /@ !^@ !^@ B@ x @ %@ K @ K @@ /!^I3_qȍ"@ xU'Y.'﬷κf5Ɗx9Δ-gʆŇ7[RyXYeyew8'?QZ^Õ ~pEW_O+F+& K=fl~pQK^OyxǴCR/{ӷK>m\6J[?HDuU;7~E >WQ4 '#I@ HQY3Qy$b޵5E hĒ]KO^Ňw;ݚmgz_{A}C+:TT=?\ʻ(:3yWZ1HG<Ȃ &o|kz&#X.iA5)$fykJE/ۂBnVݫI(R*˯\r14|=RD]jecx6x,B3Ϸ{Io^M 齛W tA Ʈ 곤J=ᓔP[Z;RrlAn3/8D@[Q~`MG-`?`</6Wũd#^%_U|'ܰϫע狡 \[SB=f:&"];o;[ǖ6T*1aj}\A bXLA-Up̅|^IE51[[+"9$"I@ H3|rȁ"0)o|z?:}8Θ|î72/y'w9yU*}%$wƉVמ뢯=7N5Xg/  Qoa ^+(Aߗa&Ή`Nxd5Sh#^Zs>b"זּx\Ggi_JBiO+*Qwκ^H5ox_)1d,"&"GrgH pm>:Wt9k_o(`(+3(I@ H'5{lh DfuwњѨz2A;^6[84!:J/[1"}olkH 7OhZ40ī@&P}117߂jމ4`&^)ǘ =)W< s.*cO `²2QuXʦ*$_5|\7*<{(zc "v]ZV?gyb]}%c!OI A*Ysf'7lO==4!%KOx)]R/Lz-Lr}ojAiGd7NoXոxQ P/ݹqH54ʊBs1/OlQ`du^eiM`]6u[Et)E:+"\knJEG{AXYWxܻc ++D~4Π+> &Κʶ^\s5rڥ`s*r#=x@ $xoYg2vuՇ6nf;r"4}!ɖYĂYQxxEV''gs >Wщh ^.u_I ؊.0"lQV^/~94 275Zj⺰3ۃ|1j㫭lfqig҉Wj':`?(Nhu%"YⰚu}J=~^xx=ĖM}TL~꤅ ^wuW^yٳ3O$ Fロi[H&& G0DA:T|^x7W>/|ĈISO=x&Mz7֮]F9齪֭[׆ {zEj7Zs'ͤbԮ]{ /_Nxw]ھ}W_ݦMcݛ_F1!^˖-mojqgzg,4" խ[7"}R.]lP+f:qJ2۳+~a&ip/ضm'U5hѢ ni@ ԰aC5:h[oU}sj@ +Vp2奎;wNDŽaܼy4ev|AF iê϶W0 y饗RJOy֭>kx?r *}hHlnccƌm@{ pL Lq@ P=3"s6-&C71\|0&w"IS4\l֬Yɓ6mJ` ,yw܀tJoꫯb}Eaf jny0h۶wX abw9{I5̇sUW]EŭtzY,+m{E\>3AȓYċL8SHo]dLl1>pK^fWI#NNlEEgϷlR] CRuM@-߯_?4"*NtUS;*+?1 dhOxz;YuNHNb5Ր3G)wޮ*@mY dzDss8m m$xĠB+ $&Kec/4Q7<% s  FIpj H |qk@%P HN<@""IR&ܬ:C ]YѹmIA*Dq,5bp'‡ʲ Nzd5՜QK#^}˸]k7(SedX&IS:r VV1^\,KT?U0:Wȩ?g&CC.hS=aT_vx8qAFnMĒОuGeKj]k}]s=jdc ^[x= w @ s 95N @i_ދJhV"z^wY)GTLa4Sb/CήT\JVWcJ/2/<,^h/{7C6e gOZ˂hQfe Ek]0$komGKB‹$ +\d[H G*6Cw⥡1X.gΜ]` /FD/b< U660nf"*3f:52jk95~3%_-[l-Fi\{*2sE2/5SXjD/"y)zHaTR3ːk!gZijdOZkp}J/2{i &^* /EX5v^bM%)JhC~Yq7Zp$F|yxEŕI hȦ\%)K$F8GmvLPJ[$^]S9` /F/슌Atܸq82m%CFFc*@]rNKka_A%(iNM.b7 ʩxȰHxVZ]Qk+R&8G=zkTsΪ%)wstA/Cx9X 6yF4ԑ|5|PjVs~%l1C hS-`.ܸa~ֶ?ҽXw>{!^ 2S /,R0`R˙mUӻd\%ŋGX J,^ș=GT_ L&~=UxMaurvM5l~*(*G~q#Z{B$R(f3rɒ%L}pF("ڊ D952Pz:S(Bx_/I&Cߕx1B9WUrvM5>qH k#6"T8x˜+j6Pƪu4,"M8%25(/LQXteBڈFJL<6 Y39'\a@c,\ IxyJ`Ѽ=/njz4.xq<\ή} yhV~q#Z{W W7ΛSx@S1hׅ!( ri3`XQɩy`hԈZ Ft%^@WQS;Gʢ׈W0D'LDv!^@W Pw3nbPB ;R4?,Ե]IR%v9Dv!E !HjI P@ xC@ /@ !^@ $/u i8~mS.جau v"[ ju {5 lY=իWV=V祵O&2緈xECXSz[Or)m>>2dXO+d@H6&I$HYe=[eFУ IV:/ݵT~jt/ЧO!^ ī[n:3g8MSiAV;?>E98yTQʶ:,^yf!^$/NRi !.QZN[PmO lٲX/ܻdAM6Qt᯶#1JqRوa^XPVżə'6U:m +ȹ$kk/ηOB97}t#ݻ{t9W&(^[lhu9lFWv@W8 >=9s^СCzkOO9aFIRaRI/x=nQ_@ċߩxaʄ{Ġ\S$J][}FoBO>yΩd͉ήGi:F/%-[{sH\ cǎHbDl2"E@H27g|͗;|r[A>[T~j0/\LAΟ?ɠAҚxь x$ߝ?8f)S|_@6eNt邁?ypmcl a($栰 xAֺSNi +> )Nգ W7 =\ ̘~Wꪫ0ĤT65k,Ukμp /\8펿a B^xqrQuB@Oږ$Ⴜ˔x H$0"d39&Pk6/8-X;IwEb(xXgE̲|_@+jW#4V*LRJ: 11ˌxz^Do~dHqln,LV/ekaEM47RBM|FƏ,^tC= ( C:u`fxMj~@ 82{Xi:K@lrJ ß5*DZ8V/^q @U1ʣ׭[4dKe#^8S R9U1>|5uĉ\T^0v[`]7r}*RBS1^gqׄ۶K0aR+T5UiJC+ T$7xkW*?5)_V=XsݚZvCOG ܕ 52HR/5+HE|_@p/VT13[XPG g /q3Z1/L8sN[o] Ibb!1j^lj1@b%>Ke#^<&z6J28PҿzEd!\z&f"振gx!pZ(T9 ED\쿵+mVegX)v ^ou&^I+/ňߠA6JDQ<<ߓ( T$6[o~$8dLu֭FK.62gH̟?OtOV9)0(ޛƴw_Jn l īOϑP.5//o]8u۠ ^yԃU?/v8i~o9;@ەRxo~ժUܧ~;N&M2[X .GC={OZf!P%BeΝ;U*N:Vի/|P/:9M͛s :x B I!k_уr$e˖QSjGIN;4_ÇOW0(Kq6MnС'V %Af3#^WW!\7ȤСCP#׾`hqwXw4o7z'0@z!CS*Ք9sVF^@VxUA&־౵'ͣcE΀^z/;mG [3g&߼>s xbNzk3ι)c$.Cd tNoTೂq\s``篃tA`TD\Eږ)5oG CzNu#d"qK=k7pq hA߼ytC 3 \C쑶Lt~(&HZҮ7E ׿oyTt|t%̣ؒ7oz?9+wu&S?ةԠA=8iE +8#F9P;-[KCvq5lؐWyy "`#btIڵkpӱcG%$ NLEZ&`DԷ~?^ xzg6mT/*ĉ! BjP# C)TM/-]4ګk36YK1R5eMGOv v`뱁}qÊ:Da!8ktFL15%*sJ8T:yMsܵU-?s߇ |1JSOQbh֬#+BЯN:TZZ?NqFc"cTp uWlْf* 9S-#0z jųSxzr4"^~jdڝl/hn>ա=yM7݄&ZbnV0WfK5_DAQ@w]ByeQ?g}6d6\L+%h:0kܹm{A2 Q8p* &^4}(!?"^t 0^DP<}9UIjƍSz,sƼ׊gރ.;lu*-GIp+p !@PB"klT6X,Ȁ$D,]QvxKf?w񛙞߽3ܫ>Rяӧ}s#^g?P{^{rЁ(h=TM?za x6 \yuͻy[:LmJl"O;jܲCcBEſ\5{" FxOT,\(:?lڴ)2֫Xi7^ %V3ux2kjjXo LK^NZIK/-7tLpl+uD~A45&fcH~| p%Md LQEEhC:(&ll>bWƿ۠ rJYCV~":㏯OMwx2Lp |]1d.V) H ~05Vxn$LJR4#uT_vo18 E?|K\da,RZ7P_\yzQןjXq FH;|L.3z}U~ *X.-MpC9QnSuMBEnoһ'%z2$tAlGǎ2hh8C6+4Kz04rKa׶x K1Q#Z|)zR93!V73)Sk~p+cuЌ6]ZE:|ꩧo:XM!ų,O^WK߷Kn{zw7Jw}RJ{tblMu?}p]^TG@$ʺ%^gy 2*9JuZIVҲkbP+a:n*LjY 9=i9Ј`fN"} V7o79LwQxQ㕘ټ4ÇuQ  iӴF R^(WlV) FOym@:e x/4 Ew 6JQDS8 vl\Og(Vs]$H}`l5a^6l|E3CgtM6:|g}B/B.b&~&QU;Cgk4 qH~F3{bv \Quow^5\[?0!ն}b L—j_?f >PĺS۵>^Q44PZiLCH^4*rcFU\G kQ8u=te.TrN̲N0U[1eʔ#ؖc+U'r]4E30+u%*P*bu'?.?@.M UiS;ArZI<Ἣ_$~؞bAKTW9dG:A~K.&9ЅpQs^bFx`ؿfSqDb)p~YL^'O([4ξǎo5|@`r\q Z!k*8G7HbW]j_n7aہX•`@JiрDA.Vl HcN11R d#;|WEetVb c-3a*6:MS;xA W1,o(l[PkS,@ۊh[ y'pi\MdZze0 *.͐ɀF4Ƕt{\g ?iH7k,`U x.ӡzWԿ/2-t>ukuci9yC([N "ag,3̑\9(p@ Ţ^LTΞJĉ ?c8qwy%izwuf71n Z_mò=̑w;PJAjoc xeڈQLe ;SZ~ә2e+SL[=JAtiL2e)SLxeʔ)S^2e+SL2ʔ)SL2e+L2eʔ)S 2eʔ)SL242$pCϓR GdBbh="s g t[ )Sxb'ۨ!Lص X'DW»j)&vORmE7Kn%U.Jb#v&j i-J* :_sRy%)ƩOUbzBv΋/Nm+ݕ :I)0Wc F[ @-`kK/#&iVVrgh[J ņ`O9cV Ν+y/XI#=dz48>< Нw) k֬ E讃>Xu Bb%4mڴJ}O}$[0aUJ%WqÇ/&[:qkn(]NK x EuaK1jM8d!p#2$)!'y).!Y N2T~3>h9wxdKo,Y2^zk׮)đTȈz+ g}v,yB跤剥&jEUoUVl3%J*!+a}ϕvΖ*WEeGs?,%Of~ʁXلږBq hL|QQ朶+~w{~-j:3'Tz/;3B__IyFhISQ3gNR-;3^Ad[8}uU|Є%E4m~]^Ҡ^tEo{P[@WH*\p*Kzj twX믿TdzJ~mQ>S ( ,h𢦽/K.[n#j4m>+Vv\\ SIC=tm0Żr7JoRV|yԓ)|Nɓm=NbxoVrr=!U^jAH=-E -sOo iPe]3sZ=Etu ˖Q֎U"=ipnnyES؂z뭋-Cgg2>).b$xTrd&G8Jg%u?묳ݶ۶~s=7.˷GlhXWVJxtiO ^E#cx]yTA%xoKI[ׯ__b'.vD#7xa蘄Y&xruxᄊ\9/]s-ɩsUꜿ:^%tOeacEFy*R|Be (-\YJ>%J4Qm޼|BN({#/q [न0Wb]3۴*5TMJx 7"PSZ:x%yef,NFc EWbҝ[q eBuXKҤ"u xa>!QNUJѪU>9ؿڹlAej[ֹ ., /Z-/^W[IhE K 4]q$EE]ǣxEXë 5#ޠ-J _*;p| c:$`>9{wZ}-O>#'+N;}f1׶l"=xhS-e/JuJro^vI0 +r,9F#Q.c>xQujJzWxL3rEkՈț/՜*:.1 ?eʔ*"RUdĉ.Ta-*_^Ťeo9֊,IslBNNWu!R+^UPq;0W4DVUi42KrJJ]@/|Cbx8 ҃睦.H"G?mr%EU@ 8CZPvm74 (ED'Ȅ&--ЦRb9J.OB/bHJ .&̦Y6:y_ DIX*घG I`Dz1j1I'$ Bvs'8 S0emxA;yc|N"x%#[bge])ss R KP*BP_~$Gy睒Jf͚(ᮻ?8VgC`6mZRHpazMFdꋒ-N0A>*R_CDH=tw#Ia|XpIК׀,@Ms5f*)fjHd" -Xϋn-]\{}[`ՈD"bپKaX:ARQ K|]i|]DcW"?'s'xz|݈S^ pwlNCz$@$"Ⱥ+찥h5W1-9,Y2E$.ݿAeJoohl1J=k\v?8K]*2=/vmO=>sUmfV۹[!J8$IH´K4Q>S=]bXl2K<$7Fb(?'X>E@?yd{MZslH3sZ=UWZatxUճAJmi]2C4vO~,lvпU Ⱥ ):V n%)͵3oEqӣps,4)MS1V/+õPIȄUd;Rb%hQuN6^mǷx {$d…m~6*7]z!{x1ZO|[* UE):V4F4R4E]L7 t)FAdi(ޣE2eVē&gN8kC]^mi UJJ0p􃩱JwKc 2`QXy#+%FQaoN#h4͉V7%aZ5Qh-,Z~*_&\{[@q*ّ) wj;FS]>O3RśK+Ѣ경j; lyb̗@D: e{J OlGtA7f~qr1-w_D9oW(iNVBjk:;:AZQ:ݢbr /n+UbNT^==_7PE#CiQ[IE9#BvS9*ś@1Yh4VLE t9 I>WM *Uloڎoxȫ(DeRy ra:Y*d]i렃Nf*)E5^ buZDbIlp8SmlqrF".%W*AO=.X^Qu-Mo{w!@(ap9Uxy_2'MJUfj ҃z%3ܹVY**(eߦu%a\|.a͇ $vM0+e(/:Enܯ)Bz@aȺ ):&/9m{GOԝv)ob_1D ¨5#aB#nO1z$QEE},h$JlB^jO4^bG!]4Og.a&P֪7^ū9UQl2+ێ0- ,85Uۃ9/HDs+.rjBT]L00ND3ᨖ0%EgGU^¬Cu7e4ڢ#ŋ%AQ HpdL"+lW^::dʕ$Ob^gj)4#G6jzlj'>aqO!-Ε\JVԛl%*#)jGtYf9AqC%-QgGx!L "lÍb) pĜ4|@(v- K3tEcu:zNF`el*.(A!-/g³)cSqBq/W$jUAtxXD3U栩π횄Y͛7 ?{t )i-9lOh~zIEN xQ3?X*f:\$m aM +ms'}Du2,(6zƋE8heHD7OS;xp= QJ=-kv. aC=ԩ.V9{L}G 0?8*+<0D)UdDiM1@gGxй¥ĩJĉ"8=OnV]ǂN_YSY+*a7*3rCF/=,Hpr cҼ&NJH*Lv"y 0]QOIf&KpPpgM<" #G; 8% ڴ&CL|NWϝ^ gW_%[=x90Cq#jmsVVmW9UumWΥ^6"(SLcBޤe4WL2eJp *3e+SL2eʔ) 2eʔ)SL2ʔ)SL2e+SL2eʔ)S^2eʔ)SLx)Ө99~DV]w?y#^|ݯO߻5wN1°c7%A>яf˔i^^"bf=,$OtNxec6*$HX(w) xnQ2W(rrH]vwJ !f) [T6 WJbFPER'#؟ūE:O,^?٥ݚ(߫ș]^9j%fhQP]Ce*oNrCLx5'8Cpk<N0!j$ 8RK!݇T1%_J!eO3:XB6sWb*K>><"C8fXAjUJe@c͚5ӦM8|HL}k=gtML`H"G/2u5o.=4=F7IHw용Ob]& !`xI'۞z$8zZq _I.ΆRi~1AŋNE <֮]+kȐB?G\~lR\dT0r]QO+T%cWi@P忺>q'~om?K?sgu^yW֖M~owq{\Ǒ7U_..͕'o{Raa../| Saǯ~wgʚ֜tI޼q7i՗2 XG}txV֗)^3f`j,^s ӄ>zŊ~ h~U|}g'c #FB4HoI)K\h^pīW PwG:*`߶ozl䦴Ǜz(#n:.-P÷~ٸr?:uUE@H`煢+<ϕg~߾췕x?>,ٹvaL׳q^y)گgmMKGRdޱ,ԶOeG-/iQ0g =~7ߌIu\anݐF?okIo" 7nݻ뮻2O:uݺu]gΜY'5EHʦX6=zXz{<=X J2)`>\uSN ?ӇE;KXK|߯?!҂0'.2c ~ 79/bR_!{mGx5m/##2S[.Zjqpp?Ox{V:َ??o[w~q?7CuMMᶦOsa(O;777}.wy#8"k ={9DN;@E$oQN~n`7Rxe&"L ~7~#~V{u]xe_\r w8:`QJ}v@!zWf(\Fx=69CTz|_y]M}뭷.Z 3d\׈/kf'6^zi~E}cQEW}V>+ᄟ4eCA O_EtqtiO^/ڕ.3 Fws xE"/GW AhhACbϞwkU(&le[/J)E\Xi1Yt6\hwlX+Dio1G6P=M Ĺ)J/+Gosqd5*9Zs!:Nr m= e{x&Ɇ*eW5:gиpytWqP{iJ{= L[< A^Sٔ*.x(}} -;Jy]̓O-Y^7tBU?yFA!h*LI&RmvVo 5k֑G2+,[VS I1Ziv>g].P8묳nHpFx_r+ tWgW^#~ $aį0aVƝwR'tҮWtxL~7C@ZD =7Z[kÆ q8T}H 6|2>e4^`=tGxGmuNH}%M("t7. :JL,ֺQ <mKݕs94j}^uc)ːF8^SEC%"b4(҉^ty;)@+Z:ꨣD14">G:4)#7|TǛ3 Fx;h#EmeKqE;w2Dxvw,eu; O`J>R2r&nlOl%5[?$s+!Oa/d LKw}71kll]`p Ӕ:oFnLkLD! O3RNsx!$s'b8;=#4><ÎZ :>dZ͔W@?r;="M ڣ9:s62S^`}&vrE]B <9(v8qbo{kt (aGtS&+yDZDKd{^dG8wE4ș#Z4_ F?EZ4BVEa-(m$փV2=1-Uƞ(AH\DŽ{bNO!XW~DZi%D;[i+D,`1mGR5ʜhw~'` Wƒ܃(]" (,;X ӛM!IKx 女WXBysHe޼yC >l)AǷV@ Dk|2 й-S"wH"qsTۼN8 72e)SLi3`,Ɵ|H`2eʔWL2u^ы!b*eiW>:"鹹s2eʔWL2uď=ucat ]ffʔ)S/6Iݚ)SL2eMxeʔ)SL2e)SL2eʔW$gsL=$B}*ӐrqvL2]5xt':!x^M L\q`Ps{b >1 ntHGT}1[P;t-Q*`#?JnQnS:h&Z>:xU(,N&KVfڋ/vxڕg+r]SrN/T>Si_|}Y53rLcx͝;W  A. =^4Gw _Q;N;S A)`,I/XG^iZ**bEJ);$xSQd-N0A*Rڴf͚iӦ1>$P1[(ݢݦ˕,O|j.E}z& ''؝7{SYwlkWX>`opoPUè #Ӹ^^{m|,%KSf~1wd-򷄟)^yYLx߿J*Yk\v Jm)WSW]uEFM(d  J()Yf/ۊVzSOp?XHy/rg+r]SL:((:Yh*oDU߄duWLvrQm Mj-*/n,dg ?i3բ4$})WΙ3m\יg)3(/_=nQn#IW}~^ o,k`Zc yܒ˫HN x%uiO!0YL[)ds_ґJ+Ak<餓dvÌ3va]v٥d۸q(Nn]e[뮻4#'OL5jAN~yvXJ.(Mf i[ .vmwqիW_ y5%Tw̙3kuˊW~6{{..YR0] qcmڮ~WkS֊Tp2@_}#5_fn𯐦yxo˒=U=@ %_zO:Rmƌ; &CxV3ͫsL Tծ(" N(28`HLP0A@(TЀFddkׁ?U|o<((3 4[;0x+WzԞEx4"Gۗsn7o&UV<&megJ] 6C''a_[oҤ KmUE>}HɽmR\azwR*iXl߾P^\ӡCԩf]&Dƍ=ΰ.qg4*/f:=H'aÆW2zFhp-c&y<^V' kr+d޽{sܖ c674XaRk-ݺu]$7?9븦N+S^Oe4,O֯j(qFBryup!(S\XnE={"hүȏ*U3CYY/umL׾׫ׁDpNx񌬋 %IԾ}^"Ziъ7n\`oVUkc9=֋m$^DE>;p`,dˏ""PyR8ȣgzi"Q!D۶iӆ*lr]#䗈-n.u!(i5DxV^MH&WSw^'NdV!)? ߣwxY-[zr#fQي䖑uW MU )m&: ӀPˊjѢ h"42Ru5V΢2N=ÜkXTC0*QJ](hL,l5s"meT+DBxlsɉzw/˗Ix_BggY&PO/^N w8?yA=)4?>BƨGt[G YOIGLD/rL_#4*εWf#>n4{rёy*ϱӧ畝u8upƐnN?/!/0x%6=BR\q\RpŎXNo<܍ٳg5ݰjw#4М"+;إ;7ڵk s%co"Ov9p[xD.sE_[F 4{Ӌz@g@|N " /#Px-`h=ׄ ^ժ\ΦMt@xc ߉ C(zeg51i`UD¤`4貃Q /&&6di-Wdߤcf65ӳf= aD@4cp ҹ%2U808[ |6ll+AFkGVrp_ѷRJqgoI~ICe+hRHCE$hJFcT{v /^ 9f=ᄈ̃X@4:=YHQauN FCO!^z%F1O¸8 YcCcĦP=gu!pJ5}7/n>ƙxloФ2!*[Ct[+$8S[\ۖ!ֳpDf{TX{Fx%>+4'F)IoBFReHKFO:4%;0L+F+;bU|.N4u/ *W!L5.5+{&yݿbDFcܲsDxSs: cDF<8V!D!{|S!,G^ݺ/WIOq, YqgR#n<nB!B!B!W ^?BW7#>^=ڤTB2YgŸvyR2)|W'GezD&`q=`gF:Mu8ʸgE,_0!=9A^tю%x[XސϨv 7[27x'|R,?ydQ ǏFי+0޽{ߌ9^q Mg;00S 1*nk[؜^;vShGqXBEL"D {wiZ1})s,0_Gco)h[2 τ_ @_Z5f־̨)8+Ul%ݻwȹst ͌":g@ 4o޼0ϨJhW]u8͚5(iw+F^g?C$9Sbeոkӎ{\t 15$3?oV觉 <3#0[<+X ,?2/V(+C-A2(F$Ja]lYag~I&V?áV[΢EԐaZx1OJqVFxbZk9nHᕣ7#Ucjɇ#Kv~@Zm[EH%>oU/Jo8cof'P#D ~ ҵbP=S^)/Մvsd"2N^A%wx!(4?)܆5kt'Gqɯ!*E oR+VP3fL[)1D6V!mG=pe.PN%\ٳ%\ ì%Ig~ /7k׮^tL@w C19WZ}q%_2:o-@w=[f0 01QKW|~=-l/Iv[q_xL"peÉIOە6m ;ܤI\8CI >oqfHF͛7Ǟ۞:2OhE۷ɍ̬s7k)dYe"ƍ (E!ZxM8qD_W_K,Rmb[6<2^-ZE:l αv߾}fO)%!eAreQx}PX"O^z0\k RKLdKqZ=r` Mm}~ivi{}$kqz#1_1:/4+[7kpe.mgh7>q>1$V.v׀^$_[9"O&ցJ ,O?$iYQ΄W6mxOsˆ =^衇2ゕa]B* Icڵ#X)^l /dZMy6xy&9W:H!K]F~PdF[*bHXpϿy=dfYh6#$,>*ԍ($vB츈JO~=^4H/2Ñ f72+2pׄPk!ŒcD 3Ś~±F@ѳ` ,tm;f9_8;! ٜx~= dL"1fm(3 Zj4UajQuhd5J/r+c#i|Ubt04˷mۆ ӯpi8+P0hUqq4gs;AǥȡI.DQa,X P# ˌ= M8c-8zBB !DA3!  Yo;-B|tKZ !BHx !BK!$E̪/V/+*KB /!BK!%%h^?B]Sk2b;:WcTy^Ǵ l7kh̸ ̬Ng4;eP"Ȗ Z̘qg2ri~=\#cGV?Zכ3bW_w Bv|Gy:L|-㦷m"҆^gW& {E>+>[Q\3UQ tR ^`!Z'ݖAScn L}Ɔ=묳ʝwu1Mcd;<cNɌ3f=13êcy|A̔бcGnFiȑ!_(Ml`:fKu^1DZ [ē7(fì4˘uNߍW^y7 `njL=6 _u9^}ѠA?!l?3#wj޽BxQ"G_X.A /\rh\#1s7G5||kO fܸƨ %9y|Mc>|7mɔs[[W|e],ON[rĵ{[ΪȅW\˟M s iӦٯL̉-[I 0+m&PBպus=gT{R2aMlE[~]^>spr-qW1]w*-)Ɛ=ڸK$(v>w-ΐʌыS\s-%yگ.qb>n'_G~?~{^9"ܳ|]{>[vANdd˽mE^-M0-1s|frim+)֭k{L&5_3FW_}5=6+o %xE:(d/]Q̾D̄ŐKn3)G^ "pV t3:tGYN͛7#i>~mcs'3i:b f|ܴE7L6ȣ%:ch;i~ ?\ءoڧ>.xgz]c)SR]uS:U~ڮz';i!W㚿=v.^: Q5vX*P4f>ǟt<s1=GåGT:b]_ڇ ~3ۗhvڽvU@'|r̙c׮^ׯ_\[xͩr\$g{^zw2[˩"K3V &*ۺ֭e<M/Mco=ެ}6=\V̛}m~;3u?nQ0dco^xE39!:T<9uB#x=PmxѣGk֬)4!w'uzɦbW[n-}FD)aӟ[lIuq7Z1Jm!W2gk֬Ḯ-㦽-J.W~6[vaV1)5h|wV ~b0: ԴmS 4a1O~=IN #o'Kkԩqmk.{t+o$+|_}Ha3-h/fQP6L[Sv"EKJJ5kF*ގ;\oҪlGLF M|HΝ:9^O¨䌒[oC#ʩ$$= /"HR~o/8qifիRJD,Zo/r 87mӦv7m4\+j@PbI ;wA41;<,jĈxXXRn%G@u?vn;t4eԩS /V:dȐfd&Y[t5.g).AآNCxŽ^TYc|ݽ{2yD4qxi3\6FI:?l32IbD2M{[~[F`.]D(|]e$wy&s<>P!Ç{؈~`=FKwIc*w=r@oʚkg?2_WdpV^snzuSDq훷k2Nj6:o):"~Gz/lmܓ/7&eذalg5ml\6Pömې/ZjIc^H%?YlGs /k)Ç oV4 >XqF~`5Q#EjҤرc˿/#f"`NnI"D >$jan{Fo?8*?L-ómoAU7oޜaz3^> FmS6RWk -txF(`]ƥj1\^I:j7 R, io/K(~p_8 FIkhKmTg`*UAu{J1T;BWtUR>n ݪ䃒9A"RyEaqⲫ~;/4x̘19o5#/(b8ZrG" K+n{dž[xh[wOUI ֛^Pxxw^:BZ~MOx*VuY&$ڻ_;rHN}m*Q37 iQ(}#P&~21A͗oKĐIWR&Qz/9Z4#F ^Af )G=ݸj 4^Yf-XK#!B%D'ð]:5eZ`,x%^V6MoײC+ɬXo'NHο'12LuۤNG!]{{Ĭg ʛ%fWxİv qQMx=i-ghRr E ^ؔ>L|B٦1zAAR&hpr,56 61r9Px"6Y%u(R׻/5]Bmm֎ͮiF*q%Yi ǽH'kٸL^9(C_-?;G58gNM8C8<^ďxLC=hn6nN-&f%׻&T"%.r -7]N]k4 cHm¥J9"_°^,49(>s!XfHxY}Fcx^/܀ /7QAYhW13Nv2$H o0T|&1tEE dt\{6:Q?UǍ[>gMERF@<.*)j\J:\.kdvB!MHH;Gt i /qqw " 9^+Dx]5ꂝ Nj;g+D]۱ږ-[k|%ָqD-xUhs9rܶmۆ6wq"P`q'3+ꮡY^'6m V|;au`LJ>P/yvQ0ҙwh#W%dۦvv /̦6چ^`la3#sh @mСr-,;׋vğDHFp$3AeaRwe 糩Bix %׺Qmsѧ8nyֱC급J]b3B8*jP ae7f2zyNpGW,r>Omǂ0[ _)DZ3qu9(\dwOvWfL(?&|*ߡ?#bywOٜ$}E[Yq\ Ÿᰤ3ƞY+7 FbJ]a>T%lȞ U#0s GtoAQ`o/"(`nzLRA5)~帓X~MZC3^ƍlذ.+3pR6.^ +&AyX&CCyxQa̓N 2`@!Ml a_7_JzE )f3cn[zU 9r=#' b}C$a Il+\Co%W(5Q$YW7@[W,?½mܹI^zYߓd$Ն[^;wL98##nHѱkO0} }HApAQaQp(47{`3qflΔ&mog/}oWqg?.-;H[P`PNJa [k6<}C1+r?&y|>MX)F$4a dN9%[REBK!ߴx7eUT|  o w1f( / /!$WsǬh03F5I%$DDK!k]{-r^Hۛ,$$B!B!B!B!B!^B!^B!^^B!^B!^B!BK!BK!BKK!BK!BK!BHx !BHx !BHxIx !BHx !BHx !B /!B /!B / /!B /!B /!B!%B!%B!%%B!%B!%B!$B!$B!$$B!$B!$B!B!B!B!B!B!B!B!B!B!B!^B!^B!^^B!^B!^B!BK!BK!BKK!BK!BK!BHx !BHx !BHxIx !BHx !BHx !B /!B /!B / /!B /!B /!B!%B!%B!%%B!%B!%B!$B!$B!$$B!$B!$B!B!B!B!B!B!B!B!B!B!(o>B!9RŗB!D?~|Bv!B!COx[nڵ)~K.裏˞ѣGRF&MRZEVX /T;oꨣy_~k6/ڂ[_.GG@=o}ʕ=܌(g}vr #FY&ެY%KyII ujn߾}O>nN`֭[WZ㎻+S=b|AΪiӦϷ 9!8mwK`u`jW='tRj՞x≊gw:$=X_UW]u dqb+QkСiz?WKxamy$][pZj5R{ף},ZPqԄDkDIomP=P 4gw~s_P0, @>|8{;cav… =؋.^:5{l|^{߷Yva4dBW^UT]zz_ /亸pŋۻw'▶_ E͛7?SrŞ+jJxy74,{~iQΝּ!- ,_ESNoÆ Jxm ._y)5QNԩBQk3Zչsg[,XPR%L=vi8 &O?Wd#G+UVќn›RI ۩SrwQ9z@9s+W I&aiDd@TI kԨ뷷'ƫ,կ~uyU={$g~'v 'p작lHcK.*q5kEވZjшoܸ1?ba˖-y+С.>ɰZn=[r-ìMg Å^S5ՙgq Fq3vҥAD /9Wp=Cj׮]hرOӧOZhwWj馛?&|ԨQj'ZxG*XXxծ* xf M"X-!M@0)4@H$@BN-6d#Sp_],>g}9&|>gkzwk>Cqɵ|P'O,zot5>9p֝ݽ{R+WCK"ЦMxU@}駟h p@ѥHpPX[* !C.t\p /TqvoV<优:Ιbx=V!;oYphx ]oͨye̼$ЊH}ZhӪ> /->m_}k׮yjdoS* >gNⲭ$_~Fg(bNgգPB.Dom^~]קҪBNK{3g.qF]V7GKUիœ}$mۍVm9IsS2 3pstUVkVj2H<ٳg$6cP<0䍚3\|.TO};2^^ti~a /z=(5«:ڱG/h6#D\۬>|ך ^5kI5߿ߞԹ<优:yϻKeI뤞GW49KrmL5H-YoƘ+eU6_(9/r晪o[yUNjo@'njE_nvKgGE[ӍkC~ cDHԨo5'3wm c헕ɶy{>3v9kJr Lcg 5kt^x5ZC݅׶קs^lhUxuujnɓ̧9}Ǣ+8Mp]ڢ ;:ɺtVm[WvKUҶCPG=z-}G5y1u*QO^U$dkީU{6=t5q{bٵFk# _QJ9gs5.=CU,Ẓnݺ5~+gck! J{m V&{岭$,5aRT&Qx r^L'6Jl+],^.1ќrGR])&4"r#̀lr;9/sUxEWY( #kwyH&d ~nI-Z܈wH7da ISolc[,lR@dnEamӢoC| r^L\Ѹ(.׍WPx1au/L+޽{m^q" d#cat1~38t|SBOQ]P%yf᥵抦V /Q͡.okפ0{SS9u|*k0BIƩ! +a0@ <!͇ٛ\_ {XU8~BKa4db?%Ǵcƅ/VaG^^[#pPDq4T{s7#G۱8;[I9zK^/^!pH$,&z%bӤv/˅^B!)(B!B!B!B!B!B!B!B!"B!"B!""B!"B!"B!BW!BW!BWW!BW!BW!B !B !B !Bx<'jIENDB`transmission-0.12.2/NEWS0000644000175000017500000003013713764054547014701 0ustar dogslegdogsleg* Changes in 0.12 ** Features *** New custom variables `transmission-add-history-variable' and `transmission-tracker-history-variable' for storing history of torrents added and tracker announce URLs input. The defaults are `transmission-add-history' and `transmission-tracker-history', respectively. ** Changes *** `transmission-turtle-mode' replaces `transmission-turtle-toggle' The former mentioned is an actual minor mode which shows a lighter in transmission buffers. *** RPC request functions work differently now. The "results" string is checked when HTTP status is checked and signals an error if not "success", which is caught and messaged. Only the "arguments" object is parsed, if possible. The return from `transmission-request' is now the decoded arguments object, and the argument to `transmission-request-async''s callback is the same object instead of the HTTP response content. * Changes in 0.11 ** Features *** Dired-like marks for torrents and their contents. For items such as torrents or files within torrents, actions which either acted on point and the items on region can now be applied on marked items. Marks take precedence, followed by the region, then the item at point. **** New commands for manipulating marks: `transmission-toggle-mark', `transmission-unmark-all', and `transmission-invert-marks' bound to the corresponding keys as in Dired. *** Added commands for viewing or displaying torrent files corresponding to various dired commands. These include `transmission-find-file-other-window', `transmission-display-file', and `transmission-view-file', bound to the corresponding keys as in Dired. *** New command `transmission-turtle-status' to show the state of turtle mode. ** Changes *** `transmission-turtle-set-days' now disables the schedule with a prefix argument instead of when empty input is given. This facilitates independently changing the days and whether the schedule is enabled. *** Many interactive functions now have signatures. These commands now somewhat reflect the Transmission API, making them more useful from Lisp. *** Bindings for adding and removing trackers in the info context have changed to "a" and "r", respectively. *** `transmission-move' is no longer bound to a key. *** Network connections are kept alive and reused. * Changes in 0.10 ** Features *** New commands for controlling turtle mode, or "alt-speed" mode. **** `transmission-turtle-toggle' for toggling whether the limits are active. **** `transmission-turtle-set-days' and `transmission-turtle-set-times' for indicating the days of the week and time of day when the limits should be automatically enabled, respectively. **** `transmission-turtle-set-speeds' to set the alternative speed limits. *** Executing a command on a file now uses MIME (mailcap) to guess possible default commands. These can be navigated with M-n in the `transmission-files-command' prompt. *** New command `transmission-free' to report free disk space *** New command `transmission-stats' to report session statistics *** Torrent specific limits are shown in info context. Specifically, ratio, download rate, and upload rate information. *** RPC authorization can be discovered via auth-source. `transmission-rpc-auth' is now like an `auth-source-search' spec, so passwords can be stored outside of `transmission-rpc-auth'. *** GeoIP associations can be cached. Unless `transmission-geoip-function' already has a built-in cache, using cache lookups can be much more efficient. This can be enabled with the option `transmission-geoip-use-cache'. ** Changes *** Torrent-specific limit commands are bound to d,l,u in info context. *** Verify command now guarded with y-or-n prompt. Accidentally invoking this on a large torrent is a waste. *** Wanted field in info context now uses "sizeWhenDone". Parsing the file data in order to compute actual wanted byte count gets expensive with more files in a torrent, as Emacs' json parsing is slow. Further, it isn't necessarily a useful measurement, as Transmission downloads the pieces containing wanted bytes, a superset of what was being computed before. *** Some commands that operate on a region now deactivate the mark. These include the move, reannounce, remove, start/stop, and verify commands. *** Automatic refreshing is inhibited if the buffer is narrowed. ** Fixes *** Point, mark, and window start are better managed across refresh. Before, if a particular buffer was being viewed in multiple windows, this would confuse `transmission-refresh' and the state would be lost. Now, the state is recorded and restored in each window showing a buffer, resulting in much improved behavior. * Changes in 0.9 ** Features *** New commands for setting torrent-specific limits. **** `transmission-set-torrent-download' and `transmission-set-torrent-upload' control torrent-specific speed limits, while `transmission-toggle-limits' toggles whether a torrent honors the global/session limits. **** `transmission-set-torrent-ratio' controls the mode and threshold for ratio. *** New option `transmission-time-zone' for configuring timestamps *** New column "Location" in the *peers* context for translating IPs. The new option `transmission-geoip-function' is used to indicate which function to use to get a location from an IP. The function `transmission-geoiplookup' is provided to call the geoiplookup(1) utility. *** `transmission-format-pieces-brief' upgrades to finer greyscale in 256 color terminals and graphical frames. *** Digit grouping delimiter is customizable. Use `transmission-digit-delimiter' to control it. Defaults to comma. ** Changes *** Pieces display customization has changed. Custom variable `transmission-pieces-display' is replaced by `transmission-pieces-function'. This more easily permits extension of pieces display. For instance, using some other function to output pieces as an image, or with a different color map. *** Option `transmission-timer-interval' renamed to `transmission-refresh-interval' *** Option `transmission-timer-p' replaced by new option `transmission-refresh-modes', a list of major modes in which to use a timer to refresh the associated buffer. *** Automatic refreshing is inhibited by isearch or an active region. *** Works with absolute path requirements in Transmission 2.90+ * Changes in 0.8 ** Features *** New context with peer information. *** New command for copying the current torrent's magnet link into the kill ring. *** `transmission-add' now has a conditional default argument. The default argument is a guess of a valid torrent at point, either as a URL, file name, magnet link, or info hash. The behavior can be customized with the new variable `transmission-torrent-functions'. *** New one-line pieces display. Visible when new custom variable `transmission-pieces-display' is set to 'brief. ** Changes *** Global upload speed limit and seed ratio limit can be set to zero. ** Fixes *** Trackers are no longer added in reverse order * Changes in 0.7 ** Features *** Removing trackers can now (finally) be done by announce URL in addition to removal by tracker ID. *** Numbers in the *info* buffer now have comma separators *** `transmission-request-async' has a new argument CALLBACK. *** More information is shown for trackers. This includes announce/scrape times and the error string for failed announces. *** More information is shown for peers. Peers "interested" in receiving and with "unchoked" bandwidth are shown. Additionally shown are numbers of peers coming from either DHT, PEX, etc. *** New custom variable `transmission-trackers'. This is used to store trackers which are added to the completion candidates when adding a tracker to a torrent. *** New command `transmission-trackers-replace' ** Fixes *** The file list header line updates correctly when units change. * Changes in 0.6 ** Features *** Each major mode now has a menu *** Torrent list conditionally shows ∞ in place of Inf for ETA *** Adding trackers now has completion. The collection of things to complete is a list of trackers from all torrents known to transmission. *** Applied some optimizations **** Replaced some seq- functions with their (faster) cl- counterparts. Ultimately dropped the dependency on seq.el **** `transmission-draw-info' should be faster now **** Set `line-move-visual' to nil in tabulated-list buffers *** New function `transmission-request-async' for sending requests without blocking to wait for a response. * Changes in 0.5 ** Features *** Download directory can be specified when adding a torrent by prefixing `transmission-add' *** New command `transmission-move' for changing download directory *** `tabulated-list-mode' is employed for torrent and file listing. This adds some sorting facilities to these listings, as well as a header line. *** New command `transmission-find-file' for visiting from the file list *** More information is shown in the info context *** Pieces are only shown if the torrent is in progress. A count of pieces obtained and a percentage are shown. ** Changes *** Renamed some things **** `transmission-file-size-units' renamed to `transmission-units' ** Fixes *** transmission-quit is smarter now *** More use is made of global and buffer-local variables For instance, `transmission-torrent-id', `transmission-torrent-vector' * Changes in 0.4.1 *** Display of pieces is now correct *** `transmission-files-command' now tries incomplete .part files *** Use 'font-lock-face instead of 'font property Fontification goes missing sometimes otherwise * Changes in 0.4 ** Features *** Switching buffers is way less annoying now Point/mark is better preserved in buffers. Opening the info and files buffers simply refresh if they are the same one (torrent id) that was drawn before. The transmission-draw and transmission-refresh functions now do what they're supposed to do. *** Torrents can now be added by BitTorrent info hash *** New commands for adding and removing trackers *** Support for RPC username and password *** More HTTP errors are caught 401 for bad RPC credentials, and a few others for an incorrect path to the RPC ** Customization *** Session address The RPC path variable `transmission-path' is now named `transmission-rpc-path'. *** RPC authorization New variable `transmission-rpc-auth' allows RPC username and password to be configured. ** Bug fixes *** transmission-prop-values-in-region does the right thing now * Changes in 0.3 ** Features *** New commands for bandwidth priority, reannounce, global seed ratio limit *** Leading directory in files list is truncated if possible *** Added more torrent information to *transmission-info* buffer. Tracker URLs and tiers, along with peers, seeders, leechers, and snatches from each are displayed. Minimal peer information--numbers connected, seeding, leeching--is displayed. Torrent pieces are now displayed. Needs fixing. ** Added a Makefile and README ** Bug fixes *** Fixed issue with not being able to add .torrent files. *** Fixed a divide-by-zero bug. * Changes in 0.2 ** Now compatible with stable Emacs 24.4 * Changes in 0.1 ** Basic Transmission torrent client. "transmission.el" is an interface to a Transmission session for GNU Emacs. Entry points are the `transmission' and `transmission-add' commands. A variety of commands are available for manipulating torrents. Some of them can be applied over multiple torrents simultaneously by selecting them within a region. *** Features a torrent list and two other contexts: one for the files contained in a torrent, the other for detailed information about the torrent. One can start/stop, verify, and remove torrents, set global rate limits, and set individual file priorities. *** Customization **** Session address The variables `transmission-host', `transmission-service', and `transmission-path' have the default of a local Transmission session. **** Appearance The variables `transmission-file-size-units' and `transmission-time-format' control the appearance of file-size/bitrates and dates, respectively. **** Timer The variables `transmission-timer-p' and `transmission-timer-interval' control whether the torrent list updates automatically, and at what period. Local variables: coding: utf-8 mode: outline paragraph-separate: "[ ]*$" end: transmission-0.12.2/LICENSE0000644000175000017500000010451313764054547015207 0ustar dogslegdogsleg GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .