Using TSV data with Jekyll
I recently added a new page to my website: /playlist. I have been kicking the idea around for some time, unsure of the best path forward for both archival reasons and ease of access (actually listening to the music). A custom web component that you feed an M3U playlist that does all the web audio streaming magic? I decided to tackle the most pressing problem first - data retrieval and archival. In other words: how do we get the song data, and how can we display it? Thankfully, Apple Music has some built-in methods for exporting a playlist! It can do XML, but it ignores the track ordering as far as I could tell. It claims to be able to do an M3U, but that resulted in an empty file for me. Finally, plain text to the rescue! Except… it’s not plain text, it’s a hidden tab-separated value (TSV1) file! Does that count as plain text?
Name Artist Composer Album Grouping Work Movement Number Movement Count Movement Name Genre Size Time Disc Number Disc Count Track Number Track Count Year Date Modified Date Added Bit Rate Sample Rate Volume Adjustment Kind Equalizer Comments Plays Last Played Skips Last Skipped My Rating Location
All Your Hiding Moonrisers Elizabeth Ann Nowicki Harsh & Exciting Instrumental 7126978 172 1 1 3 10 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:13 PM
So Hot You're Hurting My Feelings Squirrel Flower Take It or Leave It / So Hot You're Hurting My Feelings - Single Alternative 7475706 209 1 1 2 2 2020 12/1/23, 2:16 PM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:17 PM
Here's Your Song Fatal Flowers Fatal Flowers Fatal Flowers / Younger Days Pop 6362459 176 1 1 16 16 1986 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:20 PM
Barbarian Kings Morgan Delt Morgan Delt Indie Pop 8453329 233 1 1 2 11 2014 4/24/25, 6:29 PM 256 44100 Apple Music AAC audio file 3 4/30/25, 12:23 PM
Should've Been a Cowboy Toby Keith Toby Keith Should've Been a Cowboy (25th Anniversary Edition) Country 8108461 209 1 1 1 13 1993 4/26/25, 3:22 PM 4/26/25, 3:22 PM 256 44100 Apple Music AAC audio file 4 4/30/25, 12:27 PM
A Horse with No Name America Dewey Bunnell Made in America Rock 9612543 252 1 1 1 22 1971 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 2 4/30/25, 12:31 PM
On the Road Rattlesnake Milk SEAN LEWIS Chicken Fried Snake Americana 8035663 219 1 1 1 9 2022 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:35 PM
Bei Mir Bist Du Schoen Charley Crockett Sholom Secunda A Stolen Jewel Blues 6146267 166 1 1 13 13 2015 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:38 PM
Hi Dee Dee Ty Segall Ty Segall Three Bells Alternative 6918311 191 1 1 4 15 2024 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:41 PM
Her Eyes Are a Blue Million Miles Captain Beefheart & His Magic Band Captain Beefheart Clear Spot Rock 6281232 177 1 1 10 12 1972 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:44 PM
King Of The Road Roger Miller Roger Miller Golden Hits Country 5339317 147 1 1 1 11 1965 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:46 PM
Touch Me I'm Sick Mudhoney Mudhoney, Steve Turner, Dan Peters, Mark McLaughlin & Matt Lukin Superfuzz Bigmuff (Deluxe Edition) Alternative 5568110 152 1 2 1 17 1988 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:49 PM
Thick As a Brick (Edit No. 1) Jethro Tull Ian Anderson The Best of Acoustic Jethro Tull (Remastered) Rock 7427506 182 1 1 6 24 2001 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:52 PM
Steam Powered Aereo Plane John Hartford John Hartford Aereo-Plain Singer/Songwriter 8838764 223 1 1 11 16 1971 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 12:56 PM
The Landkeeper Men I Trust Jessy Caron, Emmanuelle Proulx & Dragos Chiriac Equus Asinus Folk 8179625 224 1 1 6 14 2025 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 1:04 PM
Con Te Partiro Andrea Bocelli Francesco Sartori Bocelli Pop 8895872 250 1 1 1 10 1995 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 1:08 PM
Pyramid of Health Viagra Boys Sebastian Murphy, Henrik Höckert, Tor Sjödén, Elias Jungqvist, Oskar Carls, Linus Hillborg & Pelle Gunnerfeldt viagr aboys Punk 7095223 195 1 1 4 11 2025 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 1:12 PM
Thruck Diebes YYARD Yannis Anft Pattern Gardening House 14223591 389 1 1 9 22 2025 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 1:18 PM
So Hot You're Hurting My Feelings Caroline Polachek Caroline Polachek, Daniel Nigro & Teddy Geiger Pang Indie Pop 6656021 183 1 1 12 2019 4/30/25, 8:52 AM 256 44100 Apple Music AAC audio file 1 4/30/25, 1:21 PM
It turns out, Jekyll has the native ability to work with TSV files2. Nice! There’s even a pretty nice blog post to show you how to get started (with a CSV at least)3. However, in Jekyll, so-called “Data Files” are unique from regular posts. They’re just raw YAML, JSON, CSV, or TSV data. You are expected to import and use the data on individual pages. The data files are not rendered to anything directly4. A good example of (another) place I make use of data files is my /about page, which is just a bunch of tables organized by a single .yml
file5.
There is a similar construct in Jekyll called the collection
6. However, a collection is more like a fancy category of pages. In fact, the default _posts
and _drafts
directories are actually just collections under the hood. So a custom collection allows you to have a custom directory, such as _playlists
. A collection, like a page, is designed to be rendered to an HTML file.
Possibly the most straightforward approach would be to write some kind of TSV to YAML converter and store all the song and playlist data in the collection item itself. But that just sounded bad to me, not very fun. Plus, we have super clean and high-quality TSV data from Apple, and Jekyll can read them natively, and I really just did not want to deal with it. I think of the TSV files as the gold source of truth and leaving those clean and as-is will be useful down the road.
Leaving the TSV’s clean and as-is meant that I also couldn’t easily add my own metadata. Maybe some playlists I want to include a little note for? I also want to include the Apple Music and Spotify playlist IDs for easy linking. I also need something for Jekyll to render.
What I settled on was a joint approach. The easiest way to illustrate is to examine the file structure:
_data/playlists
└── 2025
├── 7.tsv
├── 8.tsv
└── 9.tsv
_playlists
└── 2025
├── 7.md
├── 8.md
└── 9.md
Each TSV playlist file has an accompanying playlist markdown file inside the _playlists
collection. As we discussed before, the TSV files contain the data directly from Apple Music as-is, which Jekyll will helpfully process into a hash for us. The markdown file contains optional front-matter for my own metadata such as the notes and IDs I mentioned before. An example markdown file with front matter might look like:
---
notes: A note about the playlist.
apple: 2025-week-17/pl.u-gxblYeJs3lYDvk
spotify: 6uz3rlHSLGW8R4TVO2o8T8
---
However, most playlist markdown files are empty since the front matter is optional and I’m too lazy to go take the time to go fill it out for all the old playlists.
Note that the only thing that “links” a playlist markdown file to a playlist TSV file is the naming convention. There may have been a way to link to the specific playlist with a variable in front matter, but it for sure would require me to at least type a few extra characters and I’m literally too lazy. So a nice naming and file structure convention fits the bill quite nicely for me!
Inside of my liquid template, I can find the correct data for the current markdown file by parsing out some of the current file’s information:
<!-- page.path: "_playlists/2025/17.md" -->
{% assign id = page.path | split: "/" %}
{% assign year = id[1] %}
{% assign week = id[2] | split: "." | first %}
{% assign songs = site.data.playlists[year][week] %}
At this point, songs
is an array of hashes with all our glorious song data!
I went an extra step and made my playlist UI it’s own Jekyll include. See my previous post about Jekyll includes as re-usable components7 for more information on motivation and implementation details there. The full source of my playlist “component” (and everything else on this site) is available on GitHub8.
I wanted to have a dedicated /playlist page in addition to the usual index. In fact, that’s the primary interface into the data. You can dig deeper into the “archive”, but I wanted my fancy playlist partial to render in both places cleanly. Because I’d done the work to break out the playlist into a partial, it’s as simple as grabbing a few data points. I had to do some tricky logic to sort all my posts by year and get the most recent one, but I think it’s working.
<!-- get the most recent playlist -->
{% assign year = site.playlists | group_by_exp: "playlist", "playlist.path | split: '/' | slice: 1, 1 | first" | last %}
{% assign playlists = year.items | sort: "slug" %}
{% assign playlist = playlists.last %}
{% assign id = playlist.path | split: "/" %}
{% assign year = id[1] %}
{% assign week = id[2] | split: "." | first %}
{% assign songs = site.data.playlists[year][week] %}
Once I have a reference to the collection item (either the current page
, or the playlist
variable above), I can simply render my partial:
{% include playlist.html apple=playlist.apple spotify=playlist.spotify songs=songs year=year week=week notes=playlist.notes %}
Adding new playlists consists of exporting the TSV from Apple Music, and then touch
ing the markdown file (and adding notes manually). I’d really like to get rid of that last step, needing the markdown file at all - but like I outlined above, I want to keep the Apple Music TSV “pristine” and keep my personal metadata in it’s own place.
The two-pronged approach to using raw Jekyll data files, in conjunction with a Jekyll collection are what make this work. Following the same naming convention for both file types allows me to programmatically generate pages for each playlist data file. I’m sure there will be future iterations here, but for now I’m pretty happy to finally scratch this one off the to-do list!
-
There may be a way to dynamically render data files, but I couldn’t figure out how. ↩
-
https://github.com/catskull/catskull.github.io/blob/master/_data/about.yml ↩
-
https://github.com/catskull/catskull.github.io/blob/master/_includes/playlist.html ↩