Files Origin · The Magnus Institute ● Link Secure Clearance 05 SYS 0451
THE BUREAU obsidian declassified
File OCV-06211948 Clearance 05 · SEALED Vol. I · Jun 14, 2026
COVERFLOW

Albums

Statement Begins

Nine marks, an artist, a year, and the one song I never skip. That is most of an album note before the card gets hold of it. After, the same lines stand up as a record sleeve: the cover, the title, the favourite track pulled to the front like a single.

Nothing is fetched, nothing phoned out. Every word on it is mine, set the way you would want to hold a sleeve and read the back.

What the card adds is the arithmetic. It averages the nine marks, leans on the bonuses, and lands the record on a single number, so the verdict is sitting there before you have read a line of the review.

Custom Views Code

<div class="alb-card" data-name="{{file.name}}">
  <div class="alb-stage">
    <img class="alb-side alb-side-l" alt="" />
    <div class="alb-cover-wrap">
      <img class="alb-cover" id="bgImg" src="" alt="" />
      <button class="alb-more" type="button">SHOW MORE</button>
    </div>
    <img class="alb-side alb-side-r" alt="" />
  </div>
  <div class="alb-head">
    <div class="alb-accent"></div>
    <h1 class="alb-title" data-raw="{{file.basename}}"></h1>
    <p class="alb-sub"></p>
    <div class="alb-scoreline">
      <span class="alb-score"></span>
      <span class="alb-verdict"></span>
    </div>
  </div>
  <div class="alb-detail">
    <div class="alb-dims"></div>
    <div class="alb-picks"></div>
    <blockquote class="alb-lyric" hidden></blockquote>
    <p class="alb-meta"></p>
  </div>
  <div class="alb-notes"></div>
</div>
.obsidian-custom-view-editable .obsidian-custom-view-render,
.obsidian-custom-view-render { padding: 0 !important; --line-width: 100%; --max-width: none; }

.alb-card {
  --alb-accent: var(--color-accent, #d8893f);
  --alb-ink: #ece9e3;
  container-type: inline-size;
  container-name: alb;
  position: relative;
  width: 100%;
  box-sizing: border-box;
  padding: clamp(24px, 4vw, 56px) clamp(20px, 5vw, 80px) clamp(40px, 6vw, 80px);
  color: var(--alb-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 ── */
.alb-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);
}
.alb-cover-wrap { position: relative; z-index: 3; flex: 0 0 auto; }
.alb-cover {
  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;
}
.alb-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;
}
.alb-side-l { transform: rotateY(30deg) scale(.9); margin-right: clamp(16px, 5vw, 80px); transform-origin: right center; }
.alb-side-r { transform: rotateY(-30deg) scale(.9); margin-left: clamp(16px, 5vw, 80px); transform-origin: left center; }

