~kls0e hi :-)

Static snapshots of webpages

While creating my modern personal webpage anthology, I had the opportunity to test several approaches for taking static snapshots of webpages or websites.

To do this, I pulled three decades-old personal website source datasets from my archive. My goal was to make these digital artifacts accessible again as clean and static snapshots.

Although I did not use it in the final process, an honorable mention first - WebScrapBook is a great browser extension when dealing with lazy-loading content.

But let’s see how this actually unfolds:

2005: the HTML/CSS era

The oldest dataset is 21 years old at the time of writing. It originally lived under madcaps-page.de and consists purely of HTML and CSS. Because of its simplicity, this one was easy: I let HTTrack walk over it to consolidate the site structure. Afterwards, I manually fixed 404-ing links and removed external content — such as an obligatory externally hosted guestbook and a t-shirt shop (???) directly in the source code. Ready to reinstantiate.
snapshot: madcaps-page.de (2005)

to 2009: the WordPress era

The second dataset is a 17-year-old wwwroot and MySQL dump of a WordPress blog that resided under mdcp.de/blog. I spun up two local Docker instances providing Apache with PHP 7.4 and MariaDB. The database import worked flawlessly.

I remember having a couple of WordPress security plugins installed back in the day, which initially interfered with getting this ancient dump to render in the browser. After a couple of hours of debugging, I simply disabled all the legacy plugins and replaced the core files with their current successors — that is four major releases from 2.8.4 to 6.9.1. Impressively enough, it just worked.

Of course, I did not plan to maintain this WordPress instance any further. To preserve a static impression of the blog, I came across monolith. Written in Rust, it creates non-recursive webpage snapshots by bundling all assets into a single .html file using base64 inline encoding. It’s wonderful. I just had to fix countless internal links by replacing most of the href attributes to point to #.
snapshot: mdcp.de/blog (2009)

from 2009: the MySpace era

The newest dataset is a 17-year-old “File > Save as…” dump from Internet Explorer 7, containing my former MySpace profile myspace.com/matthi4s. I archived this one using monolith --isolate to prevent the archived version from calling any external JavaScript resources or tracking pixels. Some manual HTML sanitation took place as well, and there it is.
snapshot: myspace.com/matthi4s (2009)

My modern personal webpage anthology

y0s3n GIF from 2001

Let me interject for a second - and turn back the hands of time. I am a millennial born in 1985. No go dey call me Gen Z. I got my first 286 pc in 1996, at age eleven. My internet started with a 56k modem over analog landline—back when bandwidth was precious and dial-up minutes cost real money. This shaped my data hoarding habits: retaining backups of personal archives over decades and three system architectures. including old websites. Under the handle madcap, I built early pages from scratch, later on Wordpress and MySpace. How glad I am with Hugo these days.

Here are three authentic sparkling diamonds in my mother tongue german. I thank myself for bearing with me.

2005-08 / 2009-09 / 2009-11 (MySpace bonus!)

MySpace was the pioneering browser social network with wild CSS hacks. What is your personal Voyager golden record?

If you are interested, a technical writeup on this is available here: Static snapshots of webpages

Tildeverse, one box at a time

The tildeverse is a loosely associated network of free pubnix systems, text based servers to be creative on and do fun stuff with. This page is hosted at tilde.town, a pubnix by ~vilmibm and I just came across this opportunity to randomly explore it:

random ~user / random ~box / next ~user / join

Explorational links are also always available from the imprint from now on.

latest tilde.town updates (thanks ~ags):
Loading...

time.taxi – an ode to escapism

With some time on my hands, I decided to reissue a piece of internet art called time.taxi. It has been sitting on my ssd for the longest time, and its URL is squatted and stuff, but from now on, its sanctuary shall be tilde.town. sanctuary, god, I love this word. but please see for yourself:

https://tilde.town/~kls0e/time.taxi

by the way, while running the terminal-based email client ‘mutt’ for the first time yesterday while ssh’d into tilde.town, I’ve learned a couple of interesting things about this tildeverse:

there is a tilde town zine: https://tilde.town/~zine

fwiw, it’s super liberal, friendly and interesting to read. I’m looking to release one of my photos from jasmund.org in the coming issue.

