Watch progress websites exist for anime and film. They work great. Socialization is great.

But they don’t record the exact time at which I finished each episode. I find such statistics amusing to dig through in some kind of year-end review. I also found it tremendously helpful to know which episodes were most recently watched, and in what order. Helps with recollecting the context of each show, especially when chasing more than a couple of shows at the same time. Depending on your judgment, using Letterboxd and MyAnimeList may also constitute giving private information to 3rd parties.

So I ended with an organization system that’s, effectively, top layer of bullet points for the show, and inner layer for the episodes. Something like:

* STRT SHIROBAKO
** [2025-04-01 Tue 11:11] E01
** [2025-04-16 Wed 22:22] E02
* TODO Do It Yourself!!
* DONE ゆるキャンプ
** [2025-03-20 Thu 20:21] E10
** [2025-03-21 Fri 20:59] E11
** [2025-04-14 Mon 21:36] E12
* STRT mono
** [2025-04-13 Sun 16:56] E01

I want to be able to run a command, and get it as a linear timeline:

* [2025-03-20 Thu 20:21] ゆるキャンプ E10
* [2025-03-21 Fri 20:59] ゆるキャンプ E11
* [2025-04-01 Tue 11:11] SHIROBAKO E01
* [2025-04-13 Sun 16:56] mono E01
* [2025-04-14 Mon 21:36] ゆるキャンプ E12
* [2025-04-16 Wed 22:22] SHIROBAKO E02

Which turns out to be a pretty easy simple regex-based forward parsing algorithm. I thought about using org-element-map, but it seemed like a bit of an overkill?

To use, I do g g M-x org-category->timeline (evidently, I use evil-mode), bringing me to a new buffer *Category Timeline* filled out appropriately.

(defun org-category->timeline ()
  "Collect category-timestamp tree into a single, chronologically
ordered timestamp list. Starting at point."
  (interactive)
  (let ((pt (point))
        (curr-category nil)
        (curr-pt (search-forward "\n* " nil t))
        (items '()))
    ;; Collect items
    (while curr-pt
      (goto-char curr-pt)
      ;; Skip org-todo marker
      (forward-word) (forward-char)
      (setq curr-category (buffer-substring-no-properties (point) (line-end-position))
            curr-pt (save-excursion (search-forward "\n* " nil t)))
      (while-let ((i (re-search-forward (rx "\n** ["
                                            (group (* (not "]")))
                                            "] ")
                                        curr-pt t)))
        (let ((time (match-string-no-properties 1))
              (comment (buffer-substring-no-properties (point) (line-end-position))))
          (push (list (encode-time (org-parse-time-string time))
                      time curr-category comment)
                items))))
    ;; Sort by time
    (sort items (lambda (a b) (time-less-p (car a) (car b))))
    ;; Restore to starting point in the source buffer
    (goto-char pt)
    ;; Print items into a new buffer
    (switch-to-buffer "*Category Timeline*")
    (erase-buffer)
    (dolist (i items)
      (let ((time (cadr i))
            (category (caddr i))
            (comment (cadddr i)))
        (insert "* [" time "] " category " " comment ?\n)))))