Writing a plugin¶
Most of Podlibre is plugins. Importing a feed, reducing noise, transcribing, writing show notes, publishing — each one is a folder with a manifest and some Python, using the same API you are about to use.
This page takes you from nothing to a plugin that appears in the workflow and does something. It should take an afternoon. If it takes longer, that is a bug in the architecture and we want to hear about it.
Before you start¶
You need Podlibre installed, Python 3.12 or newer, and about twenty minutes. You do not need to know Qt, threads, SQL, or anything about how Podlibre draws its window. Plugins cannot draw. They describe what they want on screen and Podlibre builds it.
1. Start from the template¶
Open podlibre-plugin-template and press
Use this template. You get your own repository containing a working plugin: a manifest, one
command, one screen, a translation catalogue and a test.
my-plugin/
├── plugin.toml the manifest: what you provide, where it appears, what you need
├── plugin.py the code
├── views/
│ └── main.toml what your screen looks like
├── locales/
│ ├── en.po your strings
│ └── fr.po
├── tests/
│ └── test_plugin.py
├── AGENT.md for AI assistants: the API, the manifest schema, the house rules
├── CLAUDE.md the same, for Claude Code
└── README.md
Clone it, then point Podlibre at it:
Podlibre loads your plugin from that folder and reloads it when you save. There is no build step.
2. Say what you are¶
The manifest is the whole of your plugin's relationship with Podlibre. Nothing is discovered by scanning your code.
[plugin]
id = "mood_tagger" # unique; also the name of your folder
name = "Mood Tagger"
version = "0.1.0"
api = "1" # the plugin API you were written against
[permissions]
files = ["episode"] # you may read and write inside the open episode's folder
# no network, no model, no transcription: you did not ask, so you cannot
Ask for the least you need. The user sees this list before installing you, and again whenever you reach for something new.
3. Add a step to the workflow¶
An activity is one thing a podcaster does. Declaring one puts you in the workflow, which is where people look for work to do — not in a menu they have to remember.
[[activities]]
id = "tag_mood"
scope = "episode" # application | show | episode
phase = "enrich" # which group of the workflow you belong to
Your title and hint come from your own translation catalogue, under activity.tag_mood.title
and activity.tag_mood.hint. Podlibre never needs your strings in its own catalogue.
4. Say where you appear¶
[[surfaces]]
kind = "screen" # a full page, opened from your workflow step
activity = "tag_mood"
view = "views/main.toml"
And the view, which is a description rather than a drawing:
[[widgets]]
kind = "heading"
text = "@mood.heading" # @ means "look this up in my catalogue"
[[widgets]]
kind = "choice"
id = "mood"
label = "@mood.label"
options = ["@mood.calm", "@mood.urgent", "@mood.funny"]
[[widgets]]
kind = "button"
label = "@mood.save"
command = "mood_tagger.save" # calls your function, with the form's values
variant = "primary"
The vocabulary of widgets is listed in Surfaces. It covers forms, lists, progress and text. It does not cover arbitrary drawing — that is the trade for never being able to freeze the application.
5. Write the code¶
from podlibre import plugin
@plugin.command("mood_tagger.save")
def save(ctx, mood):
ctx.episode.set_field("episode_mood", mood)
ctx.notify("@mood.saved")
ctx is everything you are allowed to touch. It carries the open show and episode, the
capabilities your manifest asked for, a way to start a task, and a way to say something to the
user. It carries nothing you did not ask for: if you did not request network, there is no
ctx.net, and the mistake shows up the first time you run instead of in a user's bug report.
6. Take your time, visibly¶
Anything that might take longer than a moment is a task. A task reports progress, can be stopped by the user, and appears in the Tasks screen with everything else.
@plugin.command("mood_tagger.tag_everything")
def tag_everything(ctx):
episodes = ctx.show.episodes()
with ctx.task("@mood.tagging", total=len(episodes)) as t:
for n, episode in enumerate(episodes, start=1):
if t.cancelled:
return # the user pressed Stop; leave cleanly
episode.set_field("episode_mood", guess(episode))
t.step(n)
Never loop without checking t.cancelled. It is the only thing standing between a user and a
process they cannot get rid of.
Writing several fields at once¶
Each set_field is a request to Podlibre and a transaction in the show file. Writing a dozen
fields on an episode that way is a dozen of each, and importing a back catalogue turns that into
thousands. When you know the fields together, send them together:
ctx.episode.set_fields(episode_id=episode["id"], values={
"summary": episode["summary"],
"pub_date": episode["pub_date"],
"source_guid": episode["guid"],
})
One round trip, one transaction, and either all of them land or none do. ctx.show.set_fields
does the same for the show's own metadata. Use set_field for a single value a user just
changed; use set_fields whenever you are filling something in.
7. Add a field others can use¶
A field you declare becomes part of the model. Mark it shared and any other plugin can read it
— an exporter can put it in the RSS feed without having heard of you.
[[fields]]
key = "episode_mood"
on = "episode"
type = "choice"
values = ["calm", "urgent", "funny"]
shared = true
rss = "podcast:mood" # where it goes on the way out, if anywhere
8. Translate yourself¶
Strings written as @some.key in a manifest or a view are looked up in your catalogue. In code,
use ctx.t("some.key"). Your plugin is translated independently of Podlibre, so a translator can
help you without touching the application.
9. Test without a window¶
Your plugin is ordinary Python with no toolkit in it, so it tests like ordinary Python.
from podlibre.testing import fake_context
def test_it_saves_the_mood():
ctx = fake_context(episode={"title": "Pilot"})
save(ctx, mood="calm")
assert ctx.episode.field("episode_mood") == "calm"
10. Share it¶
Push your repository and tell people its address. Podlibre installs a plugin from a folder, an archive, or a repository URL, and shows the user what you asked for before anything runs.
Working with an AI assistant¶
The template's AGENT.md and CLAUDE.md describe the plugin API, the manifest schema and the
conventions this page teaches. An assistant pointed at a fresh clone has what it needs to write
a plugin with you rather than guess at the framework. The usual advice applies: read what it
writes, and check that the manifest asks only for what the plugin actually uses.
Where to look next¶
- Manifest reference — every key, with its meaning
- Surfaces — the widget vocabulary, and which surface goes where
- Permissions — what each capability grants, and what it does not
- The show file — what you are reading and writing