there is a tilderadio: https://tilderadio.org

I essentially listen to online radio every day, my favourite stations are mitunter Radio Swiss Jazz, NTS, PerfectMoods, Groove Salad and today I’m actively listening to tilderadio. So far, it’s a refreshing blend of vintage radio commercials, oldschool hip hop and wild punk. excellent. My macOS Receiver Radio app does have some buffering issues with it, so VLC is back at it.

Mapping a Beauty R1 Bluetooth Remote to Kodi on Batocera

Beauty R1 remote control

A friend is running Batocera as a media/emulation box and wanted to use a Beauty R1 Bluetooth remote with Kodi. The R1 is a hyper-affordable bluetooth media remote — and it emits mouse movement events instead of regular key presses, and Batocera's underlying buildroot Linux has no idea what to do with it out of the box. It pairs just fine using Bluetooth 4.2 tho. evtest does show some output on each button press. But nothing seems to happen in the EmulationStation or Kodi GUI.
The fix is a small Python daemon that grabs the R1's input device exclusively and translates its events into keyboard events Kodi understands.

Why it’s tricky

Running evtest on the device reveals something challenging: every directional press arrives as three separate event groups before BTN_LEFT is even set:

REL_X=-2047, REL_Y=+2047  ← calibration spike (ignore!)
REL_X=+148,  REL_Y=-354   ← return-to-center offset
BTN_LEFT=1                 ← actual button down
REL_Y=+77 (×2)            ← real directional data
BTN_LEFT=0                 ← button up

all that REL data before the button press results in RIGHT + ENTER on every single keystroke. Took a while to figure out.

The other fun one: the Camcorder button sends the same BTN_LEFT sequence as the directional pad and center button — but its pre-BTN_LEFT offset is REL_Y ≈ -30 instead of ≈ -354. That tiny difference is the only way to tell them apart.

The solution

  1. Ignore all REL events with |value| ≥ 1000 — these calibration spikes and should be discarded.
  2. Track pre_y before BTN_LEFT=1 — if abs(pre_y) < 100, it’s the Camcorder button → send KEY_ESC. Otherwise it’s probably a nav key or center.
  3. direction_sent flag — once a direction has been emitted, suppress the ENTER that would follow on BTN_LEFT=0.

Key mapping

Beauty R1 buttonSendsKodi action
D-pad UpKEY_UPNavigate up
D-pad DownKEY_DOWNNavigate down
D-pad LeftKEY_LEFTNavigate left / rewind
D-pad RightKEY_RIGHTNavigate right / forward
Center (OK)KEY_ENTERSelect / Play-Pause
HomeKEY_VOLUMEDOWNVolume down
CameraKEY_VOLUMEUPVolume up
CamcorderKEY_ESCBack / close menu

Note: the Camera button sends KEY_VOLUMEDOWN — the script remaps it to KEY_VOLUMEUP. The Home button sends KEY_HOMEPAGE — remapped to KEY_VOLUMEDOWN because that’s the desired remote button layout of choice.

The script

Save to /userdata/system/beauty_r1_mapper.py:

#!/usr/bin/env python3
"""
Beauty R1 Bluetooth Remote → Kodi Keyboard Mapper
"""

import evdev
from evdev import InputDevice, UInput, ecodes
import sys

e = ecodes

DEVICE_NAMES = ["Beauty", "beauty", "R1", "r1"]
THRESHOLD = 50
SPIKE     = 1000

def find_beauty_r1():
    devices = [InputDevice(path) for path in evdev.list_devices()]
    for device in devices:
        for name in DEVICE_NAMES:
            if name in device.name:
                print(f"Found: {device.name} ({device.path})")
                return device
    print("Beauty R1 not found. Available devices:")
    for device in devices:
        print(f"  - {device.name} ({device.path})")
    return None

