1 ;;; iplayer.el --- Browse and download BBC TV/radio shows
3 ;; Copyright (C) 2012-2013 Christophe Rhodes
5 ;; Author: Christophe Rhodes <csr21@cantab.net>
7 ;; Keywords: multimedia
9 ;; This program is free software; you can redistribute it and/or modify
10 ;; it under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation, either version 3 of the License, or
12 ;; (at your option) any later version.
14 ;; This program is distributed in the hope that it will be useful,
15 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 ;; GNU General Public License for more details.
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
24 ;; Requires and uses the 'get-iplayer' script to provide a
25 ;; convenient interface to BBC iPlayer.
28 (defvar iplayer-updating-cache-process nil)
29 (defvar iplayer-updating-cache-sentinel-info nil)
30 (defvar iplayer-updating-cache-sentinel-executing nil)
32 (defun iplayer-updating-cache-sentinel (process event)
33 ;; FIXME: assumes that all went well
34 (let* ((iplayer-updating-cache-sentinel-executing t)
35 (info (reverse iplayer-updating-cache-sentinel-info)))
36 (setq iplayer-updating-cache-process nil
37 iplayer-updating-cache-sentinel-info nil)
39 (let ((iplayer-command-frame (nth 0 info))
40 (iplayer-command-window (nth 1 info))
41 (iplayer-command-buffer (nth 2 info))
43 (function (nth 4 info)))
44 (when (and (frame-live-p iplayer-command-frame)
45 (window-live-p iplayer-command-window)
46 (buffer-live-p iplayer-command-buffer))
47 (let ((old-frame (selected-frame))
48 (old-window (selected-window))
49 (old-buffer (current-buffer)))
50 (let ((pre-command-hook
52 (select-frame iplayer-command-frame)
53 (select-window iplayer-command-window)
54 (set-buffer iplayer-command-buffer)
55 (setq pre-command-hook nil))))
56 ;; KLUDGE: execute-kbd-macro executes a normal
57 ;; command-loop, whose first action is to select the
58 ;; current frame and window, which is why we contort
59 ;; things to select the frame/window/buffer we actually
60 ;; want in pre-command-hook. I'm actually surprised
61 ;; that it works, but mine is not too much to reason
62 ;; why; lots of other ways to try to achieve this didn't
64 (if (version< emacs-version "24")
65 (execute-kbd-macro keys)
66 ;; KLUDGE: we store the function name, which is fine,
67 ;; but some of our functions need to know which
68 ;; keystrokes were used to invoke them, so we need to
69 ;; pass those along, so we need to make sure that all
70 ;; iplayer-functions accept an optional argument, argh
72 (funcall function keys))
73 ;; KLUDGE: and then we restore old state
74 (select-window old-window)
75 (select-frame old-frame)
76 (set-buffer old-buffer))))))
77 (message "Done updating iPlayer cache")))
79 (defmacro define-iplayer-command (name arglist &rest body)
80 (let (docstring interactive)
81 (when (stringp (car body))
82 (setq docstring (car body) body (cdr body)))
83 (when (and (consp (car body)) (eql (car (car body)) 'interactive))
84 (setq interactive (car body) body (cdr body)))
85 `(defun ,name ,arglist
86 ,@(when docstring (list docstring))
87 ,@(when interactive (list interactive))
88 (unless iplayer-updating-cache-process
89 (setq iplayer-updating-cache-process
90 (start-process "updating-iplayer" " *updating-iplayer*"
91 "get-iplayer" "--type" "radio,tv" "-q"))
92 (set-process-sentinel iplayer-updating-cache-process
93 'iplayer-updating-cache-sentinel)
94 (message "Updating iPlayer cache"))
95 (if iplayer-updating-cache-sentinel-executing
97 (push (list (selected-frame) (selected-window) (current-buffer) (this-command-keys-vector) ',name)
98 iplayer-updating-cache-sentinel-info)))))
100 (defun get-iplayer-tree (&rest args)
102 (apply #'call-process "get-iplayer" nil t nil "--nocopyright" "--type" "radio,tv" "--tree" "--terse" args)
103 (goto-char (point-min))
104 (let (result program episodes)
105 (while (< (point) (point-max))
108 (when (and program episodes)
109 (push (cons program (reverse episodes)) result))
110 (setf program (buffer-substring (point) (progn (end-of-line) (point))))
111 (when (string-match "^\\(tv\\|radio\\), " program)
112 (setq program (substring program (match-end 0))))
114 (unless (= (point) (point-max))
116 ((looking-at "^ \\([0-9]+\\):\\s-\\(.*\\)$")
118 (cons (buffer-substring (match-beginning 1) (match-end 1))
119 (buffer-substring (match-beginning 2) (match-end 2)))))
120 (when (string-match "^\\(tv\\|radio\\), " (cdr episode))
121 (rplacd episode (substring (cdr episode) (match-end 0))))
122 (push episode episodes))
127 (defun display-iplayer-tree (tree)
128 (with-current-buffer (get-buffer-create "*iplayer*")
129 (delete-region (point-min) (point-max))
133 (let ((program (car entry))
134 (episodes (cdr entry)))
135 (insert (propertize (format "* %s\n" program) 'face 'outline-1))
136 (dolist (episode episodes)
137 (insert (propertize (format "** %s\n" (cdr episode))
138 'face 'outline-2 'iplayer-id (car episode))))))
140 (goto-char (point-min)))
141 (switch-to-buffer (get-buffer-create "*iplayer*")))
143 (defvar iplayer-presets
149 ("!" . "BBC Radio 1")
150 ("\"" . "BBC Radio 2")
151 ("£" . "BBC Radio 3")
152 ("$" . "BBC Radio 4")
153 ("%" . "BBC Radio 5 live")
154 ("^" . "BBC 6 Music")
156 ("*" . "BBC Radio 4 Extra"))
157 "Alist mapping keys to iPlayer channels.
159 Used in the `iplayer-preset' command.")
161 (define-iplayer-command iplayer-preset (&optional keys)
162 "Switch display to a preset channel.
164 The presets are defined in the variable `iplayer-presets'."
166 (let ((keys (or (and keys (concat keys)) (this-command-keys)))
167 (presets (mapcar (lambda (x) (cons (read-kbd-macro (car x)) (cdr x))) iplayer-presets)))
170 (let ((channel (cdr (assoc keys presets))))
173 (setq mode-line-process (format "[%s]" channel))
174 (iplayer-channel (format "^%s$" channel)))
175 (error "no preset for key %s" keys)))))))
177 (defun iplayer-channel (channel)
178 (display-iplayer-tree (get-iplayer-tree "--channel" channel)))
180 (defun iplayer-download ()
182 (let ((id (get-text-property (point) 'iplayer-id)))
184 (let ((default-directory "~/iPlayer/"))
185 ;; should probably use a process filter instead to give us a
187 (message "downloading id %s" id)
188 (start-process "get-iplayer" " *get-iplayer*" "get-iplayer" "--modes=best" "--get" (format "%s" id)))
189 (message "no id at point"))))
191 (defun iplayer-previous ()
194 (outline-previous-heading)
195 (while (and (= (funcall outline-level) 1) (not (bobp)))
196 (outline-previous-heading)))
200 (outline-up-heading 1 t)
203 (defun iplayer-next ()
206 (outline-next-heading)
207 (while (and (= (funcall outline-level) 1) (not (eobp)))
208 (outline-next-heading)))
211 (outline-up-heading 1 t)
214 (defconst iplayer-mode-map
215 (let ((map (make-sparse-keymap)))
216 (define-key map (kbd "0") 'iplayer)
217 (let ((presets "123456789!\"£$%^&*()"))
218 (dotimes (i (length presets))
219 (define-key map (read-kbd-macro (substring presets i (1+ i)))
221 (define-key map (kbd "RET") 'iplayer-download)
222 (define-key map (kbd "j") 'iplayer-next)
223 (define-key map (kbd "k") 'iplayer-previous)
227 (defun iplayer-mode ()
228 "A major mode for the BBC's iPlayer.
229 \\{iplayer-mode-map}"
231 (use-local-map iplayer-mode-map)
232 (setq major-mode 'iplayer-mode mode-name "iPlayer"))
234 (define-iplayer-command iplayer (&optional keys)
235 "Start the emacs iPlayer interface."
237 (setq mode-line-process nil)
238 (display-iplayer-tree (get-iplayer-tree)))
241 (autoload 'iplayer "iplayer" "Start the emacs iPlayer interface." t)
244 ;;; iplayer.el ends here