Christophe Weblog Wiki Code Publications Music
add CBBC/CBeebies presets
[iplayer-el.git] / iplayer.el
index 1b02d16b6beed2e6a17f63a95977b1c55a7a9ac7..cb61f3b5bf4db81d8e37e63c9b9dfc45ceb368fe 100644 (file)
@@ -1,6 +1,6 @@
 ;;; iplayer.el --- Browse and download BBC TV/radio shows
 
-;; Copyright (C) 2012-2013  Christophe Rhodes
+;; Copyright (C) 2012-2014  Christophe Rhodes
 
 ;; Author: Christophe Rhodes <csr21@cantab.net>
 ;; Version: 0.1
 ;; convenient interface to BBC iPlayer.
 
 ;;; Code:
+
+(defgroup iplayer nil
+  "Browse and download BBC TV/radio shows."
+  :prefix "iplayer-"
+  :group 'applications)
+
+(defcustom iplayer-download-directory "~/iPlayer/"
+  "Directory into which shows will be downloaded."
+  :group 'iplayer
+  :type 'directory)
+
 (defvar iplayer-updating-cache-process nil)
 (defvar iplayer-updating-cache-sentinel-info nil)
 (defvar iplayer-updating-cache-sentinel-executing nil)
     (setq iplayer-updating-cache-process nil
           iplayer-updating-cache-sentinel-info nil)
     (dolist (info info)
-      (let ((iplayer-command-frame (car info))
-            (iplayer-command-window (cadr info))
-            (iplayer-command-buffer (caddr info))
-            (keys (car (cdddr info)))
-            (function (cadr (cdddr info))))
+      (let ((iplayer-command-frame (nth 0 info))
+            (iplayer-command-window (nth 1 info))
+            (iplayer-command-buffer (nth 2 info))
+            (keys (nth 3 info))
+            (function (nth 4 info)))
         (when (and (frame-live-p iplayer-command-frame)
                    (window-live-p iplayer-command-window)
                    (buffer-live-p iplayer-command-buffer))
           (let ((old-frame (selected-frame))
                 (old-window (selected-window))
                 (old-buffer (current-buffer)))
-            (let ((pre-command-hook
-                   (lambda ()
-                     (select-frame iplayer-command-frame)
-                     (select-window iplayer-command-window)
-                     (set-buffer iplayer-command-buffer)
-                     (setq pre-command-hook nil))))
-              ;; KLUDGE: execute-kbd-macro executes a normal
-              ;; command-loop, whose first action is to select the
-              ;; current frame and window, which is why we contort
-              ;; things to select the frame/window/buffer we actually
-              ;; want in pre-command-hook.  I'm actually surprised
-              ;; that it works, but mine is not too much to reason
-              ;; why; lots of other ways to try to achieve this didn't
-              ;; in fact work.
-              (if (version< emacs-version "24")
-                  (execute-kbd-macro keys)
-                ;; KLUDGE: we store the function name, which is fine,
-                ;; but some of our functions need to know which
-                ;; keystrokes were used to invoke them, so we need to
-                ;; pass those along, so we need to make sure that all
-                ;; iplayer-functions accept an optional argument, argh
-                ;; argh argh.
-                (funcall function keys))
-              ;; KLUDGE: and then we restore old state
-              (select-window old-window)
-              (select-frame old-frame)
-              (set-buffer old-buffer))))))
-    (message "Done updating iPlayer cache")))
+            (cond
+             ((version< emacs-version "24")
+              (let ((pre-command-hook
+                     (lambda ()
+                       (select-frame iplayer-command-frame)
+                       (select-window iplayer-command-window)
+                       (set-buffer iplayer-command-buffer)
+                       (setq pre-command-hook nil))))
+                ;; KLUDGE: execute-kbd-macro executes a normal
+                ;; command-loop, whose first action is to select the
+                ;; current frame and window, which is why we contort
+                ;; things to select the frame/window/buffer we actually
+                ;; want in pre-command-hook.  I'm actually surprised
+                ;; that it works, but mine is not too much to reason
+                ;; why; lots of other ways to try to achieve this didn't
+                ;; in fact work.
+                (execute-kbd-macro keys)
+                ;; KLUDGE: and then we restore old state
+                (select-window old-window)
+                (select-frame old-frame)
+                (set-buffer old-buffer)))
+             (t
+              ;; KLUDGE: we store the function name, which is fine,
+              ;; but some of our functions need to know which
+              ;; keystrokes were used to invoke them, so we need to
+              ;; pass those along, so we need to make sure that all
+              ;; iplayer-functions accept an optional argument, argh
+              ;; argh argh.
+              (with-selected-frame iplayer-command-frame
+                (with-current-buffer iplayer-command-buffer
+                  (with-selected-window iplayer-command-window
+                    (funcall function keys)))))))))
+      (message "Done updating iPlayer cache"))))
 
 (defmacro define-iplayer-command (name arglist &rest body)
   (let (docstring interactive)
     (when (stringp (car body))
       (setq docstring (car body) body (cdr body)))
-    (when (and (consp (car body)) (eql (caar body) 'interactive))
+    (when (and (consp (car body)) (eql (car (car body)) 'interactive))
       (setq interactive (car body) body (cdr body)))
     `(defun ,name ,arglist
        ,@(when docstring (list docstring))
 
 (defun display-iplayer-tree (tree)
   (with-current-buffer (get-buffer-create "*iplayer*")
-    (delete-region (point-min) (point-max))
+    (let ((buffer-read-only nil))
+      (fundamental-mode)
+      (delete-region (point-min) (point-max))
+      (dolist (entry tree)
+        (let ((program (car entry))
+              (episodes (cdr entry)))
+          (insert (propertize (format "* %s\n" program) 'face 'outline-1))
+          (dolist (episode episodes)
+            (insert (propertize (format "** %s\n" (cdr episode))
+                                'face 'outline-2 'iplayer-id (car episode)))))))
     (iplayer-mode)
     (orgstruct-mode 1)
-    (dolist (entry tree)
-      (let ((program (car entry))
-            (episodes (cdr entry)))
-        (insert (propertize (format "* %s\n" program) 'face 'outline-1))
-        (dolist (episode episodes)
-          (insert (propertize (format "** %s\n" (cdr episode))
-                              'face 'outline-2 'iplayer-id (car episode))))))
     (org-overview)
-    (goto-char (point-min)))
+    (goto-char (point-min))
+    (if iplayer-current-channel
+        (setq mode-line-process (format "[%s]" iplayer-current-channel))
+      (setq mode-line-process nil)))
   (switch-to-buffer (get-buffer-create "*iplayer*")))
 
 (defvar iplayer-presets
     ("2" . "BBC Two")
     ("3" . "BBC Three")
     ("4" . "BBC Four")
+    ("8" . "CBBC")
+    ("9" . "CBeebies")
 
     ("!" . "BBC Radio 1")
     ("\"" . "BBC Radio 2")
 
 Used in the `iplayer-preset' command.")
 
+(defcustom iplayer-startup-channel "BBC One"
+  "The channel to display at startup"
+  :type `(choice
+          ,@(mapcar (lambda (x) `(const ,(cdr x))) iplayer-presets)
+          (const :tag "Show all content" nil))
+  :group 'iplayer)
+
+(defun iplayer-frob-presets (presets)
+  (cond
+   ((version< emacs-version "24")
+    (mapcar (lambda (x) (cons (read-kbd-macro (car x)) (cdr x))) presets))
+   (t presets)))
+
+(defvar iplayer-current-channel nil)
+
 (define-iplayer-command iplayer-preset (&optional keys)
   "Switch display to a preset channel.
 
 The presets are defined in the variable `iplayer-presets'."
   (interactive)
   (let ((keys (or (and keys (concat keys)) (this-command-keys)))
-        (presets (mapcar (lambda (x) (cons (read-kbd-macro (car x)) (cdr x))) iplayer-presets)))
+        (presets (iplayer-frob-presets iplayer-presets)))
     (cond
      ((= (length keys) 1)
       (let ((channel (cdr (assoc keys presets))))
         (if channel
-            (progn
-              (setq mode-line-process (format "[%s]" channel))
-              (iplayer-channel (format "^%s$" channel)))
+            (iplayer-channel channel)
           (error "no preset for key %s" keys)))))))
 
 (defun iplayer-channel (channel)
-  (display-iplayer-tree (get-iplayer-tree "--channel" channel)))
+  (setq iplayer-current-channel channel)
+  (display-iplayer-tree (get-iplayer-tree "--channel" (format "^%s$" channel))))
+
+(define-iplayer-command iplayer-refresh (&optional keys)
+  "Refresh the current iPlayer channel display."
+  (interactive)
+  (if iplayer-current-channel
+      (iplayer-channel iplayer-current-channel)
+    (iplayer-show-all)))
+
+(defun iplayer-download-display-state (process)
+  (let ((id (process-get process 'iplayer-id))
+        (state (process-get process 'iplayer-state))
+        (progress (process-get process 'iplayer-progress)))
+    (with-current-buffer (get-buffer-create "*iplayer-progress*")
+      (goto-char (point-min))
+      (let ((found (re-search-forward (format "^%s:" id) nil 'end)))
+        (unless found
+          (unless (= (point) (progn (forward-line 0) (point)))
+            (goto-char (point-max))
+            (newline)))
+        (forward-line 0)
+        (let ((beg (point)))
+          (end-of-line)
+          (delete-region beg (point)))
+        (insert (format "%s: %s %s%%" id state progress))))))
+
+(defun iplayer-download-process-filter (process string)
+  (catch 'no-progress
+    (cond
+     ((string-match "^Starting download" string)
+      (process-put process 'iplayer-state 'downloading)
+      (process-put process 'iplayer-progress 0.0))
+     ((and (eql (process-get process 'iplayer-state) 'downloading)
+           (string-match "(\\([0-9]\\{1,3\\}.[0-9]\\)%)$" string))
+      (process-put process 'iplayer-progress (string-to-number (match-string 1 string))))
+     ((string-match "Started writing to temp file" string)
+      (process-put process 'iplayer-state 'transcoding)
+      (process-put process 'iplayer-progress 0.0))
+     ((string-match " Progress: =*>?\\([0-9]\\{1,3\\}\\)%-*|" string)
+      (let ((idx (match-beginning 0)) (data (match-data)))
+        (while (string-match " Progress: =*>?\\([0-9]\\{1,3\\}\\)%-*|" string (match-end 0))
+          (setq idx (match-beginning 0))
+          (setq data (match-data)))
+        (set-match-data data)
+        (process-put process 'iplayer-progress (string-to-number (match-string 1 string)))))
+     (t (with-current-buffer (process-buffer process)
+          (insert string))
+        (throw 'no-progress nil)))
+    (iplayer-download-display-state process)))
+
+(defun iplayer-download-process-sentinel (process string)
+  (cond
+   ((string-match "^finished" string)
+    ;; KLUDGE: get-iplayer installs signal handlers and exit with a 0
+    ;; exit code from them.  That means we can't use the sentinel to
+    ;; distinguish between being killed and exiting with success, so
+    ;; we hack around the problem.
+    (if (= (process-get process 'iplayer-progress) 100)
+        (process-put process 'iplayer-state 'finished)
+      (process-put process 'iplayer-state 'failed)))
+   ((string-match "^exited abnormally" string)
+    (process-put process 'iplayer-state 'failed)))
+  (iplayer-download-display-state process))
 
 (defun iplayer-download ()
   (interactive)
   (let ((id (get-text-property (point) 'iplayer-id)))
     (if id
-        (let ((default-directory "~/iPlayer/"))
+        (let ((default-directory iplayer-download-directory))
           ;; should probably use a process filter instead to give us a
           ;; progress bar
           (message "downloading id %s" id)
-          (start-process "get-iplayer" " *get-iplayer*" "get-iplayer" "--get" (format "%s" id)))
+          (let ((process
+                 (start-process "get-iplayer" " *get-iplayer*" "get-iplayer" "--modes=best" "--get" (format "%s" id))))
+            (process-put process 'iplayer-id id)
+            (process-put process 'iplayer-state 'connecting)
+            (process-put process 'iplayer-progress 0.0)
+            (set-process-filter process 'iplayer-download-process-filter)
+            (set-process-sentinel process 'iplayer-download-process-sentinel)
+            (display-buffer (get-buffer-create "*iplayer-progress*"))
+            (iplayer-download-display-state process)))
       (message "no id at point"))))
 
 (defun iplayer-previous ()
@@ -213,29 +320,36 @@ The presets are defined in the variable `iplayer-presets'."
 
 (defconst iplayer-mode-map
   (let ((map (make-sparse-keymap)))
-    (define-key map (kbd "0") 'iplayer)
+    (define-key map (kbd "0") 'iplayer-show-all)
     (let ((presets "123456789!\"£$%^&*()"))
       (dotimes (i (length presets))
         (define-key map (read-kbd-macro (substring presets i (1+ i)))
           'iplayer-preset)))
     (define-key map (kbd "RET") 'iplayer-download)
+    (define-key map (kbd "g") 'iplayer-refresh)
     (define-key map (kbd "j") 'iplayer-next)
     (define-key map (kbd "k") 'iplayer-previous)
+    (define-key map (kbd "n") 'iplayer-next)
+    (define-key map (kbd "p") 'iplayer-previous)
     map
     ))
 
-(defun iplayer-mode ()
+(define-derived-mode iplayer-mode special-mode "iPlayer"
   "A major mode for the BBC's iPlayer.
-\\{iplayer-mode-map}"
+\\{iplayer-mode-map}")
+
+(define-iplayer-command iplayer-show-all (&optional keys)
+  "Show all iPlayer entries."
   (interactive)
-  (use-local-map iplayer-mode-map)
-  (setq major-mode 'iplayer-mode mode-name "iPlayer"))
+  (setq iplayer-current-channel nil)
+  (display-iplayer-tree (get-iplayer-tree)))
 
 (define-iplayer-command iplayer (&optional keys)
   "Start the emacs iPlayer interface."
   (interactive)
-  (setq mode-line-process nil)
-  (display-iplayer-tree (get-iplayer-tree)))
+  (if iplayer-startup-channel
+      (iplayer-channel iplayer-startup-channel)
+    (iplayer-show-all)))
 
 ;;;###autoload
 (autoload 'iplayer "iplayer" "Start the emacs iPlayer interface." t)