def main():
    r1_device = find_beauty_r1()
    if not r1_device:
        sys.exit(1)

    try:
        r1_device.grab()
    except IOError as err:
        print(f"Failed to grab device: {err}")
        sys.exit(1)

    capabilities = {
        e.EV_KEY: [
            e.KEY_UP, e.KEY_DOWN, e.KEY_LEFT, e.KEY_RIGHT,
            e.KEY_ENTER, e.KEY_ESC, e.KEY_VOLUMEUP, e.KEY_VOLUMEDOWN,
        ]
    }
    ui = UInput(capabilities, name="Beauty R1 Virtual Keyboard")
    print("Mapper running. Ctrl+C to quit.")

    button_pressed = False
    direction_sent = False
    is_camcorder   = False
    pre_y  = 0
    post_x = 0
    post_y = 0

    try:
        for event in r1_device.read_loop():

            if event.type == e.EV_REL:
                if abs(event.value) >= SPIKE:
                    continue
                if not button_pressed:
                    if event.code == e.REL_Y:
                        pre_y += event.value
                else:
                    if not direction_sent:
                        if event.code == e.REL_X:
                            post_x += event.value
                        elif event.code == e.REL_Y:
                            post_y += event.value

            elif event.type == e.EV_KEY:

                if event.code == e.BTN_LEFT:
                    if event.value == 1:
                        button_pressed = True
                        direction_sent = False
                        post_x = 0
                        post_y = 0
                        is_camcorder = abs(pre_y) < 100
                        pre_y = 0
                    elif event.value == 0:
                        if is_camcorder:
                            ui.write(e.EV_KEY, e.KEY_ESC, 1)
                            ui.write(e.EV_KEY, e.KEY_ESC, 0)
                            ui.syn()
                            print("→ ESC (Camcorder)")
                        elif not direction_sent:
                            ui.write(e.EV_KEY, e.KEY_ENTER, 1)
                            ui.write(e.EV_KEY, e.KEY_ENTER, 0)
                            ui.syn()
                            print("→ ENTER")
                        button_pressed = False
                        direction_sent = False
                        is_camcorder   = False
                        post_x = 0
                        post_y = 0

                elif event.code == e.KEY_HOMEPAGE:
                    if event.value == 1:
                        ui.write(e.EV_KEY, e.KEY_VOLUMEDOWN, 1)
                        ui.write(e.EV_KEY, e.KEY_VOLUMEDOWN, 0)
                        ui.syn()
                        print("→ VOLUME DOWN (Home)")

                elif event.code == e.KEY_VOLUMEDOWN:
                    if event.value == 1:
                        ui.write(e.EV_KEY, e.KEY_VOLUMEUP, 1)
                        ui.write(e.EV_KEY, e.KEY_VOLUMEUP, 0)
                        ui.syn()
                        print("→ VOLUME UP (Camera)")

            elif event.type == e.EV_SYN:
                if button_pressed and not direction_sent and not is_camcorder:
                    if abs(post_x) >= THRESHOLD or abs(post_y) >= THRESHOLD:
                        if abs(post_y) >= abs(post_x):
                            key, name = (e.KEY_UP, "UP") if post_y > 0 \
                                   else (e.KEY_DOWN, "DOWN")
                        else:
                            key, name = (e.KEY_LEFT, "LEFT") if post_x > 0 \
                                   else (e.KEY_RIGHT, "RIGHT")
                        ui.write(e.EV_KEY, key, 1)
                        ui.write(e.EV_KEY, key, 0)
                        ui.syn()
                        print(f"→ {name}")
                        direction_sent = True
                        post_x = 0
                        post_y = 0

    except KeyboardInterrupt:
        print("\nStopped.")
    finally:
        r1_device.ungrab()
        ui.close()

if __name__ == "__main__":
    main()

make it executable: chmod +x /userdata/system/beauty_r1_mapper.py
test with python3 /userdata/system/beauty_r1_mapper.py

Using it on Android

If you aren’t running Linux/Batocera but rather an Android device and want to fix the Beauty R1 remote’s mapping, check out Luca’s work on GitHub:

beauty-r1-android-interceptor by olivluca

Luca came up with logic to intercept and translate the remote’s events on Android and wrote a C program to do so.

beautiful colors on Thinkpad T430 using this ICC Profile 🏳️‍🌈⃤

A friend I have met via Freifunk Berlin sold me his Thinkpad T430. It is a great, robust and versatile machine and I use it almost daily with a posix-compatible OS. The colors of its TN panel are a bit bold on the blue side. There are devices to measure screen colors and there is software to correct and balance them. This process is called monitor calibration and usually results in an .icc file containing the custom color corrections. The GPU will essentially translate its color output according to the values read from the ICC profile, be it a standard or a custom output profile. A custom ICC profile can be applied to a graphical output device on every major (and not so major) OS. The GPU will be just as powerful as with a standard profile.

