From ba76e77d935998e4b128053dcc61d2ed4884cdda Mon Sep 17 00:00:00 2001 From: soaos Date: Fri, 21 Nov 2025 21:14:12 -0500 Subject: zola migration --- blog/rockbox_stats/index.html | 560 ------------------------------------------ 1 file changed, 560 deletions(-) delete mode 100644 blog/rockbox_stats/index.html (limited to 'blog/rockbox_stats/index.html') diff --git a/blog/rockbox_stats/index.html b/blog/rockbox_stats/index.html deleted file mode 100644 index 4aca821..0000000 --- a/blog/rockbox_stats/index.html +++ /dev/null @@ -1,560 +0,0 @@ - - - - - - - - - - - -
- ↰ Back - ⌂ Home -
-
-
-
- -

Rockbox Stat Tracking

-

September 22, 2025

-

In this post I talk about how I went about setting up a stat visualization page for my rockbox mp3 player.

-
-
- - - - - - - - - - - - - - - - - - -Progressive Rock -Alternative -Post-Rock -Post-Hardcore -Post-Metal -Rock -Shoegaze -Progressive Metal - - -
A static site generation experiment
-
-
-
-

Preamble: Digital Sovereignity & Rockbox

-

- I've been building up a pretty sizeable collection of digital music - over the last couple of years. I think there's a lot of value in owning - the music I pay for and being able to choose how I listen to it. - Purchasing music also allows me to support artists in a more direct - and substantial way than the fractions of cents for using streaming services, - but that's more of a happy consequence than some moral obligation I feel. -

-

- Over the years, I've enjoyed listening to my music in a variety of ways. - For years I kept all of my music files on all of my devices and used - various local music clients depending on the platform, most notably mpd - and ncmpcpp on linux. Eventually, as I charged headlong into the glorious - world of self-hosting, I began using a central Jellyfin media server that - I stream music and video from. It's super convenient, and works on all of - my devices (including my TV!). -

-

- My media server is great, and it's been the primary way I listen to music - for a while now. But it has limitations. For example, I don't expose my media - server to the internet, so I'm unable to stream from it while I'm out and - about. And even if I could, the bandwidth requirements would be pretty high. - I figured I would need a dedicated music player if I wanted to take my music - library on the go, and settled on the HIFI Walker H2 after reading some - online recommendations. The ability to install Rockbox, an open-source firmware, - was a big factor in my decision. I couldn't tell you how the device works - out of the box, since I flashed the firmware pretty much immediately once I got it, - but I've been super impressed with how the device works while running Rockbox. -

-

-

- Screenshot of Rockbox player showing cool theme. -
I'm using a modified version of the InfoMatrix-v2 theme, which looks great.
-
- Rockbox comes with many codecs for common audio formats including FLAC and MP3. The - device boots extremely quickly, and the interface is snappy. Virtually every aspect - of the user experience is tweakable and customizable to a crazy degree. I've even begun - listening to music on my player even at home, since a device specifically for the - purpose provides less distraction while I'm trying to be productive. -

-

- All this to say I'm pretty much sold on Rockbox. But there's certain things I - still miss from my days of being a user of popular services like Spotify with - fancy APIs and data tracking. Things like Spotify wrapped or third-party apps - for visualizing playback statistics are a fun way to see what my listening history - looks like and could potentially be used to help find more music that I'd enjoy. - This is why when I noticed that Rockbock has a playback logging feature, a little - lightbulb lit up over my head. -

-
-
-

Generating and Parsing Logs

-

-

- Logging -
The logging feature can be accessed through the settings menu.
-
- Rockbox has a feature that logs playback information to a text file. This feature can - be enabled by setting Playback Settings > Logging to "On". With this setting enabled, a - new line gets added to the end of the .rockbox/playback.log file every time you play a track, - containing info about what you played and when. -

-

- The logging feature is actually already used by the LastFM scrobbler plugin that comes preloaded with - Rockbox, which is probably the simplest way to get insights into your playback. However, - I personally want to avoid using third-party services as much as possible, because it's more fun. -

