obsidian-vault-add
Add a new resource (book, podcast, TV show, movie, person, country, or city) to the vault: create the note, download a photo/cover, update today's daily note (media only), and re-index.
Argument parsing
Arguments: <type> <title> [extra info]
type(required):book,podcast,show(ortv),movie,person,country(orland),city(orstadt)title(required): the resource titleextra info(optional): author, host, episode number, season/episode, country name (for cities), etc.
Examples:
/obsidian-vault-add book "Atlas Shrugged"/obsidian-vault-add podcast "Aethervox Ehrenfeld"/obsidian-vault-add show "Silicon Valley" 1.1/obsidian-vault-add movie "Dune"/obsidian-vault-add person "Thomas Sowell"/obsidian-vault-add country "Japan"/obsidian-vault-add city "Kyoto"
If type or title is missing, ask the user before proceeding.
Step-by-step workflow
1. Web search for metadata
Search for the title to gather:
Book: title, subtitle, authors, published year, genres, pages, ISBN, short description (German preferred) Podcast: full name, host(s), language, genres, start year, description (German preferred) TV show: full name, actors, genres, network, seasons, episodes, IMDb rating, description (German preferred) Movie: full name, director(s), actors, genres, year, IMDb rating, description (German preferred) Person: full name, aliases, nationality, birthday (and death if applicable), domain/occupation, Wikipedia URL, notable works (books/articles/videos), short German biography (2–4 sentences) Country: official name, capital, continent, official language(s), currency, aliases (native name, abbreviations), short description (German preferred) City: full name, country, region/state, language(s), aliases (native name), short description (German preferred)
2. Derive filename
Colons are not allowed in filenames. If the title contains a colon (e.g. "Book: Subtitle"):
- Replace the colon (and any surrounding spaces) with
-→ filename becomesBook - Subtitle.md - Add the original title with the colon as the first alias in the frontmatter
aliaseslist
3. Check if note already exists
obsidian read file="<derived-filename>" 2>/dev/null | grep -Ev "^(20[0-9]{2}-|Your Obsidian)"
If the note already exists, inform the user and stop (unless they want to update it).
4. Create note from template
| Type | Path | Template |
|---|---|---|
| book | 03 Resources/Bücher/<title>.md |
Book Template |
| podcast | 03 Resources/Podcasts/<title>.md |
Podcast Template |
| show | 03 Resources/Serien/<title>.md |
TV Show Template |
| movie | 03 Resources/Filme/<title>.md |
Movie Template |
| person | 03 Resources/Personen/<title>.md |
Person Template |
| country | 03 Resources/Länder/<title>.md |
Country |
| city | 03 Resources/Städte/<title>.md |
City |
obsidian create path="<path>" template="<Template>" 2>/dev/null | grep -Ev "^(20[0-9]{2}-|Your Obsidian)"
5. Fill in frontmatter
Edit the note file directly (Read then Edit) — do NOT use obsidian property:set for list fields as it does not reliably handle arrays. Set all fields in one Edit call by replacing the entire frontmatter block.
IMPORTANT — preserve existing data: When updating an existing note, never discard data already present. Merge new metadata with what's there:
- Keep all existing
aliasesand append any new ones not already listed - Keep all existing
tags(only removeBOATif present) - Keep any fields already filled in; only add/update empty fields
- Keep all existing body content (sections, lists, notes)
Book frontmatter fields: aliases, tags: [Buch], photo, urls, published (YYYY-MM-DD), authors (list), genres (list), rating, pages
Podcast frontmatter fields: aliases, tags: [Podcast], photo, urls, hosts (list), guests, genres (list), language, rating, status, started, ended, episodes
TV show frontmatter fields: aliases, tags: [Serie], photo, urls, actors (list), genres (list), rating, imdb_rating, network, seasons, episodes
Movie frontmatter fields: aliases, tags: [Film], photo, urls, directors (list), actors (list), genres (list), rating, imdb_rating, year
Country frontmatter fields: aliases, tags: [Land], photo, urls, capital, continent, language (list), currency, visited, rating, cities (list of wikilinks to known city notes)
Person frontmatter fields: aliases (other names, pen names, maiden names), tags (Mann/Frau, nationality country, domain tags like Schriftsteller/Philosoph/etc.), photo, urls, related, birthday (YYYY-MM-DD), death (YYYY-MM-DD or empty), location, quotes (list of wikilinks), books (list of wikilinks to vault book notes), articles, podcasts, videos
City frontmatter fields: aliases, tags: [Stadt], photo, urls, country (wikilink), region, language (list), visited, rating
YAML quoting rule: Any frontmatter value that contains a colon must be wrapped in double quotes — including titles, descriptions, aliases, and all string fields. Example: title: "Book: Subtitle", aliases: ["Book: Subtitle"].
Remove the BOAT tag from tags — notes with links are not BOAT.
6. Add description to body
After the frontmatter, add a short German-language description paragraph (2–4 sentences). Keep any template section headers (## Zusammenfassung, ## Kernaussagen, ## Lieblingsfolgen, etc.).
Person body structure: short German bio paragraph, then ### Fakten (bullet list of key facts), then ### Ideen (empty, for future notes).
The image embed ![[filename.webp]] will be added after cover download (step 6) — do not add it manually.
7. Download cover art
Use inline Python (not the download scripts, which require manual list editing). Use Pillow if available for WebP output, otherwise JPEG.
Book — Open Library by title search:
import json, urllib.request, urllib.parse, io, os
query = "<title>"
url = f"https://openlibrary.org/search.json?title={urllib.parse.quote(query)}&limit=3"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
docs = data.get("docs", [])
cover_id = next((d["cover_i"] for d in docs if d.get("cover_i")), None)
if cover_id:
img_url = f"https://covers.openlibrary.org/b/id/{cover_id}-L.jpg"
# download and save as WebP using Pillow
Output path: 99 Meta/assets/books/<title> (<first author>).webp
Podcast — iTunes Search API:
import json, urllib.request, urllib.parse
query = "<podcast name>"
params = urllib.parse.urlencode({"term": query, "entity": "podcast", "limit": 5})
url = f"https://itunes.apple.com/search?{params}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
artwork = data["results"][0].get("artworkUrl600") if data["results"] else None
# download and save as WebP using Pillow
Output path: 99 Meta/assets/podcasts/<title>.webp
TV show — TVMaze API:
import json, urllib.request, urllib.parse
query = "<show title>"
url = f"https://api.tvmaze.com/singlesearch/shows?q={urllib.parse.quote(query)}&embed=images"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("image", {}).get("original") or data.get("image", {}).get("medium")
# download and save as WebP using Pillow
Output path: 99 Meta/assets/series/<title>.webp
Movie — Wikipedia REST API (search for poster):
import json, urllib.request, urllib.parse
title = "<movie title>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(title)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download and save as WebP using Pillow
Output path: 99 Meta/assets/movies/<title>.webp
Country — Wikipedia REST API (flag or representative image):
import json, urllib.request, urllib.parse
title = "<country name>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(title)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download and save as WebP using Pillow
Output path: 99 Meta/assets/countries/<title>.webp
Person — Wikipedia REST API, then resize to max 800px on longest side:
import json, urllib.request, urllib.parse
name = "<person name>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(name)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download, resize to max 800px longest side, save as WebP using Pillow
Output path: 99 Meta/assets/people/<title>.webp
City — Wikipedia REST API:
import json, urllib.request, urllib.parse
title = "<city name>"
search_url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{urllib.parse.quote(title)}"
req = urllib.request.Request(search_url, headers={"User-Agent": "Mozilla/5.0 VaultBot/1.0"})
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
image_url = data.get("originalimage", {}).get("source") or data.get("thumbnail", {}).get("source")
# download and save as WebP using Pillow
Output path: 99 Meta/assets/cities/<title>.webp
After successful download:
- Set
photo: <filename>in frontmatter (Edit the file) - Add
![[<filename>]]to the note body using Edit (insert before the description paragraph) — do NOT useobsidian prepend, as it escapes the!and produces\![[...]]
If cover download fails, note it to the user and continue without a photo.
Also add the new entry to the appropriate download script so future re-runs include it:
| Type | Script |
|---|---|
| book | 99 Meta/scripts/download-book-covers.py |
| podcast | 99 Meta/scripts/download-podcast-covers.py |
| show | 99 Meta/scripts/download-series-covers.py |
| movie | 99 Meta/scripts/download-movie-covers.py |
Countries, cities, and persons have no dedicated download script — skip this step for those types.
8. Update today's daily note
Skip this step for person, country, and city — they are reference notes, not media being consumed.
For media types, append to today's daily note using the correct emoji and wikilink format:
| Type | Format |
|---|---|
| book | 📚 [[<title>]] angefangen zu lesen (0 %) |
| podcast | 🎙️ [[<title>]]: <episode/topic if known> |
| show | 📺 [[<title>]] <season>.<episode> |
| movie | 🎬 [[<title>]] |
obsidian daily:append content="<entry>" 2>/dev/null | grep -Ev "^(20[0-9]{2}-|Your Obsidian)"
If the user provided season/episode or episode title in the arguments, use it; otherwise use a sensible placeholder or ask.
9. Re-index qmd
qmd update && qmd embed
Run this after the note is created and finalized so it's immediately searchable.
10. Report to user
Summarize what was done:
- Note path created
- Metadata filled (list key fields)
- Cover art status (saved path or skip reason)
- Daily note entry added
- qmd indexed
Error handling
- If note creation fails, check if the path already exists
- If cover download returns a tiny image (< 5 KB), skip it and warn the user
- If
obsidian daily:appendfails, tell the user the entry to add manually - If
qmd embedtakes too long, run it in the background withrun_in_background: true