Building a plugin, “Letterboxd for Obsidian”

Nick Felker
5 min readMay 17, 2024

--

I’ve been getting into Obsidian lately. I like using the Daily Notes feature to keep track of what I’m doing every day. I’ve been trying to use Obsidian to consolidate more of my activities into a single place.

Obsidian itself is a flexible but simple program. Its real benefit comes from the many different plugins, created by the community, to add more functionality like Dataview.

I’ve been trying out a few different plugins, to try consolidating more data into one place. But although I’ve been rating movies for a few years on Letterboxd, I couldn’t find any plugin that would sync my Letterboxd ratings to Obsidian. I decided to create one.

Letterboxd API

I couldn’t get access to the Letterboxd API. It’s currently in a private beta.

After a few days without getting a response or an API key, I decided to try a different tact. Because although the API wasn’t accessible, my Letterboxd diary has a public RSS feed that I can use to retrieve account data in a machine-readable way.

Then I can use the fast-xml-parser to read through the movies. A limitation is that only the most recent 50 movies will appear from this feed.

/**
* Represents one item in the Letterboxd RSS feed
*
* @example
* ```
* {
* "title": "Ahsoka, 2023 - ★★★★",
* "link": "https://letterboxd.com/fleker/film/ahsoka/",
* "guid": "letterboxd-review-568742403",
* "pubDate": "Thu, 4 Apr 2024 17:28:09 +1300",
* "letterboxd:watchedDate": "2024-04-04",
* "letterboxd:rewatch": "No",
* "letterboxd:filmTitle": "Ahsoka",
* "letterboxd:filmYear": 2023,
* "letterboxd:memberRating": 4,
* "tmdb:tvId": 114461,
* "description": "<p><img src=\"https://a.ltrbxd.com/resized/film-poster/1/0/5/5/4/3/0/1055430-ahsoka-0-600-0-900-crop.jpg?v=b8ec715c15\"/></p> <p>...</p> ",
* "dc:creator": "fleker"
* },
* ```
*/
interface RSSEntry {
title: string
link: string
guid: string
pubDate: string
'letterboxd:watchedDate': string
'letterboxd:rewatch': string
'letterboxd:filmTitle': string
'letterboxd:filmYear': number
'letterboxd:memberRating': number
'tmdb:tvId': number
description: string
'dc:creator': string
}

You can see from the example above the various kinds of data that the feed provides. This actually has everything I need. There's a link to my review, the number of stars, and even a watch date in the same date format that Obsidian uses.

Building an Obsidian Plugin

An Obsidian Plugin is a Node.js project with an extra Obsidian library and a variety of platform hooks.

To select and save the Letterboxd account name, I create a simple settings page that just takes in a single input.

Once I have that, I can make a query to https://letterboxd.com/${this.settings.username}/rss/ and get the data back. When I first tried doing that, using the generic fetch approach, I ran into CORS errors. I tried to build a reverse-proxy, but actually Obsidian includes a requestUrl function that handles that across operating systems.

requestUrl(`https://letterboxd.com/${this.settings.username}/rss/`)
.then(res => res.text)
.then(async res => {
const parser = new XMLParser();
let jObj = parser.parse(res);
...
}

With this data, I want to write to a particular file and save it. Obsidian has a number of File I/O functions to help manage file locks and data transactions.

requestUrl(`https://letterboxd.com/${this.settings.username}/rss/`)
.then(res => res.text)
.then(async res => {
const parser = new XMLParser();
let jObj = parser.parse(res);
const filename = normalizePath('/Letterboxd Diary.md')
const diaryMdArr = (jObj.rss.channel.item as RSSEntry[])
.sort((a, b) => a.pubDate.localeCompare(b.pubDate)) // Sort by date
.map((item: RSSEntry) => {
return `- Gave [${item['letterboxd:memberRating']} stars to ${item['letterboxd:filmTitle']}](${item['link']}) on [[${item['letterboxd:watchedDate']}]]`
})
const diaryFile = this.app.vault.getFileByPath(filename)
if (diaryFile === null) {
this.app.vault.create(filename, `${diaryMdArr.join('\n')}`)
} else {
this.app.vault.process(diaryFile, (data) => {
const diaryContentsArr = data.split('\n')
const diaryContentsSet = new Set(diaryContentsArr)
diaryMdArr.forEach((entry: string) => diaryContentsSet.add(entry))
return `${[...diaryContentsSet].join('\n')}`
})
}
})

As I can only handle the last 50 entries, I use a set structure to merge various times together.

After running it, my file is created and all my films are listed.

As I wanted, my daily notes now include backlinks to my Letterboxd Diary to create a more thorough graph.

To trigger this action, I added a command to the command palette.

Launching an Obsidian Plugin

Writing the code was fairly easy, in the grand scheme of things. In order to submit it to the Community Plugins, you need to release the code on GitHub through your project’s Releases page.

All plugins need to be open-source on GitHub. That's how Obsidian handles version control and updates.

To make things easier, there is an example GitHub Action to bundle everything and create a preliminary release.

Obsidian has a GitHub repo for its community plugins, a long JSON list where developers can send a pull request to get added.

Your code is read and goes through a series of automated tests. For instance, they caught my reverse proxy and told me to use the requestUrl instead. There are also a number of checks to ensure you adhere to the plugin style guide.

After a few rounds of automated checks, there was a human review step. They gave some more comments related to the app style (mainly just strings) and eventually gave the ‘OK’.

I think the process is quite good. The developers have done a good job of providing resources and process to ensure plugins do hit a high bar of quality. The samples and documentation also make it easy to get started.

Conclusion

The plugin is available to download through the Obsidian Community Plugins page.

As with all Obsidian plugins, the project is open source on GitHub.

--

--

Nick Felker

Social Media Expert -- Rowan University 2017 -- IoT & Assistant @ Google