-

- If I take a look at a logfile generated after a bit of listening, I'll see that I've wound up with - a series of lines that each look something like this: -

-
1758478258:336689:336718:/<microSD0>/Music/This Is The Glasshouse/This Is The Glasshouse - 867/This Is The Glasshouse - 867 - 01 Streetlight By Streetlight.flac
-
An example of a log entry for "Streetlight by Streetlight" by This is the Glasshouse. -
-
-

-

- I wasn't really able to find any information online about the format of these logs, but they appear - to be simple enough to figure out. From what I can tell, each event is broken up into 4 pieces: -

    -
  1. Timestamp: The number of milliseconds since the UNIX epoch. -
  2. Playback Duration: The amount of the song that was played, in milliseconds. -
  3. Total Track Length: The length of the played track, in milliseconds. -
  4. File Path: An absolute path to the file containing the track on the filesystem. -
- All of this is enough to know what I was listening to and when. I can use the file path to check for - audio tags which can help glean even more information about my listening habits. -

-

Now that I have this information and know how to interpret it, I'm ready to start processing it!

-
-
-

Analyzing Playback History

-

- In order to get some useful information out of my playback history, I think it's a good idea to start by - building - a database. I created a sqlite database with the following tables: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
songs
idi64PK
titleString
artistsJSON
album_idi64?
genreString?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
albums
idi64PK
titleString
artistString
cover_artBlob?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
history
idi64PK
timestampDatetime
durationi64
song_idi64
-
- I can add more columns later, but this is a good place to start. -

-

- Now, as I read through the logfile line-by-line, I can check if each album exists before - inserting it into the database: -

-
for line in log_file.lines().flatten() {
-    println!("{line}");
-    // Skip comments
-    if line.starts_with("#") {
-        continue;
-    }
-    let chunks = line.split(":").collect::>();
-
-    let timestamp = DateTime::from_timestamp_secs(
-        i64::from_str_radix(chunks[0], 10).context("Failed to parse timestamp")?,
-    )
-    .context("Failed to convert timestamp")?;
-
-    // Load tags from file on device
-    let file_path = chunks[chunks.len() - 1][1..]
-        .split_once("/")
-        .context("Missing file")?
-        .1;
-    let tags = Tag::new()
-        .read_from_path(args.mount_point.join(file_path))
-        .context("Failed to read audio tags")?;
-
-    //...
-}
-
Parsing log entry and loading audio metadata.
-
-
-
if let Some(existing_album) =
-    sqlx::query("SELECT id FROM albums WHERE title=$1 AND artist=$2")
-        .bind(album_title)
-        .bind(album_artist)
-        .fetch_optional(&mut *db)
-        .await
-        .context("Failed to execute query to find existing album")?
-{
-    let album_id: i64 = existing_album.get("id");
-    info!("Album already exists, id {album_id}");
-    //...
-} else {
-    info!("Inserting new album: {album_title} by {album_artist}");
-    //...
-    let result = sqlx::query(
-        "INSERT INTO albums (title, artist, cover_art) VALUES ($1, $2, $3);",
-    )
-    .bind(album_title)
-    .bind(album_artist)
-    .bind(cover)
-    .execute(&mut *db)
-    .await
-    .context("Failed to execute query to insert album into database")?;
-
-    //...
-}
-
Checking for an album with matching artist and title before creating a new row in the - database.
-
- I did something similar with the songs and history tables, basically building up a cache - of history information and skipping anything that's already in the database on repeat runs. -

-

- With this database constructed, it's pretty easy to get a bunch of different information - about my listening. For example (forgive me if my SQL skills are kind of ass lol): -

