Extract metadata from sgf file to create markdown note

What I’m trying to do

I play a board game called go/igo/baduk/weiqi (depending on what Asian country you hail from). Best practice is to review your own games regularly (if not every game you play), so I’ve started writing down my thoughts on each game in an Obsidian note with headings like “Overall thoughts”, “Biggest blunders”, etc.

Game records from online servers are invariably stored in an extremely concise ASCII text format called SGF (see Smart_Game_Format on Wikipedia). The files themselves are more than small enough to store in my vault, and there is even the Cgoban SGF community extension to display the files in a visually meaningful way.

I’m currently copying the sgf files into my vault, and using templater to create notes in my “go-reviews” folder. Templater creates the blank frontmatter and section headings, but I’m currently filling everything out manually after opening the sgf file in an sgf editor to extract the metadata.

But I want to automatically create a markdown note from sgf files. I’d like to automatically extract the metadata from the sgf file to create the frontmatter in the markdown, as well as a link to the unadulterated sgf file in the body of the not (so that I can load/view/potentially-alter the file in an sgf editor external to Obsidian). Every sgf file includes information like the date of the game, the player names/ranks, the software that created the file, etc. I’d like all or most of this info in the frontmatter for the markdown note.

Things I have tried

I’m fairly certain I’ll figure this out on my own eventually, but I’m down with a cold at the moment and I’m hoping for enough spoon feeding to make the project a little easier.

