IICHLIWP
Musicians
Statement Begins
Give it a name, a country, the year they formed, and it will make you a card. Then it does the part I like: it goes through the rest of the vault for every album I ever filed under that artist and builds the discography out of my own notes.
So the card is not really about the musician. It is about everything I have already said about them, gathered into one file without my asking twice.
Nothing leaves the vault. The whole picture is assembled out of notes I had forgotten were connected.
Custom Views Code

<div class="mus-card" data-name="{{file.name}}">
<div class="mus-stage">
<img class="mus-side mus-side-l" alt="" />
<div class="mus-portrait-wrap">
<img class="mus-portrait" src="" alt="" />
<button class="mus-more" type="button">SHOW MORE</button>
</div>
<img class="mus-side mus-side-r" alt="" />
</div>
<div class="mus-head">
<div class="mus-accent"></div>
<h1 class="mus-title" data-raw="{{file.basename}}"></h1>
<p class="mus-sub"></p>
</div>
<blockquote class="mus-bio" hidden></blockquote>
<div class="mus-notes"></div>
</div>
.obsidian-custom-view-editable .obsidian-custom-view-render,
.obsidian-custom-view-render { padding: 0 !important; --line-width: 100%; --max-width: none; }
.mus-card {
--mus-accent: var(--color-accent, #d8893f);
--mus-ink: #ece9e3;
container-type: inline-size;
container-name: mus;
position: relative;
width: 100%;
box-sizing: border-box;
padding: clamp(24px, 4vw, 56px) clamp(20px, 5vw, 80px) clamp(40px, 6vw, 80px);
color: var(--mus-ink);
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro', sans-serif;
background: radial-gradient(135% 95% at 50% 14%, #1b1b20 0%, #111114 55%, #07070a 100%);
overflow: hidden;
}
/* ── coverflow stage ── */
.mus-stage { position: relative; display: flex; align-items: center; justify-content: center; perspective: 1700px; min-height: clamp(260px, 44vh, 500px); margin-bottom: clamp(18px, 3vw, 34px); }
.mus-portrait-wrap { position: relative; z-index: 3; flex: 0 0 auto; }
.mus-portrait {
display: block;
width: clamp(190px, 28vw, 330px);
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 34px 80px rgba(0,0,0,.72), 0 4px 14px rgba(0,0,0,.5);
background: #0c0c0f;
}
.mus-side {
flex: 0 0 auto;
width: clamp(150px, 20vw, 250px);
aspect-ratio: 3 / 4;
object-fit: cover;
border-radius: 8px;
opacity: .92;
filter: brightness(.82) saturate(1);
box-shadow: 0 24px 56px rgba(0,0,0,.62);
z-index: 1;
}
.mus-side-l { transform: rotateY(30deg) scale(.9); margin-right: clamp(16px, 5vw, 80px); transform-origin: right center; }
.mus-side-r { transform: rotateY(-30deg) scale(.9); margin-left: clamp(16px, 5vw, 80px); transform-origin: left center; }
.mus-more {
position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%);
font: 600 11px/1 -apple-system, sans-serif; letter-spacing: .12em; color: var(--mus-ink);
background: rgba(10,10,12,.62); border: 1px solid rgba(255,255,255,.22); backdrop-filter: blur(6px);
padding: 8px 16px; border-radius: 999px; cursor: pointer; transition: background .15s ease;
}
.mus-more:hover { background: rgba(20,20,24,.85); }
/* ── head ── */
.mus-head { max-width: 760px; }
.mus-accent { width: 64px; height: 3px; background: var(--mus-accent); margin-bottom: 16px; border-radius: 2px; }
.mus-title { margin: 0; font-size: clamp(28px, 4vw, 50px); font-weight: 700; letter-spacing: -.02em; line-height: 1.05; }
.mus-sub { margin: 8px 0 0; font-size: clamp(13px, 1.2vw, 16px); color: #b7b3ab; letter-spacing: .03em; text-transform: uppercase; }
/* ── bio ── */
.mus-bio { margin: clamp(22px,3vw,32px) 0 0; max-width: 70ch; border-inline-start: 3px solid var(--mus-accent); padding: 4px 0 4px 18px; color: #d8d4cc; font-size: clamp(15px,1.2vw,18px); line-height: 1.65; font-style: normal; }
/* ── notes (albums table, revealed by SHOW MORE) ── */
.mus-notes { display: none; margin-top: clamp(26px,4vw,40px); }
.mus-card.notes-open .mus-notes { display: block; }
.mus-notes-body { font-size: 15.5px; line-height: 1.7; color: #b7b3ab; }
.mus-notes-body > :first-child { margin-top: 0; }
.mus-notes-body h2 { font-size: 14px; letter-spacing: .06em; text-transform: uppercase; color: #cfcbc3; margin: 0 0 12px; display: flex; align-items: center; gap: 14px; }
.mus-notes-body h2::after { content: ""; flex: 1; height: 1px; background: rgba(255,255,255,.1); }
.mus-notes-body a { color: var(--mus-accent); text-decoration: none; }
/* ── narrow / mobile ── */
@container mus (max-width: 640px) {
.mus-side { display: none; }
.mus-portrait { width: clamp(200px, 60vw, 300px); }
}
@media (max-width: 640px) {
.mus-side { display: none; }
.mus-portrait { width: clamp(200px, 60vw, 300px); }
}
/* ── computed discography table (replaces the .base embed) ── */
.mus-sec-title { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--bu-muted, #9a958c); margin: 0 0 10px; }
.mus-albums { width: 100%; border-collapse: collapse; font-family: var(--bu-font-body); font-size: 14px; }
.mus-albums th {
background: var(--bu-accent, #b81200); color: var(--bu-on-accent, #fff);
font-family: var(--bu-font-label); text-transform: uppercase; letter-spacing: .04em;
font-weight: 700; font-size: 11px; text-align: left; padding: 5px 10px;
}
.mus-albums td { border-bottom: 1px solid var(--bu-border, rgba(255,255,255,.08)); padding: 6px 10px; color: var(--bu-text, #c8c3ba); }
.mus-albums tbody tr:hover { background: color-mix(in srgb, var(--bu-accent, #b81200) 12%, transparent); }
.mus-al-score { color: var(--bu-brass, #ca9a4e); font-variant-numeric: tabular-nums; width: 4em; }
.mus-al-year { width: 5em; color: var(--bu-muted, #9a958c); }
.mus-albums a.internal-link { color: var(--mus-accent, var(--bu-accent, #b81200)); text-decoration: none; }
.mus-albums a.internal-link:hover { text-decoration: underline; }
const _c = this;
(async () => {
try {
const root = _c.querySelector(".mus-card");
if (!root) return;
const wantName = root.dataset.name || "";
let file = app.workspace.getActiveFile();
if ((!file && wantName) || (file && wantName && file.name !== wantName)) {
const byName = app.vault.getFiles().find(f => f.name === wantName);
if (byName) file = byName;
}
if (!file) return;
const fm = (app.metadataCache.getFileCache(file) || {}).frontmatter || {};
const clean = s => String(s == null ? "" : s).replace(/\[\[|\]\]/g, "").trim();
const list = v => Array.isArray(v) ? v : (v == null || v === "" ? [] : [v]);
// --- images: centre portrait + 2 sides (all artist) ---
const imgs = list(fm.artist_images).map(String).filter(Boolean);
const centre = imgs[0] || "";
const pic = root.querySelector(".mus-portrait"); if (pic && centre) pic.src = centre;
const sl = root.querySelector(".mus-side-l"), sr = root.querySelector(".mus-side-r");
if (sl) { const u = imgs[1] || imgs[0]; if (u) sl.src = u; else sl.remove(); }
if (sr) { const u = imgs[2] || imgs[1] || imgs[0]; if (u) sr.src = u; else sr.remove(); }
// --- title / sub ---
const title = root.querySelector(".mus-title");
if (title) title.textContent = (title.getAttribute("data-raw") || "").replace(/^Musician\s*-\s*/, "");
const sub = [clean(fm.genre), clean(fm.style), clean(fm.country), fm.formed ? String(fm.formed) : ""].filter(Boolean);
const subEl = root.querySelector(".mus-sub");
if (subEl) { if (sub.length) subEl.textContent = sub.join(" · "); else subEl.remove(); }
// --- bio ---
const bio = clean(fm.bio), bioEl = root.querySelector(".mus-bio");
if (bioEl) { if (bio) { bioEl.textContent = bio; bioEl.removeAttribute("hidden"); } else bioEl.remove(); }
// --- notes: computed discography table. (.base embeds don't paint rows inside a
// detached custom-view render — so we query + score the albums ourselves.) ---
const more = root.querySelector(".mus-more");
const notesEl = root.querySelector(".mus-notes");
let hasNotes = false;
if (notesEl) {
try {
const num = v => (typeof v === "number" && !isNaN(v)) ? v : 0;
const S = ["singing","sound_quality","album_art","production_quality","replayability","album_cohesiveness","album_tone","lyrics","personal_score"];
const scoreOf = afm => {
const pct = afm.number_of_tracks ? (Array.isArray(afm.favorite_tracks) ? afm.favorite_tracks.length : 0) / afm.number_of_tracks * 10 : 0;
const filled = S.filter(k => num(afm[k])).length + (pct ? 1 : 0);
const sum = S.reduce((a, k) => a + num(afm[k]), 0) + pct;
const bonus = (afm.favorite ? 3 : 0) + (afm.relistened_14d ? 2 : 0) + (afm.title_track_good ? 2 : 0);
return filled >= 6 ? Math.min(100, Math.round(sum / filled * 10) + bonus) : null;
};
const myAliases = list(fm.aliases).map(clean);
const myShort = (file.basename || "").replace(/^Musician\s*-\s*/, "");
const rows = [];
for (const f of app.vault.getMarkdownFiles()) {
const afm = (app.metadataCache.getFileCache(f) || {}).frontmatter; if (!afm) continue;
if (!list(afm.categories).map(clean).includes("Albums")) continue;
const match = list(afm.artist).some(a => {
const mm = String(a).match(/\[\[([^\]|#]+)/);
const tgt = mm ? mm[1].trim() : clean(a);
const dest = app.metadataCache.getFirstLinkpathDest(tgt, f.path);
if (dest && dest.path === file.path) return true;
const disp = clean(a);
return tgt === file.basename || disp === myShort || myAliases.includes(disp);
});
if (!match) continue;
rows.push({
name: f.basename.replace(/^Albums?\s*-\s*/, ""),
path: f.path,
score: scoreOf(afm),
year: afm.year || "",
genre: list(afm.genre).map(clean).join(", ")
});
}
if (rows.length) {
rows.sort((a, b) => (b.score == null ? -1 : b.score) - (a.score == null ? -1 : a.score));
const esc = s => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
const body = rows.map(r =>
`<tr><td class="mus-al-name"><a class="internal-link" data-href="${esc(r.path)}">${esc(r.name)}</a></td>` +
`<td class="mus-al-score">${r.score == null ? "—" : r.score}</td>` +
`<td class="mus-al-year">${esc(r.year)}</td><td class="mus-al-genre">${esc(r.genre)}</td></tr>`
).join("");
notesEl.innerHTML = `<p class="mus-sec-title">Discography</p><table class="mus-albums"><thead><tr><th>Album</th><th>Score</th><th>Year</th><th>Genre</th></tr></thead><tbody>${body}</tbody></table>`;
notesEl.querySelectorAll("a.internal-link").forEach(a => a.addEventListener("click", e => {
e.preventDefault(); app.workspace.openLinkText(a.dataset.href, file.path, e.ctrlKey || e.metaKey);
}));
hasNotes = true;
} else notesEl.remove();
} catch (e) { console.error("[Musicians discography]", e); if (notesEl) notesEl.remove(); }
}
if (more) {
if (!hasNotes) more.remove();
else more.addEventListener("click", () => {
const open = root.classList.toggle("notes-open");
more.textContent = open ? "SHOW LESS" : "SHOW MORE";
});
}
// --- accent matched to the centre image (bg stays neutral) ---
if (centre) {
const toHsl = (r,g,b)=>{r/=255;g/=255;b/=255;const mx=Math.max(r,g,b),mn=Math.min(r,g,b);let h,s,l=(mx+mn)/2;if(mx===mn){h=s=0;}else{const d=mx-mn;s=l>0.5?d/(2-mx-mn):d/(mx+mn);switch(mx){case r:h=(g-b)/d+(g<b?6:0);break;case g:h=(b-r)/d+2;break;default:h=(r-g)/d+4;}h/=6;}return[h*360,s,l];};
const hslToRgb=(h,s,l)=>{h/=360;let r,g,b;if(s===0){r=g=b=l;}else{const hue=(p,q,t)=>{if(t<0)t+=1;if(t>1)t-=1;if(t<1/6)return p+(q-p)*6*t;if(t<1/2)return q;if(t<2/3)return p+(q-p)*(2/3-t)*6;return p;};const q=l<0.5?l*(1+s):l+s-l*s,p=2*l-q;r=hue(p,q,h+1/3);g=hue(p,q,h);b=hue(p,q,h-1/3);}return[Math.round(r*255),Math.round(g*255),Math.round(b*255)];};
const hex=(r,g,b)=>"#"+[r,g,b].map(v=>Math.max(0,Math.min(255,v)).toString(16).padStart(2,"0")).join("");
const sample=img=>{try{const W=44,H=44,cv=document.createElement("canvas");cv.width=W;cv.height=H;const ctx=cv.getContext("2d");ctx.drawImage(img,0,0,W,H);const d=ctx.getImageData(0,0,W,H).data;let wR=0,wG=0,wB=0,ws=0;for(let i=0;i<d.length;i+=4){if(d[i+3]<128)continue;const r=d[i],g=d[i+1],b=d[i+2];const mx=Math.max(r,g,b),mn=Math.min(r,g,b);const sc=mx===0?0:(mx-mn)/mx;const w=0.1+sc*sc;wR+=r*w;wG+=g*w;wB+=b*w;ws+=w;}if(!ws)return;let[h,s,l]=toHsl(wR/ws,wG/ws,wB/ws);if(s<0.12)return;s=Math.min(Math.max(s,0.55),0.95);l=Math.min(Math.max(l,0.52),0.64);const[r,g,b]=hslToRgb(h,s,l);root.style.setProperty("--mus-accent",hex(r,g,b));}catch(e){}};
const probe = new Image(); probe.crossOrigin = "anonymous";
probe.onload = () => sample(probe); probe.src = centre;
}
} catch (e) { console.error("[Custom Views: Musicians]", e); }
})();
Templater Template
<%*
/**
* TheAudioDB → Musician Frontmatter (Obsidian Templater)
* No API key required. Pulls artist images (for the coverflow card), genre,
* style, country, formed year and an English bio. The body embeds the artist's
* albums table. Rendered by the Custom Views "Musicians" view.
*/
const ADB = "https://www.theaudiodb.com/api/v1/json/2/search.php?s=";
const today = tp.date.now("YYYY-MM-DD");
const fileTitle = tp.file.title;
// 1) Ask for artist name -------------------------------------------------
let query = await tp.system.prompt("Enter artist name (leave empty for file name):");
if (!query || !query.trim()) query = fileTitle.replace(/^Musician\s*-\s*/, "");
query = query.trim();
// 2) TheAudioDB lookup ---------------------------------------------------
let artist = null;
try {
const r = await tp.web.request(`${ADB}${encodeURIComponent(query)}`);
artist = (r && r.artists && r.artists[0]) || null;
} catch (e) { /* offline / not found — fall through to a minimal note */ }
const name = (artist && artist.strArtist) || query;
// up to 3 artist images: thumb (centre) + fanart (sides)
let images = [];
if (artist) {
const pool = [artist.strArtistThumb, artist.strArtistFanart, artist.strArtistFanart2, artist.strArtistFanart3, artist.strArtistWideThumb];
for (const u of pool) { if (u && !images.includes(u)) images.push(u); if (images.length === 3) break; }
}
const genre = (artist && artist.strGenre) || "";
const style = (artist && artist.strStyle) || "";
const country = (artist && artist.strCountry) || "";
const formed = (artist && artist.intFormedYear) || "";
let bio = (artist && artist.strBiographyEN) || "";
bio = bio.replace(/\r?\n+/g, " ").replace(/\s+/g, " ").replace(/"/g, '\\"').trim().slice(0, 900);
// 3) Rename to "Musician - Name" ----------------------------------------
const colonFixed = name.replace(/:/g, " - ");
let sanitized = colonFixed.replace(/[\/#%&{}<>*?$!'"@+`|=]/g, "").trim();
if (!sanitized) sanitized = fileTitle;
const newName = sanitized;
if (newName !== fileTitle) await tp.file.rename(newName);
// 4) Build YAML ----------------------------------------------------------
const L = [];
L.push("---");
L.push("connections:");
L.push("categories:");
L.push(' - "[[Musicians]]"');
L.push("type:");
L.push(' - "[[Musicians]]"');
if (images.length) { L.push("artist_images:"); images.forEach(u => L.push(` - ${u}`)); } else L.push("artist_images: []");
L.push(genre ? `genre: ${genre}` : "genre:");
L.push(style ? `style: ${style}` : "style:");
L.push(country ? `country: ${country}` : "country:");
L.push(formed ? `formed: ${formed}` : "formed:");
L.push(`bio: "${bio}"`);
L.push(`created: ${today}`);
// resolve to this "Musician - Name" note (and the Albums.base#Artist table
// fills via list(artist).contains(this)) without changing the filename scheme.
const aliasSet = [];
[name, sanitized].forEach(n => { n = (n || "").trim(); if (n && !aliasSet.includes(n)) aliasSet.push(n); });
L.push("---");
// 5) Body — the artist's albums table -----------------------------------
const body = ["", "## Albums", "", "![[Albums.base#Artist]]", ""];
tR += L.join("\n") + "\n" + body.join("\n");
-%>
Field Assembly
This dossier takes two plugins to assemble.
- Custom Views: the renderer. Make a view, build its rule in the menu to match
categoriescontainsMusicians(the screenshot above), then paste the template, styling, and JS blocks into their fields. - Templater: the note-maker. Paste the Templater block into a template file and bind it to a hotkey or a folder template.
The template fills the note from a public database when you create it, no key needed. After that the card runs on what is in the note.

Statement Ends