-
SELECT 
-    songs.title AS song_title,
-    songs.artists AS song_artists,
-    songs.genre AS song_genre,
-    albums.title AS album_title,
-    albums.artist AS album_artist,
-    history.timestamp AS timestamp,
-    history.duration AS duration
-FROM history
-CROSS JOIN songs ON songs.id = history.song_id
-CROSS JOIN albums ON albums.id = songs.album_id
-ORDER BY timestamp DESC;
-
Querying for a list of each history entry along with track metadata, sorted from most to - least recent.
-
-
-
SELECT 
-    songs.genre,
-    SUM(history.duration) AS total_duration
-FROM history
-CROSS JOIN songs ON history.song_id = songs.id
-GROUP BY genre
-ORDER BY total_duration DESC
-LIMIT 10; 
-
Querying for the top 10 most listened genres by playtime.
-
-

-

- It's all well and good to be able to view this information using a database client, - but it would be really cool if I could visualize this data somehow. -

-
-
-

Visualizing this Data Somehow

-

- I wanted to make this data available on my website for people to view, and for a bunch of mostly trivial - reasons I won't get into here, I have a couple of requirements for pages on this site: -

    -
  1. Pages need to be static. -
  2. Pages need to be JavaScript-free. -
- This means any chart rendering needs to be done automatically at build time before - deploying. I don't currently use a static site generator for my site (just for fun), - so I'm basically going to need to write one specifically to generate this page. -

-

- I won't get too deep into the specifics of how I queried the database and generated each visualization - on - the page, but I can explain the visualizations I created using the queries from the previous section. - For the - listening history I wanted to generate a table displaying the information. To accomplish this, I first - used a combination of sqlx's ability to convert a row to a struct and serde to serialize - the rows as JSON values. -

-
#[derive(Serialize, Deserialize, FromRow)]
-struct HistoryEntry {
-    song_title: String,
-    song_artists: Value,
-    timestamp: DateTime<Utc>,
-    duration: i64,
-    album_title: String,
-    album_artist: Option<String>,
-    song_genre: Option<String>,
-}
-
-//...later
-let history = sqlx::query_as::<_, HistoryEntry>(
-    /* SELECT... */
-).fetch_all(&mut *db).await;
-
-//...later still, tera context accepts
-let mut context = tera::Context::new();
-context.insert("history", &history);
-
-
Struct definition for a history entry, allowing conversion from a sqlx row and - de/serialization from/to JSON.
-
-

-

- In order to keep the generation as painless as possible, I decided to use the Tera template - engine, which allows me to define a template HTML file and substitute in values from - a context which I can define before rendering. In the case of the table, I can just generate a <tr> - matching the data for each item: -

-
{% macro history_table(history) %}
-<h3>Playback History</h3>
-<div class="table-container">
-    <table>
-        <thead>
-            <tr>
-                <th>Timestamp</th>
-                <th>Played Duration</th>
-                <th>Title</th>
-                <th>Artists</th>
-                <th>Album</th>
-                <th>Genre</th>
-            </tr>
-        </thead>
-        <tbody>
-            {% for item in history %}<tr>
-                <td>{{ item.timestamp | date(format="%Y-%m-%d %H:%M:%S") }}</td>
-                <td>{{ item.duration | hms }}</td>
-                <td>{{ item.song_title }}</td>
-                <td>{{ item.song_artists }}</td>
-                <td>{{ item.album_title }}</td>
-                <td>{{ item.song_genre }}</td>
-            </tr>
-            {% endfor %}
-        </tbody>
-    </table>
-</div>
-{% endmacro history_table %}
-
- A Tera macro for generating a table from a list of playback history items. - I used a macro so I can re-use this later if I want to add time range views. - (last month, year, etc.) -
-
-

-

- I wrote similar macros for each of the visualizations I wanted to create. Most are - easy, but for my top 10 genres I wanted to display a pie chart. I found a pretty decent - data visualization crate called charming that's able to render to html, however - the output contains javascript so it's a no-go for me. Luckily, it can also render to - an SVG which I can embed nicely within the page. -

- - - - - - - - - - - - - - - - - - -Progressive Rock -Alternative -Post-Rock -Post-Hardcore -Post-Metal -Rock -Shoegaze -Progressive Metal - - -
Here's one I generated just now.
-
-

-

- And that's pretty much all there is to it! The finished thing can be found here. -

-
-
- - - \ No newline at end of file -- cgit v1.2.3