Making org-mode movie diary

2023/03/05

End result is here: Movies

Putting it in org-mode

Small demo of my flow:

File organisation:

#+STARTUP: logdrawer
#+TODO: TODO(t) NEXT(n) | DONE(d!) DROPPED(c@)

* DONE La Jetée (1962) :romance:scifi:
:PROPERTIES:
:rating: 4
:imdb: tt0056119
:END:
:LOGBOOK:
- State "DONE"       from              [2022-07-11 Mon]
:END:
* Watch List [0/1]
** TODO Big Buck Bunny (2008) :animation:short:comedy:
:PROPERTIES:
:imdb:     tt1254207
:END:

And here is a minor mode to automate inserting new entries.

(define-minor-mode org-movies-mode
  "Helper functions for movies.org"
  :keymap (list
           (cons (kbd "C-c C-i") #'org-movies-template)
           (cons (kbd "C-c C-o") #'org-movies-open-letterboxd)))

(defun org-movies-open-letterboxd ()
  "Open letterboxd for current entry."
  (interactive nil 'org-movies-mode)
  (browse-url (format "https://letterboxd.com/imdb/%s"
                      (cdar (org-entry-properties nil "imdb")))))

(defun org-movies-template ()
  "Convert imdb url at point to a diary entry."
  (interactive nil 'org-movies-mode)
  (let* ((ctx (org-element-context))
         (link (if (eq (car ctx) 'link)
                   (plist-get (cadr ctx) :raw-link)
                 (error "Not an org link")))
         (imdb (or (nth 1 (s-match
                           (rx "imdb.com/title/" (group "tt" (+ digit)))
                           link))
                   (error "Not an imdb link")))
         (apikey (auth-source-pass-get 'secret "Sites/omdbapi.com"))
         (movie (with-current-buffer
                    (url-retrieve-synchronously
                     (concat "http://omdbapi.com/?apikey=" apikey "&i=" imdb))
                  (goto-char url-http-end-of-headers)
                  (json-read-from-string
                   (decode-coding-string
                    (buffer-substring (point) (point-max)) 'utf-8))))
         (genres-str (alist-get "Genre" movie nil nil #'string=))
         (genres (-map #'downcase (string-split genres-str ", ")))
         (title (alist-get "Title" movie nil nil #'string=))
         (year (alist-get "Year" movie nil nil #'string=)))

    (pcase (org-element-type (org-element-at-point))
      ('headline (ignore))
      (_ (beginning-of-line)
         (kill-line)
         (org-insert-heading)))

    (org-edit-headline (concat title " (" year ")"))
    (org-set-tags genres)
    (org-todo "TODO")
    (org-set-property "imdb" imdb)))

(org-movies-mode 1)
Code Snippet 1: Minor mode with useful helpers

Letterboxd export

I’ve fallen into this trap again… I’ve relied on a 3rd party website to manage my data. Yes, it looks good and it’s UX friendly, it’s easy to fall prey. This time it was Letterboxd. I imported my TheMovieDB watchlist there and used it as a film diary for a couple of years. One day I finally found some time to sort my movie collection with tinyMediaManager and the icing on the cake was to add my ratings to the NFO files. To my disappointment, Letterboxd doesn’t include the imdb id in the export files, so I had to write a script to fetch all their short urls one by one and extract the imdb ids from the html.

import re
import requests
import fileinput

# takes file with https://boxd.it/71O short links
for row in fileinput.input():
    row = row.strip()
    d = requests.get(row)
    imdb = re.findall('(?<=imdb.com/title/)tt[^/]+', d.text)[0]
    print(row, imdb, sep=',', flush=True)

Importing ratings in NFO files

Then another script to put the ratings in the NFO files.

import re
import sys
import csv
import glob

r = csv.reader(open("/home/sarg/rated.csv"))
rates = {}

for n in glob.glob('/media/sarg/2TB/Movies/**/**/*.nfo'):
    imdb = re.findall('(?<=<id>)tt\d+', open(n).read())[0]
    if imdb in rates:
        rates.pop(imdb)
        d = re.sub('<userrating>0.0', '<userrating>' + str(rates[imdb]), d)
        f = open(n + '.new', 'w')
        f.write(d)
        f.close()

print("High rated movies I don't have:")
for imdb in rates:
    if rates[imdb]>7:
        print(f"https://imdb.com/title/{imdb}")