Back in 2012, notebookcheck.net has published a thorough review of the T430. They have also tested the machines panel and were kind enough to publish the ICC profile generated for it. Please look for the words “Download ICC File (X-Rite i1Display 2)" on their article page.

Once downloaded, apply the ICC profile to your T430 display. The ICC profile selection menu of your OS will show a new entry that reads Monitor_20.07.2012_1_01 if you did move the .icc file to the right place.

Overall impression is much nicer and friendlier now. Natural & vivid colors. It is worth it. Watch a video and compare.

Aiyima T9 Pro with HifiberryOS 🎶

It was in the mid 1990s when my godfather introduced me to his passion of music reproduction, praising the great speakers by the small label of Orbid Sound from Odenwald area, Germany. Amazed at how good music can sound (even if compressed in ATRAC codec using Sony Minidisc further on), I was now a Hifi enthusiast as well.
When I was 14, I was lucky enough of being able to afford a small technics SC HD-310 stereo investing all of my confirmation money. I did use it as my main (and only :-)) system 10+ years from that.
It must have been around the early 2010s when I first got to know about Class D Amplifiers. As of writing this, it is 2023 I am still into that.

Main System and Signal Path

Aiyima does a pretty good job at designing and manufacturing Class D Amps, I would say. For their A07 model in conjunction with a capable low-ripple psu, I have sold my NAD C326-BEE. Further signal chain consists of an RPi 3B+ and a Hifiberry Digi+ Pro Hat, both fed by an ifi iPower, followed by a Fosi DAC-Q5 into an FX-Audio Tube-01 Tube Buffer with General Electric JAN 5654W new old stock tubes, powered by an ifi iPower X.

While using AirPlay 2 and also listening to Radio Swiss Jazz aacPlus stream on a regular basis, main audio source is Tidal Hifi Plus using HifiberryOS and the Tidal Connect Docker by TonyTromp. Speakers are a pair of Mission 700 from the 1980s.

New Amp Combines it All

Some people might consider this signal chain to be extensive. It does sound good enough. The folks at Aiyima apparently found this signal path to be worthwile as well and took a few steps to combine this way of sound processing into one single device: The Aiyima T9 Pro. It excels. I run it via its USB interface and have another RPi 3B+ hooked to it. Hagen was kind enough to point out how to use HifiberryOS with USB audio devices.

Even the great TIDAL Connect Docker works, even in Hi-Res Mode as the T9 Pro indicates 96khz input, where available.

In my sleeping room, the T9 Pro drives a pair of turquoise DUAL MN 8010 speakers from the 1990s. They originate from german hypermarket chain Karstadt efforts of stepping into the upper hifi segment.

The T9 Pro also features Toslink, Coax, Cinch RCA und Bluetooth aptX inputs by the way. In size, it is approximately only a stack of 10 slim cd jewel cases. And its speaker terminals are fairly small. 1,5 mm² is possible. Otherwise use small banana jacks.

As a side note, if you consider buying the amp, make sure to get the T9 Pro model.

I do recommend it to everybody. Thanks Aiyima, we are not affiliated but I am convinced by the performance of this precious Amp. Bang for the Buck. Now we are talking!

Update: tubes rolled for GE JAN 5654W

Replacing the stock 5725 tubes with GE JAN 5654W gives additional warmth, softer vocals and more powerful bass punch. Tube rolling and New Old Stock are great newly coined terms, too. The process is as easy as 1,2,3, they just sit in a circular 7-pin socket. Try it!

upgrade wifi on hp 17-bs notebook

The HP 17-BS XXXXX series is a widespread budget notebook line, it is somewhat affordable and has mediocre specs one can work with for every-day tasks, whatever that means.

Its build quality is on the lower edge with lotsa relatively thin plastic involved, but here is a video on how to carefully open the device without breaking it. Thanks, Christian. Cool dialect btw :)

