<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>DepUnPr</title><link href="https://kirgroup.net/blog/" rel="alternate"/><link href="https://kirgroup.net/blog/atom.xml" rel="self"/><id>https://kirgroup.net/blog/</id><updated>2026-03-16T00:00:00+00:00</updated><subtitle>Department of Unnecessary Projects</subtitle><entry><title>Matita</title><link href="https://kirgroup.net/blog/2026/03/16-matita.html" rel="alternate"/><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2026-03-16:/blog/2026/03/16-matita.html</id><summary type="html">The unnecessary dynamic dashboard for collectd/RRD</summary><content type="html">&lt;p&gt;My &amp;ldquo;homelab&amp;rdquo; at home is made up by one (1) raspberry-pi 4 which runs some services, like &lt;a href="https://pi-hole.net/"&gt;pi-hole&lt;/a&gt;, &lt;a href="https://www.wireguard.com/"&gt;wireguard&lt;/a&gt;, &lt;a href="https://www.musicpd.org/"&gt;mpd&lt;/a&gt;, etc etc.&lt;/p&gt;
&lt;p&gt;It is also running &lt;a href="https://collectd.org/"&gt;collectd&lt;/a&gt; to &amp;ldquo;gathers metrics from various sources&amp;rdquo; (as they say). This data is stored in RRD files, for the ease of config.&lt;/p&gt;
&lt;p&gt;May moons ago I wrote a bash script which, via the magic incantations of &lt;code&gt;rrdtool graph&lt;/code&gt;, generated some graphs (you can read about that in &lt;a href="/2025/01/16-DYI-RPI-homeserver-dashboard.html"&gt;this post&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Everything was nice, I was happy, my raspberry was happy. Then one sad day, someone® deleted &lt;em&gt;by accident&lt;/em&gt; the script. The script that generated all the nice graphs. The script of which there was no other copy around.&lt;/p&gt;
&lt;p&gt;I was at a crossroad: rewrite the script or start a new Unnecessary Project? After a quick meeting with the Department Council, a decision was reached: start a new project, obviously. This is the Department of Unnecessary Projects, after all!&lt;/p&gt;
&lt;p&gt;This is (more or less) the true story of how &lt;a href="https://git.sr.ht/~fabrixxm/matita"&gt;Matita&lt;/a&gt; was born.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Matita&lt;/em&gt; is a dynamic dasboard for collectd/rrd. A php file serves some API to manipulate graph configurations and the data fetched from RRD to populate those graph.&lt;/p&gt;
&lt;p&gt;An ugly html/css/js combination uses these API to show those graps on a page (using &lt;a href="https://www.chartjs.org/"&gt;Chart.js&lt;/a&gt;), automatically update the data, and handle some UI to edit the graphs.&lt;/p&gt;
&lt;p&gt;Without leaving the webpage is it possible to add, edit and remove graphs, define which data is shown and how.&lt;/p&gt;
&lt;p&gt;There are some bugs still (&lt;em&gt;eh&lt;/em&gt;), but it works enough well for me.&lt;/p&gt;
&lt;p&gt;Some screenshots:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="16-matita-1.png"&gt; 
- &lt;em&gt;Matita showing a dashboard&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="16-matita-2.png"&gt;
- &lt;em&gt;Editing the &amp;ldquo;Memory&amp;rdquo; graph&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="16-matita-3.png"&gt;
- &lt;em&gt;The Dataset Browser. Remind you of anything?&lt;/em&gt;&lt;/p&gt;</content></entry><entry><title>This blog should support webmentions now</title><link href="https://kirgroup.net/blog/2026/01/27-webmentions.html" rel="alternate"/><published>2026-01-27T00:00:00+00:00</published><updated>2026-01-27T00:00:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2026-01-27:/blog/2026/01/27-webmentions.html</id><summary type="html">This blog should support webmentions now</summary><content type="html">&lt;p&gt;I&amp;rsquo;m looking at &lt;a href="https://indieweb.org/"&gt;IndieWeb&lt;/a&gt; and &lt;a href="https://indieweb.org/Webmention"&gt;webmentions&lt;/a&gt; for a while now, thinking about how to implement that on my site.&lt;/p&gt;
&lt;p&gt;A while ago I enable &lt;a href="https://indieauth.net/"&gt;IndieAuth&lt;/a&gt; but never got to use it really.&lt;/p&gt;
&lt;p&gt;After stumbling upon Aks&amp;rsquo; post &amp;ldquo;&lt;a href="https://akselmo.dev/posts/webmention-support/"&gt;My blog has now webmention support!&lt;/a&gt;&amp;rdquo; I decided time is come to have a look at this.&lt;/p&gt;
&lt;p&gt;Now this blog &lt;em&gt;should&lt;/em&gt; receive webmentions via &lt;a href="/https://webmention.io/"&gt;webmention.io&lt;/a&gt; (on which I logged in via IndieAuth with no problems!) and &lt;em&gt;should&lt;/em&gt; send webmentions via &lt;a href="https://webmention.app/"&gt;webmention.app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the future maybe I&amp;rsquo;ll write some code to support webmentions directly.&lt;/p&gt;
&lt;p&gt;I should also write a new post about how this blog is published, because things have changed from &lt;a href="/2025/01/10-not-so-static-blogging.html"&gt;my first post&lt;/a&gt;&lt;/p&gt;</content></entry><entry><title>Auto-activate Python virtualenv in bash on directory change</title><link href="https://kirgroup.net/blog/2025/10/09-bash-auto-activate-venv.html" rel="alternate"/><published>2025-10-09T00:00:00+00:00</published><updated>2025-10-09T00:00:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2025-10-09:/blog/2025/10/09-bash-auto-activate-venv.html</id><summary type="html">The unnecessary replacement of `cd` which automatically activate and deactivate Python virtualenvs.</summary><content type="html">&lt;p&gt;I like to develop unnecessary things, and often I like to develop them with Python.&lt;/p&gt;
&lt;p&gt;When I can, I try to develop with current Debian Python version and packaged libraries, but sometime this is impossible. Or it would be unnecessary difficult to do.&lt;/p&gt;
&lt;p&gt;I this cases I create a virtualenv, using Python &lt;code&gt;venv&lt;/code&gt; module:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(my virtualenvs are usually in &lt;code&gt;venv&lt;/code&gt; or &lt;code&gt;.venv&lt;/code&gt; folders)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I know, there are n+1 programs which handle virtualenv creation and so on, but using them require to have the correct tool installed. And, anyway, I said that this is unnecessary, did I?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I also made an alias to speed up this step:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;python3 -m&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;so that I can condense the command as&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;py&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But then another non-problem arisen: I need to remember to activate the virtualenv when I&amp;rsquo;m fiddling with the project and, more important, I need to remember to deactivate it when I change project.&lt;/p&gt;
&lt;p&gt;How many time I have run commands for &lt;code&gt;project_b&lt;/code&gt; inside the virtualenv of &lt;code&gt;project_a&lt;/code&gt;? Once too many.&lt;/p&gt;
&lt;p&gt;So I decided to do the only thing appropriate: an unnecessary replacement of &lt;code&gt;cd&lt;/code&gt; which automatically activate and deactivate Python virtualenvs.&lt;/p&gt;
&lt;p&gt;The script define a function which is then aliased to &lt;code&gt;cd&lt;/code&gt;.
After changing directory, the script search for a folder called &lt;code&gt;venv&lt;/code&gt; or &lt;code&gt;.venv&lt;/code&gt; in CWD or in parent directories (up to the home). If it finds one it activate it, not before deactivating the one currently active (if any).&lt;/p&gt;
&lt;p&gt;Obviously someone immediately declared the intent to create a git repo with a malicious &lt;code&gt;venv/bin/activate&lt;/code&gt; script and trick me to clone it.&lt;/p&gt;
&lt;p&gt;So I had to add a approve/block list functionality. (which may sound necessary, given the threat, but as nobody should use this script, is it still unnecessary, and thus approved by this Department)&lt;/p&gt;
&lt;p&gt;Now the script when find a virtualenv, check if it is in one of the two lists. If it is allowed, it activates it right away, if it is blocked it ignores it. If it is not either, it asks to the user what to do, and puts it in the correct list for future reference.&lt;/p&gt;
&lt;p&gt;It works? yes.&lt;br&gt;
It is necessary? no.&lt;br&gt;
Should anyone really use this? no.  &lt;/p&gt;
&lt;p&gt;Anyway, here is the script:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://paste.sr.ht/~fabrixxm/fe248f12bc7c190ddca59769e0be1f41d8f62a4e"&gt;https://paste.sr.ht/~fabrixxm/fe248f12bc7c190ddca59769e0be1f41d8f62a4e&lt;/a&gt;&lt;/p&gt;</content></entry><entry><title>Simulating Hand-Drawn Motion with SVG Filters Without JS</title><link href="https://kirgroup.net/blog/2025/07/21-SVG-Hand-Drawing-without-js.html" rel="alternate"/><published>2025-07-21T00:00:00+00:00</published><updated>2025-07-21T00:00:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2025-07-21:/blog/2025/07/21-SVG-Hand-Drawing-without-js.html</id><summary type="html">A small note on a practical guide to implementing the boiling line animation effect using SVG filter primitives, without JavaScript</summary><content type="html">&lt;p&gt;I stumbled on &lt;a href="https://camillovisini.com/coding/simulating-hand-drawn-motion-with-svg-filters" title="Simulating Hand-Drawn Motion with SVG Filters"&gt;this post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It describe a nice way to add an &amp;ldquo;Hand-Drawn Motion&amp;rdquo; effect to an image using SVG filters and JavaScript to animate filter parameters.&lt;/p&gt;
&lt;p&gt;The effect at the end is something like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src="example1.svg"&gt;&lt;/p&gt;
&lt;p&gt;(original image &lt;a href="https://openclipart.org/detail/314470/apple"&gt;Red Apple by cyanidecupcake&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;As the article explain, the animation is the result of a chain of two effects, &lt;code&gt;feTurbulence&lt;/code&gt; and &lt;code&gt;feDisplacementMap&lt;/code&gt;, and a bit of js to change &lt;code&gt;feTurbulence&lt;/code&gt; &lt;code&gt;baseFrequency&lt;/code&gt; attribute.&lt;/p&gt;
&lt;p&gt;While js allow maximum flexibility on the output result, the same effect can be achieved without a single line of js.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://caniuse.com/svg-smil"&gt;Can I Use&lt;/a&gt; says about SVG SMIL Animation&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since January 2020, this feature works across the latest devices and major browser versions&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So we can use SMIL to animate the &lt;code&gt;baseFrequency&lt;/code&gt; attribute directly in svg:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;http://www.w3.org/2000/svg&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;1.1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;400&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;400&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0 0 400 400&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;filter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;distortionFilter&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="cm"&gt;&amp;lt;!-- Step 1: Create a turbulence field to generate noise --&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;feTurbulence&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;turbulence&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;baseFrequency=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0.01&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;numOctaves=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;seed=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;result=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;noise&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;feTurbulence1&amp;quot;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="cm"&gt;&amp;lt;!-- Step 2: Use SMIL animation to loop between baseFrequency values --&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;turbolente-baseFrequency-anim&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;attributeName=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;baseFrequency&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;values=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0.01;0.025;0.015;0.03&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;calcMode=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;discrete&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;repeatCount=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;indefinite&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="na"&gt;dur=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;.9&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/feTurbulence&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="cm"&gt;&amp;lt;!-- Step 3: Use the noise (in2) to displace the image (in) --&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;feDisplacementMap&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;in=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;SourceGraphic&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;in2=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;noise&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;scale=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;5&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;xChannelSelector=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;R&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;yChannelSelector=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;G&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;feDisplacementMap1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/filter&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="cm"&gt;&amp;lt;!-- Step 4: Apply the filter to the image --&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;400&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;400&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;cherry.png&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="na"&gt;filter=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;url(#distortionFilter)&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src="cherry.png" style="filter:url(#distortionFilter)"&gt; &lt;svg xmlns="http://www.w3.org/2000/svg" version="1.1"&gt;
  &lt;defs&gt;
    &lt;filter id="distortionFilter"&gt;
      &lt;!-- Step 1: Create a turbulence field to generate noise --&gt;
      &lt;feTurbulence
         type="turbulence"
         baseFrequency="-0.02"
         numOctaves="2"
         seed="1"
         result="noise"
         id="feTurbulence1"&gt;
         &lt;!-- Step 2: Use SMIL animation to loop between baseFrequency values --&gt; 
        &lt;animate
           id="turbolente-baseFrequency-anim"
           attributeName="baseFrequency"
           values="0.01;0.025;0.015;0.03"
           calcMode="discrete"
           repeatCount="indefinite"
           dur=".9" /&gt;
      &lt;/feTurbulence&gt;
      &lt;!-- Step 3: Use the noise (in2) to displace the image (in) --&gt;
      &lt;feDisplacementMap
         in="SourceGraphic"
         in2="noise"
         scale="5"
         xChannelSelector="R"
         yChannelSelector="G"
         id="feDisplacementMap1" /&gt;
    &lt;/filter&gt;
  &lt;/defs&gt;
&lt;/svg&gt;&lt;/p&gt;</content></entry><entry><title>MPD su RPI II : Bluetooth</title><link href="https://kirgroup.net/blog/2025/01/21-mpd-su-rpi-2-bluetooth.html" rel="alternate"/><published>2025-01-21T18:11:00+00:00</published><updated>2025-01-21T18:11:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2025-01-21:/blog/2025/01/21-mpd-su-rpi-2-bluetooth.html</id><summary type="html">Audio via bluetooth da MPD</summary><content type="html">&lt;p&gt;Seconda parte della serie &amp;ldquo;MPD su RaspberryPI&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Avevo concluso la &lt;a href="10-mpd-su-rpi.html"&gt;prima parte&lt;/a&gt; con una serie di cose da provare, tra cui l&amp;rsquo;idea di recuperare un dongle usb e provare a collegare una cassa bluetooth.&lt;/p&gt;
&lt;p&gt;Mi è poi venuto in mente che è un Raspberry4, e che il bluetooth lo ha a bordo! E a casa ho una (brutta) lampada led multicolore con integrata cassa bluetooth!&lt;/p&gt;
&lt;h2&gt;Collegamela&lt;/h2&gt;
&lt;p&gt;Prima cosa, collegare il raspberry alla cassa. Il raspberry gira Debian headless: non ho schermo collegato, non ho un desktop. E quindi di base non ho installato quello che serve per il bluetooth. Installiamolo e verifichiamo che sia in escuzione&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;bluetooth
&lt;span class="gp"&gt;# &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;bluetoothd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ora possiamo provare a collegare la cassa:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;bluetoothctl
&lt;span class="go"&gt;Agent registered&lt;/span&gt;
&lt;span class="go"&gt;[CHG] Controller AA:AA:AA:AA:AA:AA Pairable: yes&lt;/span&gt;
&lt;span class="gp"&gt;[bluetooth]# &lt;/span&gt;scan&lt;span class="w"&gt; &lt;/span&gt;on
&lt;span class="go"&gt;Discovery started&lt;/span&gt;
&lt;span class="go"&gt;[CHG] Controller AA:AA:AA:AA:AA:AA Discovering: yes&lt;/span&gt;
&lt;span class="gp"&gt;[bluetooth]#&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;E aspettiamo. Quando viene rilevata la cassa, viene stampato il nome e l&amp;rsquo;indirizzo bt (6 gruppi di due cifre esadecimali separati da &lt;code&gt;:&lt;/code&gt;, come l&amp;rsquo;indirizzo del controller qui è &lt;code&gt;AA:AA:AA:AA:AA:AA&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;Una volta che abbiamo l&amp;rsquo;indirizzo possiamo collegarci:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;[bluetooth]# &lt;/span&gt;connect&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;indirizzo&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Il sistema darà una serie di messaggi, probabilmente qualche errore, e si collegherà.&lt;/p&gt;
&lt;p&gt;La cassa in questione non ha nessun tipo di autenticazione per il collegamento, non ho esperienze altrimenti.&lt;/p&gt;
&lt;p&gt;Ora la cassa è connessa, ma come ci mando l&amp;rsquo;audio? Avendo un ambiente desktop completo solitamente c&amp;rsquo;è in esecuzione PulseAudio o PipeWire che si occupano di gestire i device audio, anche bluethoot, ma in questo caso non ci sono e non ho intenzione di installare e configurare uno dei due solo per questo.&lt;/p&gt;
&lt;p&gt;Una ricerca sull&amp;rsquo;internet mi porta a trovare &lt;a href="https://github.com/arkq/bluez-alsa/tree/master"&gt;bluez-alsa&lt;/a&gt;. Il README spiega bene cosa fa e come configurarlo. Vediamo cosa ho fatto io.&lt;/p&gt;
&lt;p&gt;Installiamo:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;bluez-alsa-utils
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Questo installa le utility per bluez-alsa e automaticamente tutti quello che serve.&lt;/p&gt;
&lt;p&gt;Verifichiamo che sia in esecuzione&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;bluealsa
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Se è tutto ok, scolleghiamo e ricolleghiamo la cassa bluetooth&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;bluetoothctl
&lt;span class="go"&gt;[...]&lt;/span&gt;
&lt;span class="gp"&gt;[bluetooth]# &lt;/span&gt;disconnect&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;indirizzo&amp;gt;
&lt;span class="gp"&gt;[bluetooth]# &lt;/span&gt;connect&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;indirizzo&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;E se tutto va bene, possiamo riprodurre un wave sulla cassa bluetooth usando aplay:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;aplay&lt;span class="w"&gt; &lt;/span&gt;-D&lt;span class="w"&gt; &lt;/span&gt;bluealsa&lt;span class="w"&gt; &lt;/span&gt;/usr/share/sounds/alsa/Front_Center.wav
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;l&amp;rsquo;opzione &lt;code&gt;-D&lt;/code&gt; indica il nome del device alsa da usare, che sarà appunto &lt;code&gt;bluealsa&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Viene creato anche un controllo mixer sul device per regolare il volume, il cui nome dipende dall&amp;rsquo;aggeggio bluetooth a cui siamo collegati. Per vedere come si chiama usiamo &lt;code&gt;amixer&lt;/code&gt; , che si trova nel pacchetto &lt;code&gt;alsa-utils&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;alsa-utils
&lt;span class="gp"&gt;# &lt;/span&gt;amixer&lt;span class="w"&gt; &lt;/span&gt;-D&lt;span class="w"&gt; &lt;/span&gt;bluealsa
&lt;span class="go"&gt;ALSA lib bluealsa-ctl.c:205:(bluealsa_dev_fetch_battery) Couldn&amp;#39;t get device battery status: Object does not exist at path “/org/bluealsa/hci0/dev_BB_BB_BB_BB_BB_BB/rfcomm”&lt;/span&gt;
&lt;span class="go"&gt;Simple mixer control &amp;#39;SUPREMA-SKP - A2DP&amp;#39;,0&lt;/span&gt;
&lt;span class="go"&gt;  Capabilities: pvolume pswitch&lt;/span&gt;
&lt;span class="go"&gt;  Playback channels: Front Left - Front Right&lt;/span&gt;
&lt;span class="go"&gt;  Limits: Playback 0 - 127&lt;/span&gt;
&lt;span class="go"&gt;  Mono:&lt;/span&gt;
&lt;span class="go"&gt;  Front Left: Playback 127 [100%] [0.00dB] [on]&lt;/span&gt;
&lt;span class="go"&gt;  Front Right: Playback 127 [100%] [0.00dB] [on]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;La mia si chiama &lt;code&gt;SUPREMA-SKP - A2DP&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;Segniamoci il nome, che ci serve per il prossimo passaggio.&lt;/p&gt;
&lt;p&gt;Prima di continuare, però, facciamo in modo che la cassa sia ricollegata in automatico:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;bluetoothctl
&lt;span class="gp"&gt;[bluetooth]# &lt;/span&gt;trust&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;indirizzo&amp;gt;
&lt;span class="gp"&gt;[bluetooth]# &lt;/span&gt;pair&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;indirizzo&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Output MPD&lt;/h2&gt;
&lt;p&gt;Ora che abbiamo un device alsa, possiamo dire a MPD di usarlo come output.&lt;/p&gt;
&lt;p&gt;Questa è la configurazione dell&amp;rsquo;output in &lt;code&gt;/etc/mpd.conf&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;audio_output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;alsa&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Cassa BT&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;qui&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mettete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quello&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;che&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;volete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bluealsa&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;mixer_type&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;software&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;mixer_device&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bluealsa&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;mixer_control&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;lt;nome controllo mixer&amp;gt;&amp;quot;&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nx"&gt;mixer_index&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;optional&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Il &lt;code&gt;&amp;lt;nome controllo mixer&amp;gt;&lt;/code&gt; è quello che abbiamo trovato prima con &lt;code&gt;amixer&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Riavviamo MPD e colleghiamoci con un client, vedremo l&amp;rsquo;output disponibile. Possiamo mandare in play un brano e regolare anche il volume d&amp;rsquo;uscita 👍&lt;/p&gt;
&lt;p&gt;&lt;img alt="Parte dell'interfaccia di MyMPD che mostra gli output disponibili e il controllo di volume" src=".attachments.282812/mympd_output_volume.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;Possiamo abilitare solo un output o più di uno, MPD invierà l&amp;rsquo;audio a tutti gli output abilitati.&lt;/p&gt;
&lt;h2&gt;Cose da provare&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bluez-alsa&lt;/code&gt; permette anche il flusso inverso dell&amp;rsquo;audio, da un dispositivo remoto al device alsa di default. Non mi serve nella mia configurazione, ma è da provare!&lt;/li&gt;
&lt;/ul&gt;</content></entry><entry><title>DIY RaspberryPI homeserver dashboard - part 1</title><link href="https://kirgroup.net/blog/2025/01/16-DYI-RPI-homeserver-dashboard.html" rel="alternate"/><published>2025-01-16T00:00:00+00:00</published><updated>2025-01-16T00:00:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2025-01-16:/blog/2025/01/16-DYI-RPI-homeserver-dashboard.html</id><summary type="html">A simple HTML homepage for my RPI gets out of my hands</summary><content type="html">&lt;h2&gt;Some back story&lt;/h2&gt;
&lt;p&gt;I have a little server at home. It&amp;rsquo;s a RaspberryPI 4, with an m.2 SSD disk in an USB enclosure as system disk and two mechanical SSD laptop disks as data store, connected via an USB SATA dock.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not much, but it&amp;rsquo;s enough for me. It runs Debian 12 and it runs some services on my home network:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;samba, to share files on data disks&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pi-hole.net/"&gt;pi-hole&lt;/a&gt;, configured as DNS for my LAN&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.wireguard.com/"&gt;wireguard&lt;/a&gt; and &lt;a href="https://github.com/ngoduykhanh/wireguard-ui"&gt;wireguard-ui&lt;/a&gt; , to get to my server from outside&lt;/li&gt;
&lt;li&gt;&lt;a href="https://syncthing.net/"&gt;syncthing&lt;/a&gt; to sync files around my devices.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://restic.net/"&gt;restic&lt;/a&gt; to backup important data (most of which comes in via syncthing)&lt;/li&gt;
&lt;li&gt;an home-grown web-based media management system, to manage my photos, videos and other things &lt;a name="0"&gt;&lt;/a&gt;&lt;sup&gt;&lt;a href="#note-0"&gt;0&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://transmissionbt.com/"&gt;transmission daemon&lt;/a&gt; to download and seed those distro ISOs&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.musicpd.org/"&gt;MPD&lt;/a&gt; , which I&amp;rsquo;m &lt;a href="/blog/tags/"&gt;writting about&lt;/a&gt; (in italian)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.navidrome.org/"&gt;navidrome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.debian.org/minidlna"&gt;minidlna&lt;/a&gt; (I&amp;rsquo;m looking for the perfect way to listen to my music. I&amp;rsquo;m testing various alternatives&amp;hellip;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of this services have a web interface which in my network are served on dedicated subdomains of &lt;code&gt;casa.rpi&lt;/code&gt; . All of those are resolved by pi-hole to my RPI.&lt;/p&gt;
&lt;h2&gt;How it started&lt;/h2&gt;
&lt;p&gt;I had lighttpd doing his job as proxy to serve services subdomains, but main domain was sitting here, empty.&lt;/p&gt;
&lt;p&gt;I then thought I could write a simple html page with links to various services, as an entry point.&lt;/p&gt;
&lt;p&gt;It was a quick page to build: a icon (service favicon linked directly) and a link for each services, some quick css and it was done.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The page as it was" src=".attachments.282862/comera.jpg"&gt;&lt;/p&gt;
&lt;p&gt;And I didn&amp;rsquo;t touch it for some years.&lt;/p&gt;
&lt;h2&gt;The rewrite&lt;/h2&gt;
&lt;p&gt;As usual, things starts with a simple need.&lt;/p&gt;
&lt;p&gt;The page color scheme was fixed and dark, and I wanted it to follow system preferences for light/dark scheme.&lt;/p&gt;
&lt;p&gt;So I replaced most of the CSS of the page with &lt;a href="https://picocss.com/"&gt;picocss&lt;/a&gt; , a simple CSS framework which comes with dark/light color scheme in a 81K minified css file.&lt;/p&gt;
&lt;p&gt;After some fiddling with html and some custom css this is how it become:&lt;/p&gt;
&lt;p&gt;&lt;img alt="New page with light/dark styles" src=".attachments.282862/pagenew_1%20%282%29.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Same overall layout, better visual outcome (imho) and dark/light support.&lt;/p&gt;
&lt;p&gt;Links animate on mouseover/click, and gets focus outline.&lt;/p&gt;
&lt;video src=".attachments.282862/pagenew_b.mp4" controls&gt;&lt;/video&gt;

&lt;p&gt;I like it 🙂 &lt;/p&gt;
&lt;p&gt;As said, icons comes from service web interface favicon. This has the nice side effect that if the service is down, icon can&amp;rsquo;t be load and become a quick visual clue that something doesn&amp;rsquo;t work.&lt;/p&gt;
&lt;p&gt;A nice touch is the logo image: it&amp;rsquo;s an SVG directly embedded into the page inside the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, without any fill color defined, which is then set by picocss. This way the logo follows the color scheme!&lt;/p&gt;
&lt;p&gt;I could have stopped here, but if I had, you wouldn&amp;rsquo;t be reading this post.&lt;/p&gt;
&lt;h2&gt;Stats!&lt;/h2&gt;
&lt;p&gt;So I looked at this page and I thought: wouldn&amp;rsquo;t be nice to have some insight on how the system is doing just in this page?&lt;/p&gt;
&lt;p&gt;So here we go collecting stats!&lt;/p&gt;
&lt;p&gt;The idea is to install &lt;code&gt;collectd&lt;/code&gt; and use &lt;code&gt;rrdtool&lt;/code&gt; to generate some graphs and then show the grap in the page.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;collectd
&lt;span class="gp"&gt;# &lt;/span&gt;apd&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;rrdtool
&lt;span class="gp"&gt;# &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;collectd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By default &lt;code&gt;collectd&lt;/code&gt; collect more stats than we need, but for now we leave the configuration as it is. &lt;/p&gt;
&lt;p&gt;To create the graph I&amp;rsquo;m using &lt;code&gt;rrdtool graph&lt;/code&gt; command. It&amp;rsquo;s not easy to use, but with some &lt;a href="https://oss.oetiker.ch/rrdtool/tut/index.en.html"&gt;tutorials&lt;/a&gt; and the &lt;a href="https://oss.oetiker.ch/rrdtool/doc/rrdgraph_graph.en.html"&gt;documentation&lt;/a&gt;, I ended up with some nice results.&lt;/p&gt;
&lt;p&gt;My page is &lt;code&gt;/var/www/html/index.html&lt;/code&gt;. I will put my graphs in subfolder &lt;code&gt;stats/&lt;/code&gt; , with a bash script to generate them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;yes, the script will be readable via webserver, but this web server is not on the internet. Everything I&amp;rsquo;m doing here should not be done on a public-facing server&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;rrdtool&lt;/code&gt; reads data from &lt;code&gt;.rrd&lt;/code&gt; files. &lt;code&gt;collectd&lt;/code&gt; write collected data to &lt;code&gt;.rrd&lt;/code&gt; files in &lt;code&gt;/var/lib/collectd/rrd/localhost&lt;/code&gt;. Here there is a folder for each plugins, and inside those an &lt;code&gt;.rrd&lt;/code&gt; folder for each metric.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;/var/lib/collectd/rrd/localhost
&lt;span class="go"&gt;cpu-0&lt;/span&gt;
&lt;span class="go"&gt;cpu-1&lt;/span&gt;
&lt;span class="go"&gt;cpu-2&lt;/span&gt;
&lt;span class="go"&gt;cpu-3&lt;/span&gt;
&lt;span class="go"&gt;cpufreq-0&lt;/span&gt;
&lt;span class="go"&gt;cpufreq-1&lt;/span&gt;
&lt;span class="go"&gt;cpufreq-2&lt;/span&gt;
&lt;span class="go"&gt;cpufreq-3&lt;/span&gt;
&lt;span class="go"&gt;cpusleep&lt;/span&gt;
&lt;span class="go"&gt;df-boot-firmware&lt;/span&gt;
&lt;span class="go"&gt;df-media-sda1&lt;/span&gt;
&lt;span class="go"&gt;df-media-sdb1&lt;/span&gt;
&lt;span class="go"&gt;df-root&lt;/span&gt;
&lt;span class="go"&gt;[...]&lt;/span&gt;
&lt;span class="go"&gt;interface-eth0&lt;/span&gt;
&lt;span class="go"&gt;interface-lo&lt;/span&gt;
&lt;span class="go"&gt;interface-wg0&lt;/span&gt;
&lt;span class="go"&gt;interface-wlan0&lt;/span&gt;
&lt;span class="go"&gt;irq&lt;/span&gt;
&lt;span class="go"&gt;load&lt;/span&gt;
&lt;span class="go"&gt;memory&lt;/span&gt;
&lt;span class="go"&gt;[...]&lt;/span&gt;
&lt;span class="gp"&gt;# &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;/var/lib/collectd/rrd/localhost/cpu-0
&lt;span class="go"&gt;cpu-idle.rrd&lt;/span&gt;
&lt;span class="go"&gt;cpu-interrupt.rrd&lt;/span&gt;
&lt;span class="go"&gt;cpu-nice.rrd&lt;/span&gt;
&lt;span class="go"&gt;cpu-softirq.rrd&lt;/span&gt;
&lt;span class="go"&gt;cpu-steal.rrd&lt;/span&gt;
&lt;span class="go"&gt;cpu-system.rrd&lt;/span&gt;
&lt;span class="go"&gt;cpu-user.rrd&lt;/span&gt;
&lt;span class="go"&gt;cpu-wait.rrd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each &lt;code&gt;.rrd&lt;/code&gt; file contains timestamped data for one or more values. We can inspect them with &lt;code&gt;rrdtool info &amp;lt;file.rrd&amp;gt;`&lt;/code&gt; &lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/var/lib/collectd/rrd/localhost
&lt;span class="gp"&gt;# &lt;/span&gt;rrdtool&lt;span class="w"&gt; &lt;/span&gt;info&lt;span class="w"&gt; &lt;/span&gt;cpu-0/cpu-user.rrd
&lt;span class="go"&gt;filename = &amp;quot;cpu-0/cpu-user.rrd&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;rrd_version = &amp;quot;0003&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;step = 10&lt;/span&gt;
&lt;span class="go"&gt;last_update = 1737033603&lt;/span&gt;
&lt;span class="go"&gt;header_size = 3496&lt;/span&gt;
&lt;span class="go"&gt;ds[value].index = 0&lt;/span&gt;
&lt;span class="go"&gt;ds[value].type = &amp;quot;DERIVE&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;ds[value].minimal_heartbeat = 20&lt;/span&gt;
&lt;span class="go"&gt;ds[value].min = 0,0000000000e+00&lt;/span&gt;
&lt;span class="go"&gt;ds[value].max = NaN&lt;/span&gt;
&lt;span class="go"&gt;ds[value].last_ds = &amp;quot;832605&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;ds[value].value = 2,1000000000e+00&lt;/span&gt;
&lt;span class="go"&gt;ds[value].unknown_sec = 0&lt;/span&gt;
&lt;span class="go"&gt;rra[0].cf = &amp;quot;AVERAGE&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;rra[0].rows = 1200&lt;/span&gt;
&lt;span class="go"&gt;rra[0].cur_row = 258&lt;/span&gt;
&lt;span class="go"&gt;rra[0].pdp_per_row = 1&lt;/span&gt;
&lt;span class="go"&gt;rra[0].xff = 1,0000000000e-01&lt;/span&gt;
&lt;span class="go"&gt;rra[0].cdp_prep[0].value = NaN&lt;/span&gt;
&lt;span class="go"&gt;rra[0].cdp_prep[0].unknown_datapoints = 0&lt;/span&gt;
&lt;span class="go"&gt;rra[1].cf = &amp;quot;MIN&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;rra[1].rows = 1200&lt;/span&gt;
&lt;span class="go"&gt;rra[1].cur_row = 177&lt;/span&gt;
&lt;span class="go"&gt;rra[1].pdp_per_row = 1&lt;/span&gt;
&lt;span class="go"&gt;rra[1].xff = 1,0000000000e-01&lt;/span&gt;
&lt;span class="go"&gt;rra[1].cdp_prep[0].value = NaN&lt;/span&gt;
&lt;span class="go"&gt;rra[1].cdp_prep[0].unknown_datapoints = 0&lt;/span&gt;
&lt;span class="go"&gt;rra[2].cf = &amp;quot;MAX&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;rra[2].rows = 1200&lt;/span&gt;
&lt;span class="go"&gt;rra[2].cur_row = 456&lt;/span&gt;
&lt;span class="go"&gt;rra[2].pdp_per_row = 1&lt;/span&gt;
&lt;span class="go"&gt;rra[2].xff = 1,0000000000e-01&lt;/span&gt;
&lt;span class="go"&gt;rra[2].cdp_prep[0].value = NaN&lt;/span&gt;
&lt;span class="go"&gt;rra[2].cdp_prep[0].unknown_datapoints = 0&lt;/span&gt;
&lt;span class="go"&gt;[...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&amp;hellip; and many more lines. What we are looking for here is &lt;code&gt;ds[value]&lt;/code&gt;. This file has one variable, named &lt;code&gt;value&lt;/code&gt;.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;rrdtool&lt;span class="w"&gt; &lt;/span&gt;info&lt;span class="w"&gt; &lt;/span&gt;interface-eth0/if_packets.rrd
&lt;span class="go"&gt;filename = &amp;quot;interface-eth0/if_packets.rrd&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;rrd_version = &amp;quot;0003&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;step = 10&lt;/span&gt;
&lt;span class="go"&gt;last_update = 1737034003&lt;/span&gt;
&lt;span class="go"&gt;header_size = 4928&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].index = 0&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].type = &amp;quot;DERIVE&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].minimal_heartbeat = 20&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].min = 0,0000000000e+00&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].max = NaN&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].last_ds = &amp;quot;40872202&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].value = 1,0290000000e+02&lt;/span&gt;
&lt;span class="go"&gt;ds[rx].unknown_sec = 0&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].index = 1&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].type = &amp;quot;DERIVE&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].minimal_heartbeat = 20&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].min = 0,0000000000e+00&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].max = NaN&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].last_ds = &amp;quot;73397119&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].value = 9,1800000000e+01&lt;/span&gt;
&lt;span class="go"&gt;ds[tx].unknown_sec = 0&lt;/span&gt;
&lt;span class="go"&gt;rra[0].cf = &amp;quot;AVERAGE&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;rra[0].rows = 1200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here we have two variables: &lt;code&gt;tx&lt;/code&gt; and &lt;code&gt;rx&lt;/code&gt; &lt;/p&gt;
&lt;h2&gt;Graphs!&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s graph our stats. We start with some basic data: system load, memory usage and number of processes.&lt;/p&gt;
&lt;p&gt;We create the script &lt;code&gt;/var/www/html/stats/makegraphs.sh&lt;/code&gt;, and make it executable.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;BASERRD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/lib/collectd/rrd/localhost&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# system load:&lt;/span&gt;
&lt;span class="c1"&gt;# we want three lines for short / mid / long term load&lt;/span&gt;
rrdtool&lt;span class="w"&gt; &lt;/span&gt;graph&lt;span class="w"&gt; &lt;/span&gt;load.png&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--lazy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--title&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Load&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:s=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/load/load.rrd:shortterm:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LINE1:s#1a5fb4:Short term&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:s:LAST:%5.2lf&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:m=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/load/load.rrd:midterm:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LINE2:m#e5a50a:Mid term&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:m:LAST:%5.2lf&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:l=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/load/load.rrd:longterm:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LINE1:l#9141ac:Long term&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:l:LAST:%5.2lf&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I will let you read the documentation for the juicy details of this command, but in short: we ask &lt;code&gt;rrdtool&lt;/code&gt; to create a graph with titled &amp;ldquo;Load&amp;rdquo;, with data from &lt;code&gt;load/load.rrd&lt;/code&gt;, first from variable &lt;code&gt;shortterm&lt;/code&gt;. We then ask to draw it as a line, with color &lt;code&gt;#1a5fb4&lt;/code&gt; and label &amp;ldquo;Short term&amp;rdquo;.  A legend entry will be added  automatically. Then we write the last value registered for the variable, with a format string: &lt;code&gt;%5.2lf&lt;/code&gt; will print the number with a width of 5 digits and a precision of 2. We repeat the code for the other variables, changing the color the label and the reference name (here &lt;code&gt;s&lt;/code&gt; for short , &lt;code&gt;m&lt;/code&gt; for mid and &lt;code&gt;l&lt;/code&gt; for long). We ask &lt;code&gt;rrdtool&lt;/code&gt; to save the result as &lt;code&gt;load.png&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is the result:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src=".attachments.282862/load.png"&gt;&lt;/p&gt;
&lt;p&gt;Colors are chosen from the &lt;a href="https://developer.gnome.org/hig/reference/palette.html"&gt;GNOME HIG palette&lt;/a&gt; . Easy 🙂 &lt;/p&gt;
&lt;p&gt;Next we draw the memory usage:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rrdtool&lt;span class="w"&gt; &lt;/span&gt;graph&lt;span class="w"&gt; &lt;/span&gt;memory.png&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--lazy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--lower-limit&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--title&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Memory&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:umem=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/memory/memory-used.rrd:value:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;AREA:umem#e01b24:Used&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:umem:LAST:%5.2lf%s&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:fmem=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/memory/memory-free.rrd:value:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;STACK:fmem#33d17a:Free&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:fmem:LAST:%5.2lf%s&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Basically it&amp;rsquo;s the same, here we draw used memory and free memory, whose data came from two different files, both with variable named &lt;code&gt;value&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;Two main differences here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the format in &lt;code&gt;GPRINT&lt;/code&gt; command has a &lt;code&gt;%s&lt;/code&gt; added at the end. This will make rrdtool to print the vaule with the appropriate SI magnitude unit and the value will be scaled accordingly (&lt;code&gt;123456&lt;/code&gt; becomes &lt;code&gt;123.47 k&lt;/code&gt;). &lt;/li&gt;
&lt;li&gt;Instead of lines, we plot stacked areas. This will show better visually how much of the total is taken by the values&lt;/li&gt;
&lt;li&gt;a new option &lt;code&gt;--lower-limit 0&lt;/code&gt; to tell &lt;code&gt;rrdtool&lt;/code&gt; to start the Y axis from 0 instead of automatically choose a lower limit based on the data. Again, this will show better how much memory the two areas are covering.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="" src=".attachments.282862/memory.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(I&amp;rsquo;m not plotting all the memory-related values, so the total area is not at 100%)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Last is a graph with processes. Which data to plot has been choose at random, I&amp;rsquo;m sure there are more useful things to know, but here it is:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rrdtool&lt;span class="w"&gt; &lt;/span&gt;graph&lt;span class="w"&gt; &lt;/span&gt;processes.png&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--lazy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;--title&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Processes&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:r=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/processes/ps_state-running.rrd:value:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LINE1:r#33d17a:Running&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:r:LAST:%5.2lf&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:s=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/processes/ps_state-sleeping.rrd:value:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LINE2:s#3584e4:Sleeping&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:s:LAST:%5.2lf&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DEF:z=&lt;/span&gt;&lt;span class="nv"&gt;$BASERRD&lt;/span&gt;&lt;span class="s2"&gt;/processes/ps_state-zombies.rrd:value:AVERAGE&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LINE1:z#e01b24:Zombies&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPRINT:z:LAST:%5.2lf&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is just like the load graph, nothing too fancy.&lt;/p&gt;
&lt;p&gt;We run the script from the folder, it generates the images. Then we add the images to the page.&lt;/p&gt;
&lt;p&gt;For a clean look, I decided to put the graphs inside a &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; element, which is collapsed by default:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;stats&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;System&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;stats/load.png&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;stats/memory.png&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;stats/processes.png&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;details&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A bit of css will position the images nicely on the page.&lt;/p&gt;
&lt;p&gt;With the same process I&amp;rsquo;ve added also graphs for disks space usage and network packets count.&lt;/p&gt;
&lt;p&gt;The result is this page:&lt;/p&gt;
&lt;p&gt;&lt;img alt="" src=".attachments.282862/pagenew_2.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Nice!&lt;/p&gt;
&lt;p&gt;With this we end the first part about my DIY homeserver dashboard.&lt;/p&gt;
&lt;p&gt;Next up: auto-update the graphs and &lt;em&gt;moar stats!!!1!&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Footnotes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a name="note-0"&gt;&lt;/a&gt;
&lt;a href="#0"&gt;^0&lt;/a&gt;: Code is available, but I&amp;rsquo;m not linking it because it&amp;rsquo;s not really ready to be used&lt;/p&gt;</content></entry><entry><title>MPD su RaspberryPI</title><link href="https://kirgroup.net/blog/2025/01/10-mpd-su-rpi.html" rel="alternate"/><published>2025-01-10T00:00:00+00:00</published><updated>2025-01-10T00:00:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2025-01-10:/blog/2025/01/10-mpd-su-rpi.html</id><summary type="html">Installazione di MPD su raspberry con output su stream http</summary><content type="html">&lt;h2&gt;Cosa serve&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;un raspberrypi (testato su 4)&lt;/li&gt;
&lt;li&gt;debian (testato con debian 12 aarch64&lt;/li&gt;
&lt;li&gt;podman (opzionale)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Podman per l&amp;rsquo;interfaccia web. Si puo&amp;rsquo; fare senza, ma non l&amp;rsquo;ho testato.&lt;/p&gt;
&lt;h2&gt;MPD&lt;/h2&gt;
&lt;h3&gt;Installazione&lt;/h3&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;mpd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Configurazione&lt;/h3&gt;
&lt;p&gt;editiamo &lt;code&gt;/etc/mpd.conf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Ecco cosa ho modificato nel mio file di configurazione.
Se erano commentati (la riga cominciava con &lt;code&gt;#&lt;/code&gt;), sono stati decommentati&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;music_directory     &amp;quot;/media/sda1/Musica&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;questa è la cartella che contiene la mia musica&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;bind_to_address&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0.0.0.0&amp;quot;&lt;/span&gt;
&lt;span class="nx"&gt;bind_to_address&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/run/mpd/socket&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Il primo è l&amp;rsquo;indirizzo di rete su cui risponderà mpd, &lt;code&gt;0.0.0.0&lt;/code&gt; per rispondere su tutte le interfacce di rete.&lt;/p&gt;
&lt;p&gt;Il secondo per potersi collegare via socket locale. Servirà per l&amp;rsquo;interfaccia web.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;zeroconf_enabled        &amp;quot;yes&amp;quot;
zeroconf_name           &amp;quot;Music Player @ %h&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sul mio raspberry ho installato Avahi, quindi ho abilitato le opzioni. Non ho ancora visto se funziona&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;audio_output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;httpd&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;My HTTP Stream&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;lame&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;vorbis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;lame&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;8000&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;bind_to_address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0.0.0.0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IPv6&lt;/span&gt;
&lt;span class="err"&gt;##&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;5.0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;bitrate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;defined&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;bitrate&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;128&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;defined&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;44100:16:1&amp;quot;&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nx"&gt;max_clients&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;optional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Qui abilito l&amp;rsquo;output via stream http, usando il server di streaming http integrato in mpd.&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;encoder è impostato a &amp;ldquo;lame&amp;rdquo; per avere lo stream in mp3. Dai miei test, il client per android &lt;a href="https://gitlab.com/gateship-one/malp"&gt;malp&lt;/a&gt; funziona molto meglio con lo stream in mp3.
Io ho deciso di impostare il bitrate a 128, vedete voi.&lt;/p&gt;
&lt;p&gt;Non ho impostato una password nel file di configurazione perché il mio mpd sarà raggiungibile solo in rete locale a casa.&lt;/p&gt;
&lt;h3&gt;Avvio&lt;/h3&gt;
&lt;p&gt;Configurato, non resta che avviare il servizio&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;mpd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A questo punto dovreste potervi collegare all&amp;rsquo;indirizzo di controllo:&lt;/p&gt;
&lt;p&gt;http://vostroserver:6600&lt;/p&gt;
&lt;p&gt;che dovrebbe rispondere qualcosa tipo&lt;/p&gt;
&lt;p&gt;&lt;code&gt;OK MPD 0.23.5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;e anche all&amp;rsquo;indirizzo dello streaming&lt;/p&gt;
&lt;p&gt;http://vostroserver:8000&lt;/p&gt;
&lt;p&gt;che non risponde perché non c&amp;rsquo;è nessun brano in esecuzione.&lt;/p&gt;
&lt;h2&gt;Client android malp&lt;/h2&gt;
&lt;p&gt;mapl è installabile da f-droid.&lt;/p&gt;
&lt;p&gt;Per prima chiede di creare il profilo. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nome profilo: quello che volete&lt;/li&gt;
&lt;li&gt;Hostname or ip: indirizzo del vostro server&lt;/li&gt;
&lt;li&gt;Port: 6600&lt;/li&gt;
&lt;li&gt;Password: vuoto, non ho impostato una password&lt;/li&gt;
&lt;li&gt;Enable streaming from serve: abilitato&lt;/li&gt;
&lt;li&gt;Streaming url: http://vostroserver:8000&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A questo punto si può andare su Library, cercare una canzone, selezionarla pigiare &amp;ldquo;play&amp;rdquo;.
Dovrebbe partire l&amp;rsquo;indicatore di avanzamento ma non si sente nulla.&lt;/p&gt;
&lt;p&gt;Aprendo il player, menu &lt;code&gt;⋮&lt;/code&gt; si seleziona &amp;ldquo;star streaming&amp;rdquo; e si aspetta. dopo una decina di secondi dovrebbe partire l&amp;rsquo;audio.
Si, bisogna farlo ogni volta.&lt;/p&gt;
&lt;h2&gt;Client web&lt;/h2&gt;
&lt;p&gt;Come client web usiamo &lt;a href="https://github.com/jcorporation/myMPD"&gt;myMPD&lt;/a&gt;,
la documentazione è qui: https://jcorporation.github.io/myMPD/&lt;/p&gt;
&lt;p&gt;Per l&amp;rsquo;installazione ci sono tre opzioni:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Docker/Podman&lt;/li&gt;
&lt;li&gt;Loro repository Debian&lt;/li&gt;
&lt;li&gt;Compilazione da sorgente&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La compilazione non sembra complicata, ma per questa volta ho scelto di usare Podman, che ho già installato sul raspino per altre cose (pihole).&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;interfaccia web sarà raggiungibile all&amp;rsquo;indirizzo&lt;/p&gt;
&lt;p&gt;htttp://vostroserver:6680/&lt;/p&gt;
&lt;h3&gt;installazione&lt;/h3&gt;
&lt;p&gt;Per tenere le cose in ordine, creo una cartella &lt;code&gt;/opt/mympd&lt;/code&gt; dove ci metto quello che serve&lt;/p&gt;
&lt;p&gt;Creo lo script bash che avvierà il container: &lt;code&gt;/opt/mympd/run.sh&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;mympd&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# girerà come utente pi:pi, che è il mio utente sul raspi&lt;/span&gt;
&lt;span class="c1"&gt;# e che ha accesso alla cartella musica&lt;/span&gt;
&lt;span class="nv"&gt;RUNAS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1000:1000&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# creo le due cartelle per salvare configurazione e cache tra i riavvi&lt;/span&gt;
&lt;span class="c1"&gt;# del container&lt;/span&gt;
&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="k"&gt;:-$(&lt;/span&gt;dirname&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$MYMPD_BASE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$MYMPD_BASE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Couldn&amp;#39;t create storage directory: &lt;/span&gt;&lt;span class="nv"&gt;$MYMPD_BASE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/workdir&amp;quot;&lt;/span&gt;
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cachedir&amp;quot;&lt;/span&gt;

chown&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$RUNAS&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/workdir&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cachedir&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# avvio il container&lt;/span&gt;
podman&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6680&lt;/span&gt;:8080&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;:1000&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;UMASK_SET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;022&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MYMPD_SSL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;MYMPD_HTTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;/run/mpd:/run/mpd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/workdir:/var/lib/mympd&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MYMPD_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/cachedir:/var/cache/mympd&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/media/sda1/Musica:/media/sda1/Musica:ro&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/lib/mpd/playlists:/var/lib/mpd/playlists:ro&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ghcr.io/jcorporation/mympd/mympd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠ OCCHIO!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ricodarsi di modificare  &lt;code&gt;"/media/sda1/Musica":/media/sda1/Musica:ro"&lt;/code&gt;
con il percorso alla giusta cartella musica&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;rendo lo script eseguibile&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;run.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;creo il service unit per systemd: &lt;code&gt;/opt/mympd/mympd.service&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Web-based MPD client&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network-online.target mpd.service&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;TimeoutStartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/bin/bash /opt/mympd/run.sh %n&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Lo installo nel sistema e lo avvio&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/opt/opt/mympd/mympd.service&lt;span class="w"&gt; &lt;/span&gt;/etc/systemd/system/
&lt;span class="gp"&gt;# &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;daemon-reload
&lt;span class="gp"&gt;# &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;mympd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ora la vostra interfaccia dovrebbe essere raggiungibile su&lt;/p&gt;
&lt;p&gt;&lt;code&gt;http://vostroserver:6680&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Configurazione&lt;/h3&gt;
&lt;p&gt;myMPD si collega a mpd via socket unix, quindi non c&amp;rsquo;è bisogno di dirgli dove trovare mpd, pero&amp;rsquo; possiamo dirgli dove trovare lo stream e di farlo partire in automatico quando c&amp;rsquo;è qualche brano in esecuzione&lt;/p&gt;
&lt;p&gt;Facciamo: menu (icona in alto a sx) -&amp;gt; Impostazioni -&amp;gt; Caratteristiche&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Riproduzione locale&lt;ul&gt;
&lt;li&gt;Abilita: check&lt;/li&gt;
&lt;li&gt;Porta del flusso: 8000&lt;/li&gt;
&lt;li&gt;URI del flusso: http://vostroserver:8000/&lt;/li&gt;
&lt;li&gt;Riproduzione automatica: check&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Poi pigiamo &amp;ldquo;Salva&amp;rdquo; in fondo alla finestra.&lt;/p&gt;
&lt;p&gt;Il resto delle impostazioni a discrezione.&lt;/p&gt;
&lt;h2&gt;Cose da provare&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;installare l&amp;rsquo;interfaccia web da sorgente&lt;/li&gt;
&lt;li&gt;configurare un secondo stream con un bitrate minore, potrebbe essere utile per quando sono collegato via VPN su rete cellulare. C&amp;rsquo;è da dire che non ho trovato un client che permetta di eseguire uno stream semplicemente scegliendolo come output&lt;/li&gt;
&lt;li&gt;recuperare un dongle bluetooth usb, collegare il raspino alla cassa bluetooth e configurarla come output di mpd&lt;/li&gt;
&lt;/ul&gt;</content></entry><entry><title>Not-so-static blogging with blag, Nextcloud and systemd</title><link href="https://kirgroup.net/blog/2025/01/10-not-so-static-blogging.html" rel="alternate"/><published>2025-01-10T00:00:00+00:00</published><updated>2025-01-10T00:00:00+00:00</updated><author><name>Fabrixxm</name></author><id>tag:kirgroup.net,2025-01-10:/blog/2025/01/10-not-so-static-blogging.html</id><summary type="html">How this blog is here</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;&lt;br&gt;
Very outdated post!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I always wanted to write a blog, but I never really know what about to write. But most importantly, I never wanted to manage Wordpress or whatever on this machine.&lt;/p&gt;
&lt;p&gt;I like the idea of static blogging, but yet, I like also to have a more friendly editor than a plain text editor.&lt;/p&gt;
&lt;p&gt;During the years I created some stupid project that I worte about on my &lt;a href="https://social.gl-como.it/~fabrixxm"&gt;fediverse page&lt;/a&gt; under the title of &amp;ldquo;Departement of Unnecessary Projects&amp;rdquo;.
Why don&amp;rsquo;t write about that on the blog?&lt;/p&gt;
&lt;p&gt;And what&amp;rsquo;s better than a Unnecessary Project for the blog?&lt;/p&gt;
&lt;h2&gt;Stack&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m already running Nextcloud on this same vps. I found myself sharing a quick &amp;ldquo;howto&amp;rdquo; via Nextcloud Notes. It was easy to write, easy to share and good looking.&lt;/p&gt;
&lt;p&gt;So I thought: won&amp;rsquo;t be nice to have something which get markdown files from Notes and build a blog out of them?&lt;/p&gt;
&lt;p&gt;Here is what I come up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blag.readthedocs.io/"&gt;blag&lt;/a&gt; as static site generator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s the first thing came up searching for static site generators packaged in debian. It&amp;rsquo;s python (which is a plus) and is very simple. I know &lt;a href="https://getpelican.com/"&gt;Pelican&lt;/a&gt;, but for a quick hack blag is more than enough.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apps.nextcloud.com/apps/notes"&gt;Nextcloud Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Markdown with preview, each note in a &lt;code&gt;.md&lt;/code&gt; file, categories as folder on disk, upload an insert images and save while typing. I mean, what do you want more?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;systemd&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For that added magic&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a web server&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s a &amp;ldquo;&lt;em&gt;we&lt;/em&gt;b log&amp;rdquo; after all. Nginx here, but not so important. Everything was already configured, it&amp;rsquo;s just a subfolder in webroot&lt;/p&gt;
&lt;h2&gt;How things are connected&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;blag&lt;/code&gt; reads content in markdown from folder &lt;code&gt;/Notes/blog&lt;/code&gt; of my Nextcloud account, directly from disk and write output to &lt;code&gt;/var/www/html/blog&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In Nextcloud I created two categories, &lt;code&gt;blog&lt;/code&gt; for live articles and pages, and &lt;code&gt;blog-drafts&lt;/code&gt; for things I&amp;rsquo;m still writting.&lt;/p&gt;
&lt;p&gt;I like to keep things (almost) tidy, so I created &lt;code&gt;/opt/blag&lt;/code&gt; to put everything related to this thing.&lt;/p&gt;
&lt;p&gt;First, I installed &lt;code&gt;blag&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;blag
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;then, as per documentation:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/opt/blag
&lt;span class="gp"&gt;# &lt;/span&gt;blag&lt;span class="w"&gt; &lt;/span&gt;quickstart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This asked me some questions and created &lt;code&gt;config.ini&lt;/code&gt; file&lt;/p&gt;
&lt;p&gt;I then created a bash script &lt;code&gt;/opt/blag/build.sh&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="nv"&gt;HERE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;dirname&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HERE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

rm&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;/var/www/kirgroup.net/blog/*

blag&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;/path/to/nextcloud/fabrixxm/files/Notes/blog&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;/var/www/html/blog/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The script first cds into the folder, to allow blag to find its configuration, then cleanup output folder (it&amp;rsquo;s the fastest way to remove deleted pages/articles)&lt;/p&gt;
&lt;p&gt;Now if I create a note in &lt;code&gt;blog&lt;/code&gt; category (with required &lt;a href="https://blag.readthedocs.io/en/latest/manual/#metadata"&gt;metadata&lt;/a&gt;) and I run &lt;code&gt;/opt/blag/build.sh&lt;/code&gt; I get my blog.&lt;/p&gt;
&lt;p&gt;But running build via ssh after editing is not what I want. So, let&amp;rsquo;s&amp;hellip;&lt;/p&gt;
&lt;h2&gt;Automate the thing&lt;/h2&gt;
&lt;p&gt;Systemd can be configured to watch a folder or a file and start a service if something changes.&lt;/p&gt;
&lt;p&gt;We need a &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html"&gt;path unit&lt;/a&gt;.  So we now write &lt;code&gt;/opt/blag/blag-rebuild.path&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Watch blog source for changes&lt;/span&gt;

&lt;span class="k"&gt;[Path]&lt;/span&gt;
&lt;span class="na"&gt;PathModified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/path/to/nextcloud/fabrixxm/files/Notes/blog&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we need a service to run &lt;code&gt;build.sh&lt;/code&gt;.
If a &lt;code&gt;.service&lt;/code&gt; has the same name as the &lt;code&gt;.path&lt;/code&gt; unit, systemd knows what to do.&lt;/p&gt;
&lt;p&gt;Here is &lt;code&gt;/opt/blag/blag-rebuild.service&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Rebuild blog&lt;/span&gt;
&lt;span class="na"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;network.target&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;oneshot&lt;/span&gt;
&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/blag/build.sh&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;RequiredBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;blag-rebuild.path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Eeasy-peasy.&lt;/p&gt;
&lt;p&gt;Now we install the units and start the watching&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;# &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/opt/blag/blag-rebuild.service&lt;span class="w"&gt; &lt;/span&gt;/etc/systemd/system
&lt;span class="gp"&gt;# &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/opt/blag/blag-rebuild.path&lt;span class="w"&gt; &lt;/span&gt;/etc/systemd/system
&lt;span class="gp"&gt;# &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;blag-rebuild.path
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The attentive reader will have noticed that the &lt;code&gt;blag build&lt;/code&gt; command in the &lt;code&gt;build.sh&lt;/code&gt; script ends with &lt;code&gt;|| true&lt;/code&gt;. That is because if build is triggered while we are editing something in Notes and it fails, the unit will be in &amp;lsquo;failed&amp;rsquo; state and will not be triggered anymore. Could be this configured in the unit file? Maybe. It was faster just to swallow any error return value in the script. Output logs will be registered anyway by journald.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s it. Now I can write a Note and have it published in this blog. This is exactly how this post has been written:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Schermata 2025-01-10 alle 16.42.15.jpeg" src="Schermata%202025-01-10%20alle%2016.42.15.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;I can also directly upload images!&lt;/p&gt;
&lt;h2&gt;Note&lt;/h2&gt;
&lt;p&gt;Some things weren&amp;rsquo;t so straight to set up:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;blag&lt;/code&gt; package in Debian 12 is a little old and doesn&amp;rsquo;t work correctly and doesn&amp;rsquo;t come with default theme templates and static files.
So I downloaded latest source code and run that instead.&lt;/p&gt;
&lt;p&gt;I also copied out static files and templates into &lt;code&gt;/opt/blag/static&lt;/code&gt; and &lt;code&gt;/opt/blag/templates&lt;/code&gt; (as expected by the code) to fix some issues in templates (most of the links were absolute which is not good when your blog is in a subdirectory) and customize things around.&lt;/p&gt;
&lt;p&gt;Sometimes Nextclouds saves the note too fast and the service unit fails with &lt;code&gt;Failed with result 'start-limit-hit'.&lt;/code&gt;. I need to find a fix for this.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m quite pleased with the results. Now I need to write more 😁&lt;/p&gt;
&lt;h1&gt;Update&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;2025-01-21&lt;/em&gt; The build script has been updated&lt;/p&gt;
&lt;p&gt;As someone noticed, the build script as it is will delete the blog before building it. But if the build fails, no more blog! Here is the updated version:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/decrypted/nextclouddata/fabrixxm/files/Notes/blog&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/www/kirgroup.net/blog&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;BUILDDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/tmp/blog&amp;quot;&lt;/span&gt;

&lt;span class="nv"&gt;HERE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;dirname&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$HERE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

rm&lt;span class="w"&gt; &lt;/span&gt;-fr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$BUILDDIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;blag&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SOURCE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$BUILDDIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-fr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$BUILDDIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will build the blog in a temp folder, and only if it builds successfully the blog folder is removed and replaced by the new version.&lt;/p&gt;</content></entry></feed>