I’m fairly certain I can use tp.file.content() and some Javascript to extract the metadata I need for the markdown template (e.g. `RegExp(“PB\[.*\]”) to extract the black player’s name) , but I’m struggling to figure out the easiest possible workflow to actually create the markdown file while also leaving the source sgf file untouched.

I’d like to just drag files from Finder on my mac into a folder in my vault, and have that action trigger the creation of a related markdown note that I can subsequently edit. I don’t particularly care about the folder hierarchy. It doesn’t matter to me if the sgf and review folders are siblings or hierarchal.

I’d like the sgf filenames to be unchanged from whatever I download from a server (each server has their own naming conventions), but I’d like my markdown note filenames to always be have filenames in the format <DatePlayed>-<BlackPlayerName>-<WhitePlayerName>-<Result>.sgf where all fields are extracted from the metadata in the sgf file.

Any and all help will be greatly appreciated. I’m just looking for enough pseudo-code to get going.

Thanks in advance!

this is something I’ve been meaning to do for my own go study at some point, so would be very keen to see how you get on. From what you’ve described, opening the file and using that as a basis for templater or api calls seems to be the smartest thing.

If I take a crack at this task I’ll post here.

I’m cheezopath on ogs as well :slight_smile:

1 Like

Thanks! Will update as I make progress. I’m new to templater so I’m unsure quite how to proceed, but here’s my basic game plan (do the easiest things first):

  • Create a basic markdown template with the frontmatter and section headings I want
  • Create two folders in my vault: sgf and go-review
  • Write a templater user script to parse an sgf and extract the metadata into object attributes (e.g. sgf.playerBlack might return “Wrexrrw” and sgf.fileName would give a path to the source file)
  • Update the template to call that userScript and populate the markdown frontmatter and filename (always using the same sgf filename at first)
  • Configure templater to automatically add a new note in go-reviews using that template whenever a file is added to the sgf folder

My javascript chops are probably good enough to accomplish the next item on the list, but things get a little handwavy for me on the final two steps. I’m not at all sure how to handle two files in a single template with templater.

[I’m Wrexrrw, 8K on OGS and 4K Fox, currently.]

My template looks like this at the moment, if it helps (I’m filling it out manually):

---
sgf:
date:
server:
lost?:
black-player:
white-player:
black-rank:
white-rank:
result:
komi:
handi:
---

Game file (click to open in Sabaki): `= this.sgf`

## Overall thoughts

## Biggest blunders

## Uncomfortable bits

## Further Study

  • Create a basic markdown template with the frontmatter and section headings I want
  • Create two folders in my vault: sgf and go-review
  • Write a templater user script to parse an sgf and extract the metadata into object attributes (e.g. sgf.playerBlack might return “Wrexrrw” and sgf.fileName would give a path to the source file)
  • Update the template to call that userScript and populate the markdown frontmatter and filename (always using the same sgf filename at first)
  • Configure templater to automatically add a new note in go-reviews using that template whenever a file is added to the sgf folder

I’ve got the parser working. Not the most beautiful code I’ve ever written, but I hate Javascript and it works:

/**
 * parseSgf() - Extract metadata from SGF file contents
 * returns an object with attributes for each bit of game metadata
 */
function sanitize(result) {
  return result?.length ? result[1] : "";
}

module.exports = function(sgfString) {
  const place = /PC\[(.*?)\]/i.exec(sgfString);       // Place played
  const datePlayed = /DT\[(.*?)\]/i.exec(sgfString);  // Date played
  const app = /AP\[(.*?)\]/i.exec(sgfString);         // Applicatio used
  const size = /SZ\[(.*?)\]/i.exec(sgfString);        // Board size
  const game = /GM\[(.*?)\]/i.exec(sgfString);       // Game type (should be 1 for go)
  const annotator = /AN\[(.*?)\]/i.exec(sgfString);   // annotater
  const blackRank = /BR\[(.*?)\]/i.exec(sgfString);   // Black rank
  const whiteRank = /WR\[(.*?)\]/i.exec(sgfString);   // White rank
  const playerBlack = /PB\[(.*?)\]/i.exec(sgfString); // White rank
  const playerWhite = /PW\[(.*?)\]/i.exec(sgfString); // White rank
  const copyright = /CP\[(.*?)\]/i.exec(sgfString);   // Copyright info
  const timeLimit = /TM\[(.*?)\]/i.exec(sgfString);   // (Main) time limit in seconds
  const overtime = /OT\[(.*?)\]/i.exec(sgfString);    // Overtime
  const results = /RE\[(.*?)\]/i.exec(sgfString);     // Results
  const rules = /RU\[(.*?)\]/i.exec(sgfString);       // Game rules (Japanese/Chinese/AGA, etc.)
  const handicap = /HA\[(.*?)\]/i.exec(sgfString);    // Handicap
  const komi = /KM\[(.*?)\]/i.exec(sgfString);        // Komi

  const sgf = {
    place: sanitize(place),
    datePlayed: sanitize(datePlayed),
    app: sanitize(app),
    size: sanitize(size),
    game: sanitize(game),
    annotator: sanitize(annotator),
    blackRank: sanitize(blackRank),
    whiteRank: sanitize(whiteRank),
    playerBlack: sanitize(playerBlack),
    playerWhite: sanitize(playerWhite),
    copyright: sanitize(copyright),
    timeLimit: sanitize(timeLimit),
    overtime: sanitize(overtime),
    results: sanitize(results),
    rules: sanitize(rules),
    handicap: sanitize(handicap),
    komi: sanitize(komi)
  };

  return sgf;
}

I’ve also now got a template that works if I hardcode a path to an sgf file:

<%*
const filename = "Go-reviews/sgf/78701698-147-darkkman-Wrexrrw.sgf"
const file = tp.file.find_tfile(filename)
const sgf = await tp.file.include(file)
const meta = tp.user.parseSgf(sgf)
_%>
---
place: "<% meta.place %>"
datePlayed: "<% meta.datePlayed %>"
app: "<% meta.app %>"
size: "<% meta.size %>"
annotator: "<% meta.annotator %>"
playerBlack: "<% meta.playerBlack %>"
playerWhite: "<% meta.playerWhite %>"
blackRank: "<% meta.blackRank %>"
whiteRank: "<% meta.whiteRank %>"
copyright: "<% meta.copyright %>"
timeLimit: "<% meta.timeLimit %>"
overtime: "<% meta.overtime %>"
results: "<% meta.results %>"
rules: "<% meta.rules %>"
handicap: "<% meta.handicap %>"
komi: "<% meta.komi %>"
sgf: "![[<% filename %>]]"
---


Game file: `= this.sgf`

## Overall thoughts

<% tp.file.cursor() %>

## Biggest blunders

## Uncomfortable bits

## Further Study

Now I’ve got to figure out some way to intercept the addition of an sgf file to the vault, get the filename of that sgf file, parse the contents, then create a new markdown note with the appropriate filename and metadata.

Still pretty unsure how to proceeed with that bit.

Preferred workflow: dragging an sgf file into the vault triggers templater to parse the content and create an automatically titled review note.

Alternative: The act of creating a new review note prompts for an sgf filepath.

The alternative is multiple actions so it’s not ideal, but it looks like it might be easier to implement?

It’s confusing dealing with two files (esp. when one isn’t markdown).

Success!

I ended up going the alternate route, but it’s not too much of a pain because the sgf files added most recently show up at the top of the suggester file list. It’s effectively just another click after dragging and dropping the sgf file.

See the screen recording I made for how to install this into Osbsidian if you’d like create to your own game reviews (knowing there are at least two users, @cheezopath and me, was all the motivation I needed).

I needed to remove as much friction as possible to review my own games. Doing so has definitely improved my game.

The two required files are available on Github. Feel free to modify for your own uses. Don’t look at me if your computer catches fire after running this.

I ended up not needing any help other than Zach Young’s templater snippets to figure out how to do this. It ain’t pretty, but it works. Still not sure if this was THE BEST WAY TO DO IT :trade_mark: but it suffices for my needs.

1 Like

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.