Automating Tasks with Emacs Lisp

Updates

Automating Tasks with Emacs Lisp

Today we're going to write a lot of Emacs Lisp code to automate a particular monotonous task: editing my YouTube video descriptions!

Why? It's nice to be able to keep all of your video descriptions following a standard format so that they can be automatically updated using code for things like:

  • Adding links to new playlists to every video
  • Updating links to my websites
  • Temporary links to occasional events I want to promote

Here's the flow I have in mind:

  • Download the description of a particular video to a file on disk (in a temporary location)
  • Open the buffer to make any desired edits
  • Use a command to send the changes back to the original video
  • Make it possible to download all video descriptions at once
  • Create a bulk editing action to edit (and verify) changes to all description files
  • Mass-upload all changed description files back to YouTube

We probably won't get through all of this today! I do have a starting point, though. My live-crafter package has some YouTube Data API code we can borrow to kick things off!

Reference

The final code

;; -*- lexical-binding: t; -*-

(require 'simple-httpd)

(defvar video-meta-client-id nil)
(defvar video-meta-api-key nil)
(defvar video-meta-client-secret nil)
(defvar video-meta--access-token-func nil)
(defvar video-meta--auth-redirect-uri "http://localhost:3000/oauth2_callback")
(defvar video-meta--use-bearer-auth nil)
(defvar video-meta--request-url nil)
(defvar video-meta--video-file-path "/tmp/video-meta")

(defun video-meta--get-access-token ()
  (funcall video-meta--access-token-func))

(defun video-meta--build-params (params)
  (string-join (mapcar (lambda (pair)
                         (format "%s=%s" (car pair) (cdr pair)))
                       params)
               "&"))

(defun video-meta--build-uri (path params)
  (let ((param-string (video-meta--build-params params)))
    (concat path
            (if (> (length param-string) 0) "?" "")
            param-string)))

(defun video-meta--http-get (url params)
  (plz 'get (video-meta--build-uri url params)
    :as #'json-read
    :else (lambda (err) (message "ERROR: %S" err))
    :headers `(("Content-Type" . "application/json")
               ("Authorization" . ,(format "Bearer %s" (video-meta--get-access-token))))))

(defun video-meta--http-put (url params body)
  (plz 'put (video-meta--build-uri url params)
    :as #'json-read
    :body (json-encode body)
    :headers `(("Content-Type" . "application/json")
               ("Authorization" . ,(format "Bearer %s" (video-meta--get-access-token))))))

(defun video-meta--build-auth-url (client-id redirect-uri)
  (video-meta--build-uri
   "https://accounts.google.com/o/oauth2/v2/auth"
   `((client_id . ,client-id)
     (response_type . "code")
     (redirect_uri . ,redirect-uri)
     ("scope" . ,(string-join '("https://www.googleapis.com/auth/youtube"
                                "https://www.googleapis.com/auth/youtube.force-ssl"
                                "https://www.googleapis.com/auth/youtube.readonly")
                              " ")))))

(defun video-meta--extract-token (json-body)
  (cdr (assoc 'access_token json-body)))

(defun video-meta--request-token (auth-code)
  (let* ((params `((client_id . ,video-meta-client-id)
                   (client_secret . ,video-meta-client-secret)
                   (code . ,auth-code)
                   (grant_type . "authorization_code")
                   (redirect_uri . ,video-meta--auth-redirect-uri)))
         (url-request-method "POST")
         (url-request-extra-headers
          '(("Content-Type" . "application/x-www-form-urlencoded")))
         (url-request-data (video-meta--build-params params)))
    (with-temp-buffer
      (url-retrieve
       "https://oauth2.googleapis.com/token"
       (lambda (status)
         (message "In callback!")
         (re-search-forward "^\n")
         (let ((token-details (json-read)))
           (setq video-meta--access-token-func
                 #'(lambda ()
                     (video-meta--extract-token token-details)))))
       nil
       t))))

(defun video-meta-authenticate ()
  (interactive)
  (let ((httpd-port 3000)
        (auth-url (video-meta--build-auth-url video-meta-client-id
                                                "http://localhost:3000/oauth2_callback")))
    (httpd-start)
    (browse-url auth-url)))

(defservlet* oauth2_callback text/plain (code error)
  (if error
      (message "Error during YouTube authentication: %s" error)
    (progn
      (message "Requesting token!")
      (video-meta--request-token code)))
  (httpd-stop))

(defun video-meta-get-subscriber-count ()
  (let ((params `((part . "statistics")
                  (mine . "true")
                  (key . ,video-meta-api-key))))
    (video-meta--http-get
     "https://youtube.googleapis.com/youtube/v3/channels"
     params
     (lambda (response)
       (message "Got response! %S" response)))))

(defun video-meta--extract-video-list (response)
  (cdr (assoc 'items response)))

(defun video-meta--extract-video-details (video)
  (let ((snippet (cdr (assoc 'snippet video))))
    `((id . ,(cdr (assoc 'id video)))
      (category-id . ,(cdr (assoc 'categoryId snippet)))
      (title . ,(cdr (assoc 'title snippet)))
      (description . ,(cdr (assoc 'description snippet))))))

(defun video-meta-get-video-details (video-id)
  (let ((response (video-meta--http-get
                   "https://youtube.googleapis.com/youtube/v3/videos"
                   `((part . "snippet")
                     (id . ,video-id)
                     (key . ,video-meta-api-key)))))

    (video-meta--extract-video-details
      (aref (video-meta--extract-video-list response)
            0))))

(defun video-meta--download-video-description (video-id)
  (let* ((video (video-meta-get-video-details video-id))
         (description (cdr (assoc 'description video))))
    ;; Save the file to the path using the video id as filename
    (with-temp-file (expand-file-name (format "%s.txt" video-id)
                                      video-meta--video-file-path)
      ;; Try pretty printing with pp and with-output-to-string
      (insert (format "%S" (map-delete video 'description)))
      (insert "\n---\n")
      (insert description))))

;; (video-meta--download-video-description "za99DwdZEyg")

(defun video-meta--upload-video-description (video-id category-id title description)
  (let ((response (video-meta--http-put
                   "https://youtube.googleapis.com/youtube/v3/videos"
                   `((part . "snippet")
                     (id . ,video-id)
                     (key . ,video-meta-api-key))

                   `((id . ,video-id)
                     (snippet . ((title . ,title)
                                 (categoryId . ,category-id)
                                 (description . ,description)))))))))


;; (let* ((video-id "za99DwdZEyg")
;;        (video (video-meta-get-video-details video-id)))

;;   (video-meta--upload-video-description video-id
;;                                         (cdr (assoc 'category-id video))
;;                                         (cdr (assoc 'title video))
;;                                         (concat "Testing description uploading!  " (cdr (assoc 'description video)))))

;; (video-meta--upload-video-description "za99DwdZEyg" "Automated Org Mode Website Publishing with GitHub or SourceHut")

(provide 'video-meta)

Enjoyed this stream? Explore our hands-on courses for deeper, structured learning on Guile Scheme and more.

Get the System Crafters Newsletter
Updates on open source tools, tutorials, and community projects. We'll also occasionally let you know about new courses and resources.
Name (optional)
Email Address