.alb-more {
  position: absolute;
  left: 50%; bottom: 14px;
  transform: translateX(-50%);
  font: 600 11px/1 -apple-system, sans-serif;
  letter-spacing: .12em;
  color: var(--alb-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;
}
.alb-more:hover { background: rgba(20,20,24,.85); }

/* ── head ── */
.alb-head { max-width: 760px; }
.alb-accent { width: 64px; height: 3px; background: var(--alb-accent); margin-bottom: 16px; border-radius: 2px; }
.alb-title { margin: 0; font-size: clamp(28px, 4vw, 50px); font-weight: 700; letter-spacing: -.02em; line-height: 1.05; }
.alb-sub { margin: 8px 0 0; font-size: clamp(13px, 1.2vw, 16px); color: #b7b3ab; letter-spacing: .02em; }
.alb-scoreline { display: flex; align-items: baseline; gap: 16px; margin-top: 16px; flex-wrap: wrap; }
.alb-score { font-size: clamp(40px, 6vw, 70px); font-weight: 800; line-height: 1; color: #fff; }
.alb-score[data-empty="1"] { font-size: clamp(20px,2.4vw,26px); font-weight: 600; color: #8d8980; }
.alb-verdict { font-size: clamp(13px, 1.2vw, 16px); color: #c9c5bd; max-width: 460px; line-height: 1.4; font-style: italic; }

/* ── detail / scorecard (always visible) ── */
.alb-detail { margin-top: clamp(26px, 4vw, 44px); display: flex; flex-direction: column; gap: clamp(22px,3vw,32px); }
.alb-dims { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px 40px; }
.alb-dim { display: grid; grid-template-columns: 92px 1fr 30px; align-items: center; gap: 12px; }
.alb-dim-k { font-size: 11px; letter-spacing: .07em; text-transform: uppercase; color: #97938b; white-space: nowrap; }
.alb-dim-bar { height: 6px; border-radius: 3px; background: rgba(255,255,255,.10); overflow: hidden; }
.alb-dim-bar > i { display: block; height: 100%; background: var(--alb-accent); border-radius: 3px; }
.alb-dim-v { font-size: 13px; font-weight: 600; color: var(--alb-ink); text-align: right; }

.alb-sec-title { font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: #8d8980; margin: 0 0 12px; display: flex; align-items: center; gap: 14px; }
.alb-sec-title::after { content: ""; flex: 1; height: 1px; background: rgba(255,255,255,.1); }
.alb-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.alb-chip { font-size: 13px; color: #cfcbc3; border: 1px solid rgba(255,255,255,.14); border-radius: 999px; padding: 5px 12px; }
.alb-chip.is-fav { border-color: var(--alb-accent); color: #fff; }
.alb-chip.is-fav::before { content: "\2605  "; color: var(--alb-accent); }

.alb-lyric { margin: 0; border-inline-start: 3px solid var(--alb-accent); padding: 4px 0 4px 18px; color: #d8d4cc; font-style: italic; font-size: clamp(14px,1.3vw,17px); line-height: 1.55; }
.alb-lyric cite { display: block; margin-top: 8px; font-style: normal; font-size: 12px; letter-spacing: .04em; color: #8d8980; }

.alb-meta { margin: 0; font-size: 12.5px; color: #8d8980; letter-spacing: .02em; }
.alb-meta b { color: #b7b3ab; font-weight: 600; }

/* ── notes (rendered body, revealed by SHOW MORE) ── */
.alb-notes { display: none; margin-top: clamp(26px,4vw,40px); }
.alb-card.notes-open .alb-notes { display: block; }
.alb-notes-body { font-size: 15.5px; line-height: 1.7; color: #b7b3ab; }
.alb-notes-body > :first-child { margin-top: 0; }
.alb-notes-body h2 { font-size: 14px; letter-spacing: .06em; text-transform: uppercase; color: #cfcbc3; margin: 24px 0 8px; }
.alb-notes-body h3 { font-size: 14px; color: #cfcbc3; margin: 16px 0 6px; }
.alb-notes-body a { color: var(--alb-accent); text-decoration: none; }

/* ── narrow / mobile ── */
@container alb (max-width: 640px) {
  .alb-side { display: none; }
  .alb-cover { width: clamp(200px, 60vw, 300px); }
  .alb-dims { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
  .alb-side { display: none; }
  .alb-cover { width: clamp(200px, 60vw, 300px); }
  .alb-dims { grid-template-columns: 1fr; }
}
const _c = this;
(async () => {
  try {
    const root = _c.querySelector(".alb-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]);
    // display label for a link: prefer the [[target|DISPLAY]] part, else strip a "Type - " prefix
    const label = s => { let v = clean(s); v = v.includes("|") ? v.split("|").pop() : v.replace(/^[^|]+ - /, ""); return v.trim(); };

    // --- cover (local [[png]] -> resource path, or remote URL) ---
    let coverVal = fm.cover; if (Array.isArray(coverVal)) coverVal = coverVal[0];
    let resolved = "", dest = null;
    if (coverVal) {
      const s = String(coverVal).trim();
      if (/^https?:\/\//.test(s)) resolved = s;
      else {
        const m = s.match(/\[\[([^\]|#]+)/);
        const linkpath = m ? m[1].trim() : s.replace(/^!?\[\[|\]\]$/g, "").trim();
        dest = app.metadataCache.getFirstLinkpathDest(linkpath, file.path);
        if (dest) resolved = app.vault.getResourcePath(dest);
      }
      if (resolved) { const c = root.querySelector(".alb-cover"); if (c) c.src = resolved; }
    }

    // --- artist images -> side panels ---
    const imgs = list(fm.artist_images).map(String).filter(Boolean);
    const sl = root.querySelector(".alb-side-l"), sr = root.querySelector(".alb-side-r");
    if (sl) { if (imgs[0]) sl.src = imgs[0]; else sl.remove(); }
    if (sr) { if (imgs[1]) sr.src = imgs[1]; else if (imgs[0]) sr.src = imgs[0]; else sr.remove(); }

    // --- title / sub ---
    const title = root.querySelector(".alb-title");
    if (title) title.textContent = (title.getAttribute("data-raw") || "").replace(/^Albums?\s*-\s*/, "");
    const artists = list(fm.artist).map(label).filter(Boolean);
    const subBits = [];
    if (artists.length) subBits.push(artists.join(", "));
    if (fm.year) subBits.push(String(fm.year));
    if (fm.number_of_tracks) subBits.push(fm.number_of_tracks + " tracks");
    const sub = root.querySelector(".alb-sub"); if (sub) sub.textContent = subBits.join("  ·  ");

    // --- Total Album Score (audiophage formula, verbatim) ---
    const num = v => (typeof v === "number" && !isNaN(v)) ? v : 0;
    const S = ["sound_quality","production_quality","album_tone","singing","lyrics","album_art","replayability","album_cohesiveness"];
    const tracks = Array.isArray(fm.favorite_tracks) ? fm.favorite_tracks.length : 0;
    const pct = fm.number_of_tracks ? tracks / fm.number_of_tracks * 10 : 0;
    const fav = fm.favorite ? 10 : 0, rel = fm.relistened_14d ? 10 : 0, tt = fm.title_track_good ? 10 : 0;
    const sum = S.reduce((a, k) => a + num(fm[k]), 0) + pct + num(fm.personal_score) + fav + rel + tt;
    const filled = S.filter(k => num(fm[k])).length + (pct ? 1 : 0) + (num(fm.personal_score) ? 1 : 0) + (fav ? 1 : 0) + (rel ? 1 : 0) + (tt ? 1 : 0);
    const total = filled ? Math.round(sum / filled * 10) : 0;
    const B = [[100,'A "True 10" — flawless in every aspect.'],[95,'A "10" — an all-time great with only unoverlookable minor flaws.'],[90,'A potential 10 held back by some flaws, or a genre-defining record. An all-time great.'],[85,'A potential AOTY contender. Memorable and influential, very few flaws.'],[80,'A good album that stands out against most other music. Worth listening to.'],[75,'Above average songwriting, several elevating elements and 1 spectacular redeeming quality.'],[70,'An average album with some elevating element (vocals, instrumentation).'],[65,'Above average, with noticeable flaws or lack of originality.'],[60,'An OK album that offers some memorable or enjoyable elements.'],[55,'Above-par but fails on most aspects; generally disappointing.'],[50,'The midpoint — average, forgettable.'],[45,'Subpar, fails to deliver on most aspects.'],[40,'Poor, significant flaws, little to enjoy.'],[35,'Below average, noticeable flaws, lack of originality.'],[30,'Flat average, typical, forgettable.'],[25,'Disappointing, major issues outweigh positives.'],[20,'Largely unenjoyable, few redeeming qualities.'],[15,'Severely flawed, not worth experiencing.'],[10,'Among the worst in its class, nearly unbearable.'],[5,'Abysmal — a complete failure, no redeeming qualities.'],[0,'Not worth mentioning.']];
    const scoreEl = root.querySelector(".alb-score"), verdictEl = root.querySelector(".alb-verdict");
    if (filled) { scoreEl.textContent = String(total); verdictEl.textContent = (B.find(b => total >= b[0]) || B[B.length - 1])[1]; }
    else { scoreEl.textContent = "Not yet scored"; scoreEl.setAttribute("data-empty", "1"); }

    // --- dimension bars ---
    const DIMS = [["sound_quality","Sound"],["production_quality","Production"],["album_tone","Tone"],["singing","Vocals"],["lyrics","Lyrics"],["album_art","Art"],["replayability","Replay"],["album_cohesiveness","Cohesion"],["personal_score","Personal"]];
    const dimsEl = root.querySelector(".alb-dims");
    if (dimsEl) DIMS.forEach(([k, label]) => {
      const val = num(fm[k]); if (!val) return;
      const row = document.createElement("div"); row.className = "alb-dim";
      row.innerHTML = `<span class="alb-dim-k">${label}</span><span class="alb-dim-bar"><i style="width:${Math.min(100, val * 10)}%"></i></span><span class="alb-dim-v">${val}</span>`;
      dimsEl.appendChild(row);
    });

    // --- favorite tracks ---
    const ftracks = list(fm.favorite_tracks).map(clean).filter(Boolean);
    const topTrack = clean(fm.favorite_track);
    const picksEl = root.querySelector(".alb-picks");
    if (picksEl && (ftracks.length || topTrack)) {
      const ordered = []; if (topTrack) ordered.push(topTrack);
      ftracks.forEach(t => { if (t !== topTrack) ordered.push(t); });
      picksEl.innerHTML = `<p class="alb-sec-title">Favorite tracks</p><div class="alb-chips">${ordered.map((t, i) => `<span class="alb-chip${i === 0 && topTrack ? " is-fav" : ""}">${t}</span>`).join("")}</div>`;
    } else if (picksEl) picksEl.remove();

    // --- favorite lyric ---
    const lyric = clean(fm.favorite_lyrics), lyricEl = root.querySelector(".alb-lyric");
    if (lyricEl && lyric) {
      const src = clean(fm.favorite_lyrics_track);
      lyricEl.innerHTML = `“${lyric}”` + (src ? `<cite>— ${src}</cite>` : "");
      lyricEl.removeAttribute("hidden");
    } else if (lyricEl) lyricEl.remove();

    // --- review metadata ---
    const meta = [];
    if (fm.date_reviewed) meta.push(`<b>Reviewed</b> ${String(fm.date_reviewed).slice(0,10)}`);
    if (fm.gear) meta.push(clean(fm.gear));
    if (fm.listening_source) meta.push(clean(fm.listening_source));
    if (fm.collection_type) meta.push(clean(fm.collection_type));
    const genres = list(fm.genre).map(clean).filter(Boolean);
    if (genres.length) meta.push(genres.join(" · "));
    const metaEl = root.querySelector(".alb-meta");
    if (metaEl) { if (meta.length) metaEl.innerHTML = meta.join("  ·  "); else metaEl.remove(); }

    // --- notes: render the body (minus frontmatter + score callout) ---
    const more = root.querySelector(".alb-more");
    const notesEl = root.querySelector(".alb-notes");
    let hasNotes = false;
    if (notesEl) {
      try {
        const obs = require("obsidian");
        let body = await app.vault.read(file);
        body = body.replace(/^---[\s\S]*?\n---\n?/, "");                                   // strip frontmatter
        body = body.replace(/^\s*((?:>.*\n?)+)\n?/, m => /dataviewjs|\[!abstract\]/.test(m) ? "" : m); // strip score callout
        body = body.trim();
        const meaningful = body.replace(/^#{1,6}\s.*$/gm, "").replace(/\s+/g, "");          // real content beyond headings?
        if (meaningful) {
          notesEl.innerHTML = '<p class="alb-sec-title">Notes</p><div class="alb-notes-body markdown-rendered"></div>';
          const target = notesEl.querySelector(".alb-notes-body");
          const comp = new obs.Component();
          if (obs.MarkdownRenderer && obs.MarkdownRenderer.render) await obs.MarkdownRenderer.render(app, body, target, file.path, comp);
          else if (obs.MarkdownRenderer) await obs.MarkdownRenderer.renderMarkdown(body, target, file.path, comp);
          hasNotes = true;
        } else notesEl.remove();
      } catch (e) { console.error("[Albums notes]", 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 colour matched to the cover (bg stays neutral) ---
    if (resolved) {
      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;                                  // near-greyscale cover → keep default accent
          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("--alb-accent", hex(r,g,b));
        } catch (e) { /* tainted canvas → keep default accent */ }
      };
      let src = resolved, isBlob = false, cors = false;
      if (dest) { try { const buf = await app.vault.readBinary(dest); src = URL.createObjectURL(new Blob([buf])); isBlob = true; } catch (e) {} }
      else cors = true;
      const probe = new Image(); if (cors) probe.crossOrigin = "anonymous";
      probe.onload = () => { sample(probe); if (isBlob) URL.revokeObjectURL(src); };
      probe.onerror = () => { if (isBlob) URL.revokeObjectURL(src); };
      probe.src = src;
    }
  } catch (e) { console.error("[Custom Views: Albums]", e); }
})();

Templater Template

<%*
/**
 * MusicBrainz → Album Frontmatter (Obsidian Templater)
 * No API key required. Searches MusicBrainz release-groups, pulls cover from
 * Cover Art Archive, writes full frontmatter, renames the note.
 * Rendered as a card by the Custom Views "Albums" view.
 */
const MB = "https://musicbrainz.org/ws/2";
const CAA = "https://coverartarchive.org";
const today = tp.date.now("YYYY-MM-DD");
const fileTitle = tp.file.title;

// 0) Importer handshake — stardust-importer queues {artist, album, year,
// trackCount, listenedAt} keyed by filename. When present we auto-select the
// best MusicBrainz match instead of prompting; on a weak match we still attach
// the best guess and flag the note for review (option b).
const __P = app.plugins.plugins["stardust-importer"]?.pending?.[fileTitle];

let query;
let chosen = null;
let needsReview = false;

if (__P) {
  query = (__P.album || fileTitle).trim();
  let searchResponse;
  try {
    searchResponse = await tp.web.request(
      `${MB}/release-group?query=${encodeURIComponent(`"${query}" AND artist:"${__P.artist || ""}" AND primarytype:Album`)}&fmt=json&limit=15`
    );
  } catch (e) { tR += `MusicBrainz search error: ${e}`; return; }
  const results = (searchResponse && searchResponse["release-groups"]) || [];
  if (!results.length) { tR += `No MusicBrainz match for "${query}".`; return; }
  // Auto-pick: prefer an artist-credit + exact-title match, then closest year.
  const wantArtist = (__P.artist || "").toLowerCase().trim();
  const wantYear = parseInt((__P.year || "").toString().slice(0, 4)) || null;
  const scored = results.map(r => {
    const a = ((r["artist-credit"] || []).map(c => c.name).join(" ") || "").toLowerCase();
    const y = parseInt((r["first-release-date"] || "").slice(0, 4)) || null;
    const titleExact = (r.title || "").toLowerCase().trim() === query.toLowerCase();
    const artistMatch = !!wantArtist && a.includes(wantArtist);
    const yearGap = (wantYear && y) ? Math.abs(wantYear - y) : 99;
    let score = (titleExact ? 2 : 0) + (artistMatch ? 3 : 0) - Math.min(yearGap, 10) * 0.1;
    return { r, score, artistMatch, titleExact };
  }).sort((x, y) => y.score - x.score);
  chosen = scored[0].r;
  needsReview = !(scored[0].artistMatch && scored[0].titleExact);
} else {
  // 1) Ask for album title -----------------------------------------------
  query = await tp.system.prompt("Enter album title (leave empty for file name):");
  if (!query || !query.trim()) query = fileTitle;
  query = query.trim();

  // 2) Search MusicBrainz release-groups (albums only) ------------------
  let searchResponse;
  try {
    searchResponse = await tp.web.request(
      `${MB}/release-group?query=${encodeURIComponent(query)}%20AND%20primarytype:Album&fmt=json&limit=15`
    );
  } catch (e) { tR += `MusicBrainz search error: ${e}`; return; }

  const results = (searchResponse && searchResponse["release-groups"]) || [];
  if (!results.length) { tR += `No results for "${query}".`; return; }

  const display = results.map(r => {
    const t = r.title || "Unknown";
    const y = (r["first-release-date"] || "").slice(0, 4) || "????";
    const a = (r["artist-credit"] && r["artist-credit"][0] && r["artist-credit"][0].name) || "?";
    return `${t} (${y}) — ${a}`;
  });

  // 3) Pick -------------------------------------------------------------
  chosen = await tp.system.suggester(display, results, true, "Pick an album");
  if (!chosen) { tR += "Selection cancelled."; return; }
}

// 4) Extract -------------------------------------------------------------
const albumTitle = (chosen.title || fileTitle).trim();
const year = (chosen["first-release-date"] || "").slice(0, 4) || "";
const artists = (chosen["artist-credit"] || []).map(c => c.name).filter(Boolean);
const tagObjs = (chosen.tags || []).sort((a,b) => (b.count || 0) - (a.count || 0));
const genres  = tagObjs.slice(0, 5).map(t => t.name).map(s => s.replace(/\b\w/g, c => c.toUpperCase()));

// Cover Art Archive serves the cover for the release-group via redirect;
// the URL works directly as an <img src>.
const cover = `${CAA}/release-group/${chosen.id}/front-500`;

// 5) Confirm cover exists (some release-groups have no front cover) -----
// Try a tiny HEAD-ish request; if 404 we drop the cover URL.
let coverOk = true;
try {
  const r = await tp.web.request(`${CAA}/release-group/${chosen.id}`);
  // If JSON of images returns no front, mark missing.
  if (r && Array.isArray(r.images) && !r.images.some(i => i.front)) coverOk = false;
} catch (e) { coverOk = false; }
const coverURL = coverOk ? cover : "";

// 5b) Artist images (TheAudioDB, no key) for the coverflow side panels --
// Up to 2 landscape artist photos; the Albums card uses them as the tilted
// panels flanking the cover. Falls back to cover-only if none are found.
let artistImages = [];
const primaryArtist = artists[0] || "";
if (primaryArtist) {
  try {
    const adb = await tp.web.request(
      `https://www.theaudiodb.com/api/v1/json/2/search.php?s=${encodeURIComponent(primaryArtist)}`
    );
    const a = (adb && adb.artists && adb.artists[0]) || null;
    if (a) {
      const pool = [a.strArtistFanart, a.strArtistFanart2, a.strArtistFanart3, a.strArtistWideThumb, a.strArtistThumb];
      for (const u of pool) {
        if (u && !artistImages.includes(u)) artistImages.push(u);
        if (artistImages.length === 2) break;
      }
    }
  } catch (e) { /* no artist images — card falls back to the cover only */ }
}

// Helper: link an existing artist note if present
async function noteExists(name) {
  for (const folder of ["References", ""]) {
    const path = folder ? `${folder}/${name}.md` : `${name}.md`;
    try { if (await tp.file.exists(path)) return true; } catch (e) {}
  }
  return false;
}
async function linkOrName(names, alwaysLinkFirst = 2) {
  const out = [];
  for (let i = 0; i < names.length; i++) {
    if (i < alwaysLinkFirst) out.push(`[[${names[i]}]]`);
    else out.push((await noteExists(names[i])) ? `[[${names[i]}]]` : names[i]);
  }
  return out;
}
const artistLinks = await linkOrName(artists);
const genreLinks  = await linkOrName(genres, 0);  // genres always wiki-linked iff a note exists; else plain

// 6) Rename to "Albums - Title" ----------------------------------------
const colonFixed = albumTitle.replace(/:/g, " - ");
let sanitized = colonFixed.replace(/[\/#%&{}<>*?$!'"@+`|=]/g, "").trim();
if (!sanitized) sanitized = fileTitle;
const requiresAlias = sanitized !== albumTitle;
const newName = sanitized;
if (newName !== fileTitle) await tp.file.rename(newName);

// 7) Build YAML ---------------------------------------------------------
// Identity + classification fields come from MusicBrainz; the review/scoring
// fields below are written blank for you to fill in by hand. The computed
// fields (Total Album Score, Rating Description, Percentage of Album Liked,
// Core Fields, Bonus) are NOT stored here — they are derived live by formulas in
// Albums.base, the same way Notion recalculated its formula columns.
const L = [];
L.push("---");
L.push("connections:");
L.push("categories:");
L.push('  - "[[Albums]]"');
// target = filename-sanitized name so it matches the Musician note, display = raw name)
if (artists.length) {
  L.push("artist:");
  artists.forEach(a => {
    const tgt = a.replace(/[\/#%&{}<>*?$!'"@+`|=]/g, "").trim();
    const disp = a.replace(/[\[\]|]/g, "");
    L.push('  - "[[' + tgt + '|' + disp + ']]"');
  });
} else L.push("artist: []");
if (genreLinks.length) { L.push("genre:"); genreLinks.forEach(g => L.push(`  - "${g}"`)); } else L.push("genre: []");
L.push(coverURL ? `cover: "${coverURL}"` : "cover: []");
if (artistImages.length) { L.push("artist_images:"); artistImages.forEach(u => L.push(`  - ${u}`)); } else L.push("artist_images: []");
L.push(`year: ${year}`);
L.push(`created: ${(__P && __P.listenedAt) || today}`);
L.push(`mbid: ${chosen.id}`);

// Review meta -----------------------------------------------------------
L.push("status: Active");
L.push(`date_reviewed: ${(__P && __P.listenedAt) || today}`);
L.push("gear: ");
L.push("listening_source: ");
L.push("collection_type: ");
L.push("language: English");
L.push(`number_of_tracks: ${(__P && __P.trackCount) || ""}`);

// Scores — each 0–10 -----------------------------------------------------
L.push("sound_quality: ");
L.push("production_quality: ");
L.push("album_tone: ");
L.push("singing: ");
L.push("lyrics: ");
L.push("album_art: ");
L.push("replayability: ");
L.push("album_cohesiveness: ");
L.push("personal_score: ");

// Flags ------------------------------------------------------------------
L.push("favorite: false");
L.push("relistened_14d: false");
L.push("title_track_good: false");
if (__P && needsReview) L.push("needs_review: true");

// Favorites --------------------------------------------------------------
L.push("favorite_tracks: []");
L.push("favorite_lyrics_track: ");
L.push("favorite_lyrics: ");
L.push("---");

// Body scaffold ----------------------------------------------------------
// The dataviewjs block recomputes the Total Album Score live from this note's
// own scores — the same formula as Albums.base, shown on the page itself.
const body = [
  "",
  ...(__P && needsReview
    ? ["> [!warning] Auto-matched album — verify this is the right release (artist / year / edition), then clear `needs_review`.", ""]
    : []),
  "> [!abstract] Score",
  "> ```dataviewjs",
  "> const p = dv.current();",
  '> const n = v => (typeof v === "number" && !isNaN(v)) ? v : 0;',
  '> const C = ["sound_quality","production_quality","album_tone","singing","lyrics","album_art","replayability","album_cohesiveness","personal_score"];',
  "> const tracks = Array.isArray(p.favorite_tracks) ? p.favorite_tracks.length : 0;",
  "> const pct = p.number_of_tracks ? tracks / p.number_of_tracks * 10 : 0;",
  "> const sum = C.reduce((a,k)=>a+n(p[k]),0) + pct;",
  "> const count = C.filter(k=>n(p[k])).length + (pct?1:0);",
  "> const bonus = (p.favorite?3:0) + (p.relistened_14d?2:0) + (p.title_track_good?2:0);",
  "> const total = count >= 6 ? Math.min(100, Math.round(sum/count*9 + bonus/7*10)) : null;",
  "> const B = [[100,'A \"True 10\" — flawless in every aspect.'],[95,'A \"10\" — an all-time great with only unoverlookable minor flaws.'],[90,'A potential 10 held back by some flaws, or a genre-defining record. An all-time great.'],[85,'A potential AOTY contender. Memorable and influential, very few flaws.'],[80,'A good album that stands out against most other music. Worth listening to.'],[75,'Above average songwriting, several elevating elements and 1 spectacular redeeming quality.'],[70,'An average album with some elevating element (vocals, instrumentation).'],[65,'Above average, with noticeable flaws or lack of originality.'],[60,'An OK album that offers some memorable or enjoyable elements.'],[55,'Above-par but fails on most aspects; generally disappointing.'],[50,'The midpoint — average, forgettable.'],[45,'Subpar, fails to deliver on most aspects.'],[40,'Poor, significant flaws, little to enjoy.'],[35,'Below average, noticeable flaws, lack of originality.'],[30,'Flat average, typical, forgettable.'],[25,'Disappointing, major issues outweigh positives.'],[20,'Largely unenjoyable, few redeeming qualities.'],[15,'Severely flawed, not worth experiencing.'],[10,'Among the worst in its class, nearly unbearable.'],[5,'Abysmal — a complete failure, no redeeming qualities.'],[0,'Not worth mentioning.']];",
  "> const verdict = total === null ? '' : (B.find(b => total >= b[0]) || B[B.length-1])[1];",
  "> dv.paragraph(total === null ? `_Not yet scored — ${count}/6 core fields._` : `**${total}** — ${verdict}`);",
  "> ```",
  "",
  "## Listening Tips",
  "",
  "## First Impressions",
  "",
  "## Second Listen",
  "",
  "## Artist Impressions",
  "",
  "## Album History",
  "",
  "## Track Breakdown",
  "",
  "## Album Details",
  ""
];
tR += L.join("\n") + "\n" + body.join("\n");
-%>
DECLASSIFIED

Field Assembly

Want a record sleeve like this of your own? Two plugins do the work.

  • Custom Views: the renderer. Make a view, build its rule in the menu to match categories contains Albums (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

▌ Cross-Referenced Files