Turns out it is relatively easy to swap the wifi card. This notebook sports only 1 antenna. The stock RTL8723BE performs absurdly poor with its 1x1 2.4 ghz stream. Better buy an RTL8821CE 802.11AC 1X1 Wi-Fi + BT 4.2 Combo on ebay or anywhere else for 6€, it is compatible and it is not block-listed efi-wise and delivers beyond stellar in comparison, given there is an 802.11ac access point around.

add AirPrint to a Brother MFC-7360N

With all the recent corona changes, we are in need of a home printer. Of course, b/w laser is the way to go, cheap inkjet printers are a scam and tend to break right after the warranty period. So I decided to buy a 4-in-1 Brother MFP, the MFC-7360N. It can print, scan, copy and fax and I got it from eBay kleinanzeigen for 20.- € with an empty toner cartridge.

I am really happy with this machine and can recommend it to anyone, 3rd party toners are very affordable (around 12.- €) and last for thousands of pages.

I also like to print scanned developed analog b/w photos with it as they look like photocopies, but that is a matter of taste I guess.

So the machine works well and performs great, and there is an iOS app for it which allows you to scan to your phone. You can also print with this app but I was looking for AirPrint support to integrate directly with iOS devices on the same network. The printer does not feature this relatively new protocol but if you have a linux machine on the network, maybe a raspberry pi, install CUPS properly and use the brlaser v4 ppd with it by Peter De Wachter, thanks! Presto, there’s your AirPrint, works like a charm.

Another reason I recommend Brother is that there is an undocumented “toner empty”-reset command which will just let you print along until the toner is physically used up:

1. Schalten Sie Ihr Brother MFC Gerät an
2. Öffnen Sie die Frontklappe -lassen Sie den Toner im Drucker
3. Drücken Sie die Taste STORNO 1x -nur einmal
4. Es erscheint die Anzeige "Trommel ersetzen?" 1. JA 2. NEIN -hier nichts auswählen, weiter mit
5. Drücken Sie die STERN-Taste *
6. Drücken Sie die Taste 0 zweimal nacheinander
7. Schließen Sie die Frontklappe
8. fertig

found in a german video review by VideoP, thanks.

So it turns out that the toner cartridge I got with the printer is far from empty and the Brother continues to print just fine :)

Dynamically adjusting bandwidth limits

Full quote by Perry:

“I got a request from a refugee center to set up qos so that in the evenings and on weekends the bandwidth-limit gets raised from 35Mbit to 45Mbit, and back down to 35Mbit Mon-Fri 7:00 to 20:00. To do this, I did the following. It’s a hack, which could be improved (like storing up slow speed and fast speed in a config file somewhere).

So this is what I did…. I wrote the following script (/root/qos-schedule.sh)

#!/bin/sh

. /lib/functions.sh

SLOW=35000
FAST=45000

SPEED=$SLOW

#check to see if it's the weekend
if [[ $(date +%u) -gt 5 ]]; then
  SPEED=$FAST
else
  # check to see if it's not between 7:00 and 20:00
  if [[ $(date +%H) -lt 7 ]] || [[ $(date +%H) -ge 20 ]]; then
    SPEED=$FAST
  fi
fi

#compare the speed to the current settings.
OLDSPEED=$(uci get qos.ffuplink.download)
if [[ $OLDSPEED -ne $SPEED ]]; then
  logger -t qos-schedule.sh "setting time-based qos to $SPEED"
  uci set qos.ffuplink.download=$SPEED
  uci commit qos
  /etc/init.d/qos reload
fi

I added it to crontab to run every hour

Additionally I added the script to /etc/rc.local (so that the correct speed is set at startup)

Additionally I created a hotplug script /etc/hotplug.d/ntp/50-qos-schedule

#!/bin/sh

#logger -t hotplug-ntp-qos-schedule "Action is $ACTION, stratum is $stratum"
[ "$ACTION" = stratum ] || exit 0

logger -t hotplug-ntp-qos-schedule "Running qos-schedule from ntp hotplug event"
/root/qos-schedule.sh

Lastly, I added /root/qos-schedule.sh and /etc/hotplug.d/ntp/50-qos-schedule to /etc/sysupgrade.conf

kls0e Would you like to add that to your list of super cool tutorials?”

absolutely.