[{"content":"A dockerized script for converting a FLAC music library to a lossy Opus/MP3 format making maintaining an offline music collection easy.\nnietaki/lossifier converts your lossless .flac music collection to a mirrored .opus or .mp3 collection Shell 0 0 More info on the setup in the blogpost:\nOwning your music (collection) without losing your mind, part 2 27 March 2026\u0026middot;1536 words\u0026middot;8 mins Music Homelab Docker Scripting (part 1 here) Now that we have a way to get our music and our player(s) picked out, let\u0026rsquo;s come up with an easy to use workflow and an organisatonial structure that works for us. It\u0026rsquo;s going to be a lengthy one, so let\u0026rsquo;s just get started. Here\u0026rsquo;s the gist of the workflow: flowchart TD cd((CDs)) cd-- dBPoweramp CD Ripper --\u003eflac bandcamp@{ shape: cloud } bandcamp--\u003eflac subgraph TrueNas direction TD flac@{ shape: documents, label: \"/flac_music\" } opus@{ shape: documents, label: \"/opus_music\" } mp3@{ shape: documents, label: \"/mp3_music\" } plex@{ shape: \"lin-rect\", label: \"PLEX server\" } flac-. lossifier-opus .-\u003eopus flac-. lossifier-mp3 .-\u003emp3 flac===plex end rb@{ shape: card, label: \"rockbox DAP\"} android@{ shape: card, label: \"Android DAP\"} snow@{ shape: card, label: \"snowsky DAP\"} laptop@{ shape: card, label: \"laptop\"} opus-- rclone --\u003erb mp3-- rclone --\u003esnow opus-. autosync .-\u003eandroid plex--\u003elaptop plex--\u003eandroid flowchart TD subgraph legend [Legend] direction TD style legend fill:none smb@{ shape: documents, label: \"SMB share\" } docker@{ shape: \"lin-rect\", label: \"docker service\" } dap@{ shape: card, label: \"player hardware\"} Com@{ shape: braces, label: \"dotted arrows run\\n on a schedule\" } end As you can see the centerpiece of the system is a TrueNas NAS. Yes, the \u0026ldquo;logo\u0026rdquo; is AI-generated, one of the few things here that are :D\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/lossifier/","section":"Projects","summary":"A dockerized script for converting a FLAC music library to a lossy Opus/MP3 format making maintaining an offline music collection easy.\nnietaki/lossifier converts your lossless .flac music collection to a mirrored .opus or .mp3 collection ","title":"lossifier","type":"projects"},{"content":"A while back I was looking for a creative outlet I could make something I\u0026rsquo;d like on a daily basis and (finally) got into creative coding.\nI chose love2d as my framework and started making animations, aiming to create a new one on a daily basis.\nPrevious Next As I built up my lua toolkit the creations got more complex over time, with 3d rendering, a custom GLSL pre-processor and tracing profiler.\nThe best way to see the animations is on my instagram hightlights - all the animations are there in the order I made them, with some hand-picked background music:\nCreative Coding animations https://www.instagram.com/ A collection of my creative coding animations. Turn the sound on! I enjoyed the process so much that I basically got into the demoscene - I\u0026rsquo;m sitting on a 4k demo I made in a week but barely missed the deadline for the 2025 edition of Xenium - hoping to get it released in the 2026 edition!\nAs a side-note, love2d is an utterly amazing framework - very easy to get into, but incredibly powerful. If you\u0026rsquo;re clever about it, you can use lua api for many of the low-level graphics operations without having to write a single line of c++. I just hope they finally release the 12.0 version - it\u0026rsquo;s already very usable and more powerful than the 11.5 that\u0026rsquo;s been around for a while, but it\u0026rsquo;s still missing the amazing 11.5 API docs.\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/creative-coding/","section":"Projects","summary":"A while back I was looking for a creative outlet I could make something I’d like on a daily basis and (finally) got into creative coding.\nI chose love2d as my framework and started making animations, aiming to create a new one on a daily basis.\nPrevious Next As I built up my lua toolkit the creations got more complex over time, with 3d rendering, a custom GLSL pre-processor and tracing profiler.\n","title":"Creative Coding","type":"projects"},{"content":"After using ergonomic keyboards for over a decade I designed one that\u0026rsquo;s close to perfect - not for most, but definitely for me.\nnietaki/revolt-keyboard Makefile 3 1 More details on the NTK Studio product page:\nRevolt Keyboard https://ntkstudio.net/ Revolt is the split ergonomic keyboard that puts the user first. It prioritises an abundance of keys over a tiny footprint, a practical layout over trendy looks, and a diy-friendly, modular build over surface-deep minimalism. It boasts 68 low-profile switches on a tented base, backlit home-row keys to help you find your bearing, and a single microcontroller running the show and providing touchpoint-mouse functionality. It’s designed to last you for decades with no issues, but if any do come up it’s easy and cheap to fix without complicated disassembly. ","date":"10 April 2026","externalUrl":null,"permalink":"/projects/revolt-keyboard/","section":"Projects","summary":"After using ergonomic keyboards for over a decade I designed one that’s close to perfect - not for most, but definitely for me.\nnietaki/revolt-keyboard ","title":"Revolt keyboard","type":"projects"},{"content":"Examine a random Epstein file from the millions that have been released. You can narrow the pool down by file type or text contents.\nAnother small project I wrote while teaching myself golang. It uses the Echo web framework and sqlite as the database.\nNote The logo was (obviously) AI generated, but the code was written by hand as a learning exercise, not vibe-coded.\nI\u0026rsquo;m pretty happy how well it runs on under 100MB of RAM and a single core of an Intel N150 CPU of a NAS, while serving over 0.5TB of files and costing me literally no money in hosting fees.\nEpstein Roulette - examine a random file from 1,057,713 available! https://epsteinroulette.win/ Files categorised into video, audio and PDFiles for your peeping pleasure. PDFiles' content search now available! Note The idea might seem in bad taste (and maybe it is) - my intention was to not let the topic die down and empower people to see for themselves what the files contain in at least a semi-entertaining way\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/epstein-roulette/","section":"Projects","summary":"Examine a random Epstein file from the millions that have been released. You can narrow the pool down by file type or text contents.\nAnother small project I wrote while teaching myself golang. It uses the Echo web framework and sqlite as the database.\nNote The logo was (obviously) AI generated, but the code was written by hand as a learning exercise, not vibe-coded.\n","title":"Epstein Roulette","type":"projects"},{"content":" (not just) Split Keyboard Gallery https://splitkeyboard.gallery/ Split Keyboard Gallery - tool for browsing over 100 keyboards and filtering them by properties A weekend project to teach myself myself Alpine.js and hugo. And to get the word about my Revolt keyboard out there.\nNTK-studio/split-keyboard-gallery HTML 0 0 ","date":"10 April 2026","externalUrl":null,"permalink":"/projects/split-keyboard-gallery/","section":"Projects","summary":" (not just) Split Keyboard Gallery https://splitkeyboard.gallery/ Split Keyboard Gallery - tool for browsing over 100 keyboards and filtering them by properties A weekend project to teach myself myself Alpine.js and hugo. And to get the word about my Revolt keyboard out there.\n","title":"Split Keyboard Gallery","type":"projects"},{"content":"A thin Elixir wrapper for :redbug production-friendly Erlang interactive tracing debugger. It tries to preserve :redbug\u0026rsquo;s simple and intuitive interface while making it more convenient to use by Elixir developers.\nnietaki/rexbug A thin Elixir wrapper for the redbug Erlang tracing debugger. Elixir 261 16 Since I created rexbug, it became the de-facto standard Elixir tool, totalling over half a million downloads to date.\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/rexbug/","section":"Projects","summary":"A thin Elixir wrapper for :redbug production-friendly Erlang interactive tracing debugger. It tries to preserve :redbug’s simple and intuitive interface while making it more convenient to use by Elixir developers.\nnietaki/rexbug A thin Elixir wrapper for the redbug Erlang tracing debugger. ","title":"Rexbug","type":"projects"},{"content":" nietaki/markdown-resume A simple tool for generating good looking CV pdfs from vanilla markdown files HTML 42 17 I always felt a developer\u0026rsquo;s CV should be easy to edit in a code editor and compiled into a handsome and consistent pdf at a moment\u0026rsquo;s notice. After using someone else\u0026rsquo;s LaTeX template (which I couldn\u0026rsquo;t really understand, much less improve) for years I decided to make this project, for both myself and others.\nI made a simple default style, but made sure new ones can be added easily and that anyone can use the tool regardless of their computer setup.\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/markdown-resume/","section":"Projects","summary":" nietaki/markdown-resume A simple tool for generating good looking CV pdfs from vanilla markdown files ","title":"markdown-resume","type":"projects"},{"content":" A kinetic sculpture in the form of an interactive coffee table. While it might not end up being commercially available, the tech side of things was a success, especially considering the bootstrapping approach in a small team. From the aluminum and 3d printed plastic mechanism I designed from scratch, through the electronics, microcontroller \u0026ldquo;brain\u0026rdquo; with some clever C++ code on it and the Flutter app that controlled it all - it all works pretty damn well and looks good in the wood and concrete package.\nAmpersand Table https://ampersandtable.com/ Inspired by ever changing sea shores from around the world, Ampersand table combines the beauty of movement with fine materials and craftsmanship. Glass tabletop reveals a layer of sand that is used as a medium. In it, a steel ball traces intricate algorithmic patterns. All is controlled with a near silent mechanism hidden underneath. I\u0026rsquo;m open to selling the IP while I focus on other things - get in touch if you\u0026rsquo;re interested.\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/ampersand-table/","section":"Projects","summary":" A kinetic sculpture in the form of an interactive coffee table. While it might not end up being commercially available, the tech side of things was a success, especially considering the bootstrapping approach in a small team. From the aluminum and 3d printed plastic mechanism I designed from scratch, through the electronics, microcontroller “brain” with some clever C++ code on it and the Flutter app that controlled it all - it all works pretty damn well and looks good in the wood and concrete package.\n","title":"Ampersand Table","type":"projects"},{"content":" CrowdHailer/raxx Interface for HTTP webservers, frameworks and clients Elixir 406 28 All three projects started by Pete, presenting a simple and powerful alternative to the standard Elixir cowboy/Plug/Phoenix web stack. For a while I was contributing to them as much as I could and Raxx has now reached 1.0 and stabilised a bit.\nWhile the project never really caught on, I still believe it had some significant benefit over the Elixir-standard plug/cowboy stack.\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/raxx-ace/","section":"Projects","summary":" CrowdHailer/raxx Interface for HTTP webservers, frameworks and clients ","title":"Raxx/Ace/Raxx.Kit","type":"projects"},{"content":" ","date":"10 April 2026","externalUrl":null,"permalink":"/","section":"","summary":"","title":"","type":"page"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/3d-design/","section":"Tags","summary":"","title":"3d Design","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/alpine.js/","section":"Tags","summary":"","title":"Alpine.js","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/c/","section":"Tags","summary":"","title":"C","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/c++/","section":"Tags","summary":"","title":"C++","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/debugging/","section":"Tags","summary":"","title":"Debugging","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/docker/","section":"Tags","summary":"","title":"Docker","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/electronics/","section":"Tags","summary":"","title":"Electronics","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/elixir/","section":"Tags","summary":"","title":"Elixir","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/flutter/","section":"Tags","summary":"","title":"Flutter","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/fusion-360/","section":"Tags","summary":"","title":"Fusion 360","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/glsl/","section":"Tags","summary":"","title":"GLSL","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/golang/","section":"Tags","summary":"","title":"Golang","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/homelab/","section":"Tags","summary":"","title":"Homelab","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/keyboards/","section":"Tags","summary":"","title":"Keyboards","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/kicad/","section":"Tags","summary":"","title":"Kicad","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/lua/","section":"Tags","summary":"","title":"Lua","type":"tags"},{"content":"There\u0026rsquo;s a bunch of projects that make my life a whole lot better. I can only preach about them to my friends so much, so here\u0026rsquo;s some links to spread the word otherwise.\nBlowfish # The Hugo template I used for the latest incarnation of this blog.\nI knew it was good as soon as I saw it, but the fact that it took me less than a day to move from the old jekyll blog to hugo, add some customizations without slowing down and be very happy with the results makes it just exceptional.\nNeoVim # Ever since university days I liked keyboard-focused tools that allow you to do exactly what you need quickly. Also, as a developer I appreciate being able to work on a project in any technology without fighting my muscle memory or giving up on the convenience of workflows I built up over the years.\nNeoVim gives me just that. Between the config I automatically pull to my OSX or Linux dev machines and LSP for tech-stack related tools, my NeoVim feels like home regardless of what I\u0026rsquo;m working on. I used it for backend, frontend, embedded programming, and writing documentation and it works, and fast too.\nAt one point in time I did try and make Spacemacs what NeoVim is for me right now, but I definitely lost that fight :D\nTodoist # Todoist is quickly becoming my favourite personal task management system. It\u0026rsquo;s simple, powerful and cross-platform. I tried many similar apps before but this seems to be the version that feels the best day to day.\nNotion # Notion is basically the endgame of productivity tools and something I\u0026rsquo;ve been looking for years. It does rich-text hierarchical note-taking with Markdown-style shortcuts (on web and mobile), team collaboration, the ability to publish any section of your workspace online and much more. The killer feature are their \u0026ldquo;databases\u0026rdquo; - a way to work with structured information with convenient UI. Depending on your use-case a database can be used like a google sheet, a turbo-charged calendar, a kanban board, a gallery, or all of the above at the same time. I\u0026rsquo;ve seen Notion used a CRM, a Trello alternative, internal or public knowledge base and much more.\nIn the interest of objectivity, Notion isn\u0026rsquo;t without its flaws: It has very limited offline capabilities, the \u0026ldquo;databases\u0026rdquo; can be problematic in more complex use-cases and hundreds of rows, it lacks isolated password protected sections for the more private notes (which I used in Standard Notes). It\u0026rsquo;s not a google sheet replacement either - there are limits to its power of expression. Still, it\u0026rsquo;s my go-to for basically any brainstorming, note-taking or organisational task.\nMiro # Miro fills in the gaps of Notion as a brainstorming / system design tool. It\u0026rsquo;s a digital whiteboard perfectly suited for running workshops, making diagrams or just free-form brainstorming. I tried many similar tools, but Miro is hands down the best of the bunch, especially when it comes to UX and collaboration tools. The free tier is very full-featured and doesn\u0026rsquo;t struggle even if you throw a lot of content at it.\n1Password # 1Password is the password manager I use ever since I abandoned LastPass after they locked me out of my account for a day or so. It integrates beautifully with OSX, alright with Windows and more than good enough just in a browser under Linux.\nI used it in a team context as well and had no problems there either.\nDash # Dash is a universal API documentation browser. It comes with hundreds (!) of docsets, for both language core libraries and third-party libraries, all under one convenient UI. It\u0026rsquo;s only available for OSX, but the author worked with other developers to provide alternatives for other platforms.\nYou might not think like it\u0026rsquo;s much different from googling code and bookmarking a handful of api docs pages, but from my experience it really is.\n","date":"10 April 2026","externalUrl":null,"permalink":"/awesome/","section":"","summary":"There’s a bunch of projects that make my life a whole lot better. I can only preach about them to my friends so much, so here’s some links to spread the word otherwise.\nBlowfish # The Hugo template I used for the latest incarnation of this blog.\nI knew it was good as soon as I saw it, but the fact that it took me less than a day to move from the old jekyll blog to hugo, add some customizations without slowing down and be very happy with the results makes it just exceptional.\n","title":"My small collection of awesome","type":"page"},{"content":"Some of the (side)projects I worked on\n","date":"10 April 2026","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"Some of the (side)projects I worked on\n","title":"Projects","type":"projects"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/scripting/","section":"Tags","summary":"","title":"Scripting","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/shaders/","section":"Tags","summary":"","title":"Shaders","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/sqlite/","section":"Tags","summary":"","title":"Sqlite","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/tools/","section":"Tags","summary":"","title":"Tools","type":"tags"},{"content":"","date":"10 April 2026","externalUrl":null,"permalink":"/tags/tracing/","section":"Tags","summary":"","title":"Tracing","type":"tags"},{"content":" Blog # ","date":"27 March 2026","externalUrl":null,"permalink":"/posts/","section":"","summary":"Blog # ","title":"","type":"posts"},{"content":"","date":"27 March 2026","externalUrl":null,"permalink":"/tags/music/","section":"Tags","summary":"","title":"Music","type":"tags"},{"content":"(part 1 here)\nNow that we have a way to get our music and our player(s) picked out, let\u0026rsquo;s come up with an easy to use workflow and an organisatonial structure that works for us. It\u0026rsquo;s going to be a lengthy one, so let\u0026rsquo;s just get started.\nHere\u0026rsquo;s the gist of the workflow:\nflowchart TD cd((CDs)) cd-- dBPoweramp CD Ripper --\u003eflac bandcamp@{ shape: cloud } bandcamp--\u003eflac subgraph TrueNas direction TD flac@{ shape: documents, label: \"/flac_music\" } opus@{ shape: documents, label: \"/opus_music\" } mp3@{ shape: documents, label: \"/mp3_music\" } plex@{ shape: \"lin-rect\", label: \"PLEX server\" } flac-. lossifier-opus .-\u003eopus flac-. lossifier-mp3 .-\u003emp3 flac===plex end rb@{ shape: card, label: \"rockbox DAP\"} android@{ shape: card, label: \"Android DAP\"} snow@{ shape: card, label: \"snowsky DAP\"} laptop@{ shape: card, label: \"laptop\"} opus-- rclone --\u003erb mp3-- rclone --\u003esnow opus-. autosync .-\u003eandroid plex--\u003elaptop plex--\u003eandroid flowchart TD subgraph legend [Legend] direction TD style legend fill:none smb@{ shape: documents, label: \"SMB share\" } docker@{ shape: \"lin-rect\", label: \"docker service\" } dap@{ shape: card, label: \"player hardware\"} Com@{ shape: braces, label: \"dotted arrows run\\n on a schedule\" } end As you can see the centerpiece of the system is a TrueNas NAS.\nTrueNas # I became a big fan of those as soon as I set one up - it runs on any hardware, it\u0026rsquo;s trivial to set up, but at the same time it offers some advanced features, even in the free version.\nThe features we\u0026rsquo;ll care about the most here:\nSMB shares # TrueNas lets you create network shares for your datasets and it helps you configure ACL for users and service accounts.\nOne of our SMB shares (marked with /flac_music) becomes the \u0026ldquo;source of truth\u0026rdquo; of our music collection - this is where we dump the music downloaded from bandcamp.\ndBpoweramp CD Ripper has no issues ripping directly to a network share, which makes that workflow trivial (remember you\u0026rsquo;ll need to have the network share mounted first)\nRunning Docker services # TrueNas lets us run dokerized apps - both pre-packaged ones available in its built-in catalog and custom ones. As long as there\u0026rsquo;s a docker image published somewhere (even in a private repository), TrueNas should be able to run it.\nThat\u0026rsquo;s how we get the PLEX server for our plexamp - we just need to mount a volume with the flac music.\nWe can also use Docker to handle our music conversion.\nLossifier # I made lossifier - a handful of bash scripts in a trench coat Docker container that can help create and maintain a \u0026ldquo;lossy\u0026rdquo; version of a lossless FLAC music collection.\nnietaki/lossifier converts your lossless .flac music collection to a mirrored .opus or .mp3 collection Shell 0 0 You can check out DockerHub and GitHub for more info on the project and its features, but what we need to do is create a deployment per target format in the TrueNAS UI - just go to Apps -\u0026gt; Discover Apps -\u0026gt; Custom App and configure it a bit like this:\ndocker image and tag on this screen conversion settings 568 is the apps user of TrueNas, helps keep the file permissions reasonable note the readonly mount of the flac directory I\u0026rsquo;m overprovisioning here, it would probably get by without any issues on 20MB of RAM and 1 CPU Cron # The idea behind Lossifier is that you run it on a schedule, to keep the lossy collections in sync with your master collection. The initial run will take a while (let\u0026rsquo;s say 2h for every 1000 songs in the collection), but the subsequent runs should be quick - they just verify the state and address the updates.\nWe can use TrueNas\u0026rsquo; built in cron tool to kick off a run every 15 minutes, for example. In the UI go to System -\u0026gt; Advanced Settings -\u0026gt; Cron -\u0026gt; Add and add the schedule:\nYou might wonder how TrueNas derives the container name from its configuration - the result is pretty funky. I wonder as well and didn\u0026rsquo;t quite figure it out - the important thing is that you can check what it is and that it doesn\u0026rsquo;t seem to change over time - even if you update the image or its config:\n$ sudo docker ps -a --format \u0026#39;{{.Image}} {{.Names}}\u0026#39; nietaki/lossifier:latest ix-lossifier-mp3-lossifier-mp3-1 cloudflare/cloudflared:2026.2.0 ix-cloudflared-cloudflared-1 plexinc/pms-docker:plexpass ix-plex-plex-1 I personally run the opus conversion every 15 minutes - seems like a good balance between getting the music ready to put on your devices quickly and saving compute / making sure the runs don\u0026rsquo;t come close to overlapping in time. The mp3s I don\u0026rsquo;t care as much about, so they\u0026rsquo;re run less frequently.\nWith this working the techy part is basically done, let\u0026rsquo;s talk about music organisation.\nDirectory structure (to save your sanity) # Contrary to popular belief (of the /r/PleX subreddit) there is more than one sensible way of organising your music. The way I organise mine had the following goals:\nalbums organised by Album Artist to make it easy to navigate the directory structure (this part is not controversial) dead-simple, platform agnostic playlist management compatibility with all the players I might want to use The playlists part is the difficult one - Plex wants to do it for you even though it\u0026rsquo;s abysmal at it, Poweramp on Android does a good job but would be a huge pain to sync with your \u0026ldquo;source of truth\u0026rdquo;, similar thing with Rockbox. Creating your playlists in one of these tools and using them in the others is basically impossible.\nSo what I went for instead is organising the playlists as directories inside the FLAC source-of-truth dataset - if I want to add a song to a playlist I just copy it to the playlists directory.\nThis works for me because I don\u0026rsquo;t care much about the song order inside a playlist (so I guess they\u0026rsquo;re more like collections) or having the files duplicated inside my collection. As long as the players don\u0026rsquo;t pick them up when I\u0026rsquo;m listening to albums and don\u0026rsquo;t present duplicated tracks inside any given album.\nLet\u0026rsquo;s see what it looks like in practice:\n$ tree -d . ├── Music │ ├── Albums │ │ ├── 6ix Toys │ │ │ └── 6ix Toys │ │ ├── Afro Kolektyw │ │ │ └── Czarno Widzę │ │ ├── Agnieszka Osiecka │ │ │ ├── Sześć oceanów, piosenki 1962-2013 - CD1 - Ocean popielaty │ │ │ ├── Sześć oceanów, piosenki 1962-2013 - CD2 - Ocean rozowy │ │ │ ├── Sześć oceanów, piosenki 1962-2013 - CD3 - Ocean niespokojny │ │ │ ├── Sześć oceanów, piosenki 1962-2013 - CD4 - Ocean burz │ │ │ ├── Sześć oceanów, piosenki 1962-2013 - CD5 - Ocean granatowy │ │ │ └── Sześć oceanów, piosenki 1962-2013 - CD6 - Ocean zielony ... │ │ └── Łona I Webber │ │ └── Cztery I Pół │ ├── Playlists │ │ ├── 00s MTV music │ │ ├── 4x4 - Leading The Blind - soundtrack │ │ ├── 5 a.m │ │ ├── Liquid DnB subset ... │ │ ├── sappy songs │ │ └── ~ rockmetal │ └── m3us # this contains .m3u playlist files for each of the /Music/Playlists/* directories └── Playlists # this does as well You\u0026rsquo;ll see that /Music/Playlists is separate from /Music/Albums and we have additional directories with .m3u playlist files - the most widely supported standard around. Those are generated by lossifier, if you configure it for that purpose.\nThe reason I have two directories with playlist files is for compatibility with different players. Ancient Android standards expect music in /Music and playlist in /Playlists, so that works well for the Android-based DAPs.\nFor some reason I don\u0026rsquo;t really remember anymore, in Rockbox it worked out better to have the m3us directly inside /Music.\nThe way to handle the album/playlist separation in Plex is simple enough - create a \u0026ldquo;Library\u0026rdquo; for each - you can use the \u0026ldquo;main\u0026rdquo; one in any way offered by plexamp, and in the Playlists one it\u0026rsquo;s usually best to browse by directories.\nSynchronising music # If you already have your DAP(s) configured the way you like, the remaining puzzle-piece is keeping them synchronized with the library.\nDragging-and-dropping files from an SMB share is good and all, but even with lossy compression it\u0026rsquo;s easy to get your library to tens or even hundreds of Gigabytes. Even if your OS attempts to do some \u0026ldquo;clever\u0026rdquo; directory merging it can take a while.\nOn Android-based devices you can use something like Autosync. You can set it up so that it connects to a data source of your choice (probably SMB in our case, could be FTP, S3, pCloud, \u0026hellip;) and pulls in the updates.\nFor regular players we can use rclone - basically a \u0026ldquo;cloud-aware\u0026rdquo; rsync (and what Autosync uses underneath its slick UX).\nAn example invocation I\u0026rsquo;d use:\n$ rclone copy -v --size-only --checkers 2 --transfers 1 --exclude \u0026#34;.*\u0026#34; \\ --order-by \u0026#39;name,ascending\u0026#39; --stats 20s --max-backlog 200000 \\ smb_share_name:/opus_music local:/Volumes/EROS512 The sync command is even more powerful than the copy one, but you do need to be more careful with it - it\u0026rsquo;s easy to accidentally have it delete much more than the files no longer in your library. Like all the config files on your DAP for example 😁\n","date":"27 March 2026","externalUrl":null,"permalink":"/2026/03/27/owning-your-music-part-2/","section":"","summary":"(part 1 here)\nNow that we have a way to get our music and our player(s) picked out, let’s come up with an easy to use workflow and an organisatonial structure that works for us. It’s going to be a lengthy one, so let’s just get started.\nHere’s the gist of the workflow:\nflowchart TD cd((CDs)) cd-- dBPoweramp CD Ripper --\u003eflac bandcamp@{ shape: cloud } bandcamp--\u003eflac subgraph TrueNas direction TD flac@{ shape: documents, label: \"/flac_music\" } opus@{ shape: documents, label: \"/opus_music\" } mp3@{ shape: documents, label: \"/mp3_music\" } plex@{ shape: \"lin-rect\", label: \"PLEX server\" } flac-. lossifier-opus .-\u003eopus flac-. lossifier-mp3 .-\u003emp3 flac===plex end rb@{ shape: card, label: \"rockbox DAP\"} android@{ shape: card, label: \"Android DAP\"} snow@{ shape: card, label: \"snowsky DAP\"} laptop@{ shape: card, label: \"laptop\"} opus-- rclone --\u003erb mp3-- rclone --\u003esnow opus-. autosync .-\u003eandroid plex--\u003elaptop plex--\u003eandroid flowchart TD subgraph legend [Legend] direction TD style legend fill:none smb@{ shape: documents, label: \"SMB share\" } docker@{ shape: \"lin-rect\", label: \"docker service\" } dap@{ shape: card, label: \"player hardware\"} Com@{ shape: braces, label: \"dotted arrows run\\n on a schedule\" } end As you can see the centerpiece of the system is a TrueNas NAS.\n","title":"Owning your music (collection) without losing your mind, part 2","type":"posts"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/tags/configuration/","section":"Tags","summary":"","title":"Configuration","type":"tags"},{"content":"","date":"24 March 2026","externalUrl":null,"permalink":"/tags/design/","section":"Tags","summary":"","title":"Design","type":"tags"},{"content":"Srcery color scheme is awesome, but it\u0026rsquo;s not nearly as popular as some of the other ones. So if you adopted it as your main color scheme, you sometimes gotta do some legwork to make your devenv consistent.\nSo since I\u0026rsquo;m adopting Zellij, I sort of had to make this cheat sheet:\ntype name full name color primary black srcery-palette-primary-black \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary red srcery-palette-primary-red \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary green srcery-palette-primary-green \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary yellow srcery-palette-primary-yellow \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary blue srcery-palette-primary-blue \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary magenta srcery-palette-primary-magenta \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary cyan srcery-palette-primary-cyan \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary white srcery-palette-primary-white \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-black srcery-palette-primary-bright-black \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-red srcery-palette-primary-bright-red \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-green srcery-palette-primary-bright-green \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-yellow srcery-palette-primary-bright-yellow \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-blue srcery-palette-primary-bright-blue \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-magenta srcery-palette-primary-bright-magenta \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-cyan srcery-palette-primary-bright-cyan \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; primary bright-white srcery-palette-primary-bright-white \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary orange srcery-palette-secondary-orange \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary bright-orange srcery-palette-secondary-bright-orange \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary hard-black srcery-palette-secondary-hard-black \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary teal srcery-palette-secondary-teal \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray1 srcery-palette-secondary-xgray1 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray2 srcery-palette-secondary-xgray2 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray3 srcery-palette-secondary-xgray3 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray4 srcery-palette-secondary-xgray4 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray5 srcery-palette-secondary-xgray5 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray6 srcery-palette-secondary-xgray6 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray7 srcery-palette-secondary-xgray7 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray8 srcery-palette-secondary-xgray8 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray9 srcery-palette-secondary-xgray9 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray10 srcery-palette-secondary-xgray10 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray11 srcery-palette-secondary-xgray11 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; secondary xgray12 srcery-palette-secondary-xgray12 \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; \u0026nbsp; ","date":"24 March 2026","externalUrl":null,"permalink":"/2026/03/24/srcery-cheat-sheet/","section":"","summary":"Srcery color scheme is awesome, but it’s not nearly as popular as some of the other ones. So if you adopted it as your main color scheme, you sometimes gotta do some legwork to make your devenv consistent.\nSo since I’m adopting Zellij, I sort of had to make this cheat sheet:\ntype name full name color primary black srcery-palette-primary-black                   primary red srcery-palette-primary-red                   primary green srcery-palette-primary-green                   primary yellow srcery-palette-primary-yellow                   primary blue srcery-palette-primary-blue                   primary magenta srcery-palette-primary-magenta                   primary cyan srcery-palette-primary-cyan                   primary white srcery-palette-primary-white                   primary bright-black srcery-palette-primary-bright-black                   primary bright-red srcery-palette-primary-bright-red                   primary bright-green srcery-palette-primary-bright-green                   primary bright-yellow srcery-palette-primary-bright-yellow                   primary bright-blue srcery-palette-primary-bright-blue                   primary bright-magenta srcery-palette-primary-bright-magenta                   primary bright-cyan srcery-palette-primary-bright-cyan                   primary bright-white srcery-palette-primary-bright-white                   secondary orange srcery-palette-secondary-orange                   secondary bright-orange srcery-palette-secondary-bright-orange                   secondary hard-black srcery-palette-secondary-hard-black                   secondary teal srcery-palette-secondary-teal                   secondary xgray1 srcery-palette-secondary-xgray1                   secondary xgray2 srcery-palette-secondary-xgray2                   secondary xgray3 srcery-palette-secondary-xgray3                   secondary xgray4 srcery-palette-secondary-xgray4                   secondary xgray5 srcery-palette-secondary-xgray5                   secondary xgray6 srcery-palette-secondary-xgray6                   secondary xgray7 srcery-palette-secondary-xgray7                   secondary xgray8 srcery-palette-secondary-xgray8                   secondary xgray9 srcery-palette-secondary-xgray9                   secondary xgray10 srcery-palette-secondary-xgray10                   secondary xgray11 srcery-palette-secondary-xgray11                   secondary xgray12 srcery-palette-secondary-xgray12                   ","title":"Srcery cheat sheet - This should already be up somewhere","type":"posts"},{"content":"","date":"25 February 2026","externalUrl":null,"permalink":"/tags/cloudflare/","section":"Tags","summary":"","title":"Cloudflare","type":"tags"},{"content":"","date":"25 February 2026","externalUrl":null,"permalink":"/tags/devops/","section":"Tags","summary":"","title":"DevOps","type":"tags"},{"content":"For some context: Recently I\u0026rsquo;ve been sharpening my Kubernetes skills by setting up a small 6 node k3s cluster at home. The place I currently live doesn\u0026rsquo;t have a public IP address, so I chose to set up a Cloudflare Tunnel to expose services to the internet.\nI chose to have the cloudflared daemon running on the host machines and the overall setup quick and pain-free. The whole thing seemed to work well, but I noticed that over time (within hours) the tunneled services would start responding more slowly and eventually Cloudflare would display 523 errors.\nThe cloudflare dashboard was showing the tunnels as healthy and the services were working correctly when accessed from LAN (over their .internal domains).\nThe cloudflared logs were showing some errors that didn\u0026rsquo;t suggest any obvious solutions:\n$ journalctl -u cloudflared | grep 21:05 Feb 23 21:05:13 suplex6 cloudflared[3473875]: 2026-02-23T20:05:13Z ERR failed to accept incoming stream requests error=\u0026#34;failed to accept QUIC stream: timeout: no recent network activity\u0026#34; connIndex=3 event=0 ip=198.41.200.13 Feb 23 21:05:13 suplex6 cloudflared[3473875]: 2026-02-23T20:05:13Z ERR failed to run the datagram handler error=\u0026#34;context canceled\u0026#34; connIndex=3 event=0 ip=198.41.200.13 Feb 23 21:05:13 suplex6 cloudflared[3473875]: 2026-02-23T20:05:13Z WRN failed to serve tunnel connection error=\u0026#34;accept stream listener encountered a failure while serving\u0026#34; connIndex=3 event=0 ip=198.41.200.13 Feb 23 21:05:13 suplex6 cloudflared[3473875]: 2026-02-23T20:05:13Z WRN Serve tunnel error error=\u0026#34;accept stream listener encountered a failure while serving\u0026#34; connIndex=3 event=0 ip=198.41.200.13 Feb 23 21:05:13 suplex6 cloudflared[3473875]: 2026-02-23T20:05:13Z INF Retrying connection in up to 1s connIndex=3 event=0 ip=198.41.200.13 Feb 23 21:05:15 suplex6 cloudflared[3473875]: 2026-02-23T20:05:15Z ERR failed to accept incoming stream requests error=\u0026#34;failed to accept QUIC stream: timeout: no recent network activity\u0026#34; connIndex=0 event=0 ip=198.41.200.73 Doing the lazy thing and setting up some staggered cloudflared service restarts in cron didn\u0026rsquo;t really do the trick (and wasn\u0026rsquo;t really a solution). Some googling led me to this github issue and this reddit thread, which got me on the right track.\nThe tunnels by default try to use the QUIC protocol to talk to the Cloudflare edge, falling back to HTTP/2 based on some obscure criteria. QUIC is UDP-based, which can make it harder to troubleshoot, especially when you don\u0026rsquo;t have control of all of the network infrastructure involved. Switching the tunnels to use HTTP/2 seemed like a good thing to try.\nA small gotcha is that it\u0026rsquo;s not a setting you can change in the Cloudflare dashboard (even though it might seem like it could be, in the Speed -\u0026gt; Settings -\u0026gt; Protocol Optimization on the domain dashboard page) - you gotta use the cloudflared setting.\nI was already using Ansible to manage all of the cluster machines, so the change was pretty small - I had to make sure the cloudflared tunnel invocation of the cloudflared.service used the --protocol http2 flag. Here\u0026rsquo;s the (gist of) the ansible task file:\n- name: Create /usr/share/keyrings directory file: path: /usr/share/keyrings state: directory mode: \u0026#39;0755\u0026#39; become: true - name: Add Cloudflare GPG key ansible.builtin.get_url: url: https://pkg.cloudflare.com/cloudflare-public-v2.gpg dest: /usr/share/keyrings/cloudflare-public-v2.gpg mode: \u0026#39;0644\u0026#39; become: true - name: Add Cloudflare repository into sources list ansible.builtin.apt_repository: repo: \u0026#39;deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main\u0026#39; filename: cloudflared state: present become: true - name: Install Cloudflared become: true apt: update_cache: true state: latest name: - cloudflared - name: Populate service facts ansible.builtin.service_facts: no_log: true - name: install the cloudflared service ansible.builtin.command: argv: - /usr/local/bin/cloudflared - service - install - \u0026#34;{{ cloudflared_key }}\u0026#34; when: ansible_facts.services[\u0026#39;cloudflared.service\u0026#39;] is not defined no_log: true become: true ## START OF THE FIX - name: ensure cloudflared uses http2 ansible.builtin.replace: path: /etc/systemd/system/cloudflared.service regexp: \u0026#39;tunnel run\u0026#39; replace: \u0026#39;tunnel --protocol http2 run\u0026#39; after: \u0026#39;\\[Service\\]\u0026#39; become: true register: cloudlflared_protocol_replace - name: conditionally reload the systemd daemon if the cloudflared service file was modified ansible.builtin.systemd: daemon_reload: true when: cloudlflared_protocol_replace.changed become: true ## END OF THE FIX - name: insantly restart cloudflared service ansible.builtin.systemd: name: cloudflared.service state: restarted become: true A cleaner solution would probably be to maintain a config file, but it was pretty late at night and this used the fewest moving parts to get the job done.\nedit: Alternatively you can set the env var TUNNEL_TRANSPORT_PROTOCOL to http2 when starting cloudflared - that\u0026rsquo;s the route I went with on another one of my servers, where the daemon is run inside a docker container based on a pre-made image.\nAnd like that, the problem went away - the communication with the services behind the tunnels have been quick and solid in the last day (knock on wood), and another heisenbug got squashed.\nThe whole story isn\u0026rsquo;t super exciting, I just didn\u0026rsquo;t want to be the \u0026ldquo;nvm, I solved it myself!\u0026rdquo; guy on the internet :D\n","date":"25 February 2026","externalUrl":null,"permalink":"/2026/02/25/fixing-523-errors-with-cloudflare-tunnels/","section":"","summary":"For some context: Recently I’ve been sharpening my Kubernetes skills by setting up a small 6 node k3s cluster at home. The place I currently live doesn’t have a public IP address, so I chose to set up a Cloudflare Tunnel to expose services to the internet.\nI chose to have the cloudflared daemon running on the host machines and the overall setup quick and pain-free. The whole thing seemed to work well, but I noticed that over time (within hours) the tunneled services would start responding more slowly and eventually Cloudflare would display 523 errors.\n","title":"Fixing Cloudflare 523 errors","type":"posts"},{"content":"","date":"25 February 2026","externalUrl":null,"permalink":"/tags/kubernetes/","section":"Tags","summary":"","title":"Kubernetes","type":"tags"},{"content":"Recently, after learning how bad spotify is for its artists I made a deliberate effort to move my family to Tidal - 8/10 decision, would recommend.\nBut to take step further I decided to slowly move towards \u0026ldquo;owning my music\u0026rdquo; - maintaining my own digital collection of music and making sure I have a convenient way of listening to it.\nThere were a number of reasons for it\na push against the you\u0026rsquo;ll own nothing and be happy about it shift in consumer culture moving back to listening to music in a more deliberate way, without phone distractions the joy of using a dedicated device - you can listen to music for hours without worrying about draining your phone\u0026rsquo;s battery, the UX can be better, and you can use good wired headphones I\u0026rsquo;m turning into the \u0026ldquo;old guy yells at cloud\u0026rdquo; guy and I\u0026rsquo;m recently gravitating towards maintaining my own \u0026ldquo;infrastructure\u0026rdquo; Let me share what setup/workflow I ended up with. In this post I\u0026rsquo;ll talk about where I get my music from and what I play it on (plus the reasons behind the decisions). In the next one I\u0026rsquo;ll share how I store and manage the collection in a pain-free and player-agnostic way.\nObtaining music # There\u0026rsquo;s many options here - I went with (mainly) ripping CDs and bandcamp.\nI have sailed the high seas in my life but, surprisingly, ripping CDs turned out to be such a fast, robust and pleasant experience, that I didn\u0026rsquo;t even consider going to the effort of stealing music (: I already had a collection of ~150 favourite CDs from back in the day and was happy to buy a bunch more, most of them used.\nFor ripping software I ended up using dBpoweramp CD Ripper, which is a paid product, but I really like how robust it is and how easy it makes it to combine metadata from multiple sources.\nAs for bandcamp, it\u0026rsquo;s arguably the best way for you to support smaller contemporary artists. It\u0026rsquo;s also good for albums which are harder to obtain in physical form. One of my favourite Polish rap albums was a limited edition and is now being sold second-hand for ~700 EUR used. On bandcamp it costs ~5EUR and virtually all of it goes to the artist.\nPlayer Hardware # For dedicated portable Digital Audio Players, there\u0026rsquo;s two main avenues:\nan Android-based DAP, with mainly touchscreen UX # There\u0026rsquo;s many different options, and they vary when it comes to the latest version of Android they support and the hardware quality - for the most part the audio quality in all of them is more than good enough for any reasonable person.\nMy favourite one\u0026rsquo;s the HiBy M300. It\u0026rsquo;s small, has a good battery life and enough side-buttons to control the music without having to look at the player. Mine runs Android 13 - new enough to feel modern, but I wouldn\u0026rsquo;t do emails or online banking on it :]\nPhysical buttons, custom OS # These are interesting, because there\u0026rsquo;s such a wide range of them, some from 20+ years ago, some just released.\nApart from the screen/button UX and the battery life, they differ based on SD card and bluetooth support. But the main differentiator for me is - is there stable Rockbox support for this thing?.\nRockbox is open-source digital audio player firmware that in return for a pretty steep learning curve offers impressive features on a surprising breadth of hardware.\nI have modded an iPod mini 2 with a brand new battery and a CF card replacing their tiny little hard drive, but by far my favourite Rockbox-based player is the Aigo EROS Q with this gorgeous terminal-inspired theme with a few customizations I made myself.\nPlayer Software # I\u0026rsquo;m sure there\u0026rsquo;s more options here than I\u0026rsquo;ll ever know. I just found some that work for me, but I\u0026rsquo;m not married to my choices here. My criteria are by no means special: nice UI, good search, .opus support, gapless playback, and a library view that encourages you to look around and find something that fits your mood.\nOne thing I do care about and which was an issue with some options, was basically \u0026ldquo;vendor lock-in\u0026rdquo;. This showed up mainly in the way different platforms approach custom playlists - most of them offer a way to create and edit them in-app, but make no effort to make them easy to sync or share with other apps. This might be OK if you have a platform you love made by a developer you trust, but for me the whole point was being \u0026ldquo;independent\u0026rdquo; with the music collection for years to come.\nAndroid player # I went for Poweramp, very much inspired by the video below. Have a watch if you\u0026rsquo;re at all interested, there\u0026rsquo;s some good ideas there - from a clean launcher setup, to a well-working app syncing your music from cloud storage or your own server\nDesktop player # I\u0026rsquo;ve seen people recommend Foobar2000, but it didn\u0026rsquo;t make sense to me at a glance. I already had experience running a Plex server, so plexamp was an easy choice.\nPlex does have the issue of wanting to be the place that manages your playlists, even though it has some of the worst UX I have ever seen for the purpose. I\u0026rsquo;ve seen people on reddit trying to argue that case, but the Plex zealots have no patience for the heretics :D.\nLuckily I managed to get around the playlist issue with how I manage my data\u0026hellip;\n\u0026hellip;but we\u0026rsquo;ll get to that in part 2\n","date":"15 January 2026","externalUrl":null,"permalink":"/2026/01/15/owning-your-music-without-losing-your-mind/","section":"","summary":"Recently, after learning how bad spotify is for its artists I made a deliberate effort to move my family to Tidal - 8/10 decision, would recommend.\nBut to take step further I decided to slowly move towards “owning my music” - maintaining my own digital collection of music and making sure I have a convenient way of listening to it.\nThere were a number of reasons for it\n","title":"Owning your music (collection) without losing your mind, part 1","type":"posts"},{"content":"My name is Jacek Królikowski, I studied Computer Science at the University of Warsaw and worked as a software engineer at Microsoft and a number of startups (including my own) in London and Warsaw.\nI enjoy having a broad understanding of engineering (software and otherwise), but I specialise in the back end side of things, especially using Erlang/Elixir. I\u0026rsquo;m passionate about functional programming, resilient system design, and using the right tool for the job at hand.\nI\u0026rsquo;m always happy to chat about software engineering, maker culture, dawless music making, and BJJ.\nWanna get in touch? Send an email to nietaki(at)gmail(d0t)com or try an appropriate link from the footer ;) I\u0026rsquo;m also available for contract work - send me an email if you if you have a project you think I might be able to help you with.\nWhy \u0026ldquo;almost done\u0026rdquo;? # Back when I first created the blog I had a nasty habit of starting projects and abandoning them once I solved the hard part of the problem :D\nI have since made a point of finishing what I started, so now I like to think of \u0026ldquo;almost done\u0026rdquo; as a metaphor for a software project\u0026rsquo;s life cycle. To misquote Bjarne Stroustrup:\nThe only truly finished projects are the ones nobody uses\n","date":"1 January 2025","externalUrl":null,"permalink":"/about/","section":"","summary":"My name is Jacek Królikowski, I studied Computer Science at the University of Warsaw and worked as a software engineer at Microsoft and a number of startups (including my own) in London and Warsaw.\nI enjoy having a broad understanding of engineering (software and otherwise), but I specialise in the back end side of things, especially using Erlang/Elixir. I’m passionate about functional programming, resilient system design, and using the right tool for the job at hand.\n","title":"About","type":"page"},{"content":"I personally hate it when people post clickbait titles and take their sweet time getting to the point, so let\u0026rsquo;s do this first:\nTL;DR: Some Elixir string operations, most notably String.at/2 work in linear time, as opposed to constant time, like the intuition might suggest. This is because the String module is UTF-8 aware. UTF-8 encodes characters outside of ASCII with more than one byte, so in order to find the n-th character in a string you need to process it from the beginning, you can\u0026rsquo;t just use an offset in memory.\nIn this blogpost I go a bit more in depth about how UTF-8 works and compare some approaches to getting performant results, so even if you knew the tl;dr you might find something interesting here regardless.\nContext # I as doing an algorithmic exercise containing string manipulation in Elixir and saw that even though my approach was correct and seemed pretty optimal, it was timing out on larger inputs - and timing out by a lot. After looking into it a bit I realised I was using String.at/2 as if it was C-style char array access operator, even though on some level I knew it didn\u0026rsquo;t really work like that.\nLet\u0026rsquo;s make it a bit more specific and look at a concrete example. Here\u0026rsquo;s an efficient way of checking if an (ASCII) string is a palindrome in C:\n// code taken from https://rosettacode.org/wiki/Palindrome_detection#C #include \u0026lt;string.h\u0026gt; bool palindrome(const char *s) { int i,l; l = strlen(s); for(i=0; i\u0026lt;l/2; i++) { if ( s[i] != s[l-i-1] ) return false; } return true; } The approach is simple enough, works in pessimistic $O(n)$ time and doesn\u0026rsquo;t do the same work twice (doesn\u0026rsquo;t cross the middle of the string). Here\u0026rsquo;s a naive implementation of this same idea in Elixir:\ndef check_palindrome_with_string_at(s) do len = String.length(s) Range.new(0, div(len, 2) - 1, 1) |\u0026gt; Enum.reduce_while(true, fn index, _still_palindrome -\u0026gt; if String.at(s, index) == String.at(s, len - index - 1) do {:cont, true} else {:halt, false} end end) end Again, simple enough, with a good example of how you can use Enum.reduce_while/2 how in an imperative language you\u0026rsquo;d use for loops. The problem is, like I hinted before, that both String.length/1 and String.at/2 are linear time complexity operations, which brings the whole algorithm to $O(n^2)$. It might not sound so bad, but if you run some benchmarks, you\u0026rsquo;ll see, that running it on an ASCII string 50kB in length takes more than half a minute* That\u0026rsquo;s unacceptable for something that should take essentially as much time as iterating through the string!\n*Note, this is the pessimistic running time - when the long string actually is a palindrome. For a random string that starts and ends with different characters, it would be much faster and not much slower than more optimized approaches.\nWhy is it like this? # a small ASCII / Unicode / UTF8 refresher, feel free to skip ahead\nUnlike ASCII encoding, where every character is encoded in exactly one bytes, and many characters can\u0026rsquo;t be encoded at all, the dominating encoding we use these days is UTF-8, which is a specific implementation of the Unicode Standard.\nUnicode lets us represent virtually any glyph humanity came up with (including emojis, but excluding Klingon), and most of them have their own code point - a number that represents their glyph, usually represented in hexadecimal. The way the standard is set up, the maximum code point value is 1114111 - way more than you could represent in one, or even two bytes.\nThe neat thing is, that the codepoints from 0 to 127 are the same in ASCII and Unicode, and UTF-8 encodes them in just one byte. This means for most simple English texts an algorithm that works for ASCII encoding will happily work on a text encoded with UTF-8.\n{% include img.html src=\u0026quot;/img/asciifull.gif\u0026quot; href=\u0026ldquo;https://www.asciitable.com/\" caption=\u0026ldquo;I have looked up this exact ASCII table probably hundreds of times since my university days:D\u0026rdquo; %}\nSo since many of the codepoints don\u0026rsquo;t fit in a single byte, UTF-8 has a mechanism for expressing multibyte codepoints. It\u0026rsquo;s a prefix encoding, where if the first bit of the byte is a 0, it\u0026rsquo;s a single byte ASCII-like value, if it\u0026rsquo;s a 1, it\u0026rsquo;s the first byte of a longer sequence (with additional logic on top of that). If this already is a little confusing, don\u0026rsquo;t worry, I\u0026rsquo;ll add some examples below.\nTo complicate things a bit more, some codepoints are not \u0026ldquo;standalone\u0026rdquo; but used in combination with others, to build grapheme clusters (also known as graphemes), which have a specific meaning.\nHere are two examples: # Combining e, or U+65 - Latin Small Letter E with U+301 - Combining Acute Accent gives us é, also known as \u0026amp;eacute;\ngrapheme é codepoints U+65 U+301 bytes 0x65 0xCC 0x81 binary 01100101 11001100 10000001 Similarly, this is how you construct the \u0026ldquo;OK hand sign with medium skin tone emoji\u0026rdquo;:\ngrapheme 👌🏽 codepoints 👌 U+1F44C 🏽 U+1F3FD bytes 0xF0 0x9F 0x91 0x8C 0xF0 0x9F 0x8F 0xBD binary 11110000 10011111 10010001 10001100 11110000 10011111 10001111 10111101 You can use the below form to pick apart arbitrary Unicode text, using ardislu\u0026rsquo;s tool\ntext to check What about those palindromes then? # Now we have a good idea how UTF-8 works, and understand that Elixir\u0026rsquo;s String module \u0026ldquo;thinks\u0026rdquo; in terms of graphemes (not bytes). That explains why String.length/1 is $O(n)$ - with variable grapheme byte size it needs to \u0026ldquo;read\u0026rdquo; it all and count all the characters. Similarly String.at/2 - in order to fish out the nth character, we need to know where exactly in the binary it is.\nDoes that mean our precious palindrome checking function can\u0026rsquo;t be made any faster? Of course not!\nIf we\u0026rsquo;re sure we\u0026rsquo;re only handling ASCII characters (like the C program from before was), we can use byte_size/1 and binary_part/3, which make no assumptions about the data held by the binary:\ndef check_palindrome_with_binary_part(s) do len = byte_size(s) Range.new(0, div(len, 2) - 1, 1) |\u0026gt; Enum.reduce_while(true, fn index, _still_palindrome -\u0026gt; if binary_part(s, index, 1) == binary_part(s, len - index - 1, 1) do {:cont, true} else {:halt, false} end end) end This can handle our 50kB palindrome in a mere 2ms! That\u0026rsquo;s 20,000x faster \u0026#x1f680;\nHow about if we just do the check \u0026ldquo;by definition\u0026rdquo; and check if the reversed string is the same as the string forwards?\ndef check_palindrome_with_string_reverse(s) do s == String.reverse(s) end This solution is embarrassingly simple, handles Unicode, and it turns out it\u0026rsquo;s almost as fast - under 3ms on my machine. Which makes sense, because we\u0026rsquo;re just doing linear operations sequentially in this one.\nAnother approach suggested by @codeaddict is using to_charlist/1:\ndef check_palindrome_charlist(s) do erlang_str = to_charlist(s) :string.equal(erlang_str, :lists.reverse(erlang_str)) end It is more than 2x faster than the check_palindrome_with_binary_part solution, but, interestingly, it uses much more memory - around 620kB for the 50k input, whereas check_palindrome_with_binary_part uses under 1kB. Instinctively the bigger memory consumption makes sense, since the function needs to build two very long lists in order to work, instead of using basically just the input data structure.\nI added the full benchmarks for the longest input below\nWas it all for nothing then? # I don\u0026rsquo;t think so \u0026#x1f604;\nOur toy example isn\u0026rsquo;t an end-all-be-all of string manipulation algorithms - the String.reverse/1 wouldn\u0026rsquo;t have worked in my original task. But understanding the trade-offs helped me come up with a solution that\u0026rsquo;s simple, correct and fast.\nBut mainly: understanding the behaviour and performance of the functions we use can be critical. The way the String module works helps the programmers not shoot themselves in the foot in the most common scenarios, but there\u0026rsquo;s plenty of situations where you might want to use the lower-level functions (either exclusively or complementing the high-level ones).\nIn the nietaki/string_playground repo I benchmarked the functions mentioned in this blogpost and a couple of others and added some property-based tests for them - you can use it as a starting point if you want to run some experiments of your own. As an example of how the learnings from this blogpost can be used in a slightly more realistic scenario, I added the Adaptive.slice_beginning/2 function, which can be used to slice chunks off of the beginning of a string without breaking any grapheme clusters, but also without doing the work of scanning the string from the beginning.\nNOTE In order not obscure the main point I didn\u0026rsquo;t cover string normalization. In our palindrome example we might want to normalize the strings before checking their palindromidity, to make sure we\u0026rsquo;re not producing false negatives.\nBenchmarks # You\u0026rsquo;ll find all the benchmarked approaches here.\n##### With input 50000 ##### Name ips average deviation median 99th % check_palindrome_charlist_optimized 1529.12 0.65 ms ±71.22% 0.35 ms 1.88 ms check_palindrome_charlist 1246.36 0.80 ms ±61.33% 0.48 ms 2.08 ms check_palindrome_with_binary_part 610.27 1.64 ms ±9.25% 1.60 ms 2.35 ms check_palindrome_with_string_reverse 391.64 2.55 ms ±10.48% 2.48 ms 3.63 ms check_palindrome_with_graphemes 129.10 7.75 ms ±52.14% 6.58 ms 27.08 ms check_palindrome_with_string_at 0.0264 37840.16 ms ±0.00% 37840.16 ms 37840.16 ms Comparison: check_palindrome_charlist_optimized 1529.12 check_palindrome_charlist 1246.36 - 1.23x slower +0.148 ms check_palindrome_with_binary_part 610.27 - 2.51x slower +0.98 ms check_palindrome_with_string_reverse 391.64 - 3.90x slower +1.90 ms check_palindrome_with_graphemes 129.10 - 11.84x slower +7.09 ms check_palindrome_with_string_at 0.0264 - 57862.00x slower +37839.51 ms Memory usage statistics: Name Memory usage check_palindrome_charlist_optimized 0.62 MB check_palindrome_charlist 0.62 MB - 1.00x memory usage +0 MB check_palindrome_with_binary_part 0.00019 MB - 0.00x memory usage -0.61948 MB check_palindrome_with_string_reverse 5.34 MB - 8.62x memory usage +4.72 MB check_palindrome_with_graphemes 7.25 MB - 11.70x memory usage +6.63 MB check_palindrome_with_string_at 114448.97 MB - 184694.30x memory usage +114448.36 MB **All measurements for memory usage were the same** ","date":"21 April 2023","externalUrl":null,"permalink":"/2023/04/21/elixir-string-operations-seem-slow-and-why-its-a-good-thing/","section":"","summary":"I personally hate it when people post clickbait titles and take their sweet time getting to the point, so let’s do this first:\nTL;DR: Some Elixir string operations, most notably String.at/2 work in linear time, as opposed to constant time, like the intuition might suggest. This is because the String module is UTF-8 aware. UTF-8 encodes characters outside of ASCII with more than one byte, so in order to find the n-th character in a string you need to process it from the beginning, you can’t just use an offset in memory.\n","title":"Elixir string operations seem slow (and why it's a good thing)","type":"posts"},{"content":"","date":"21 April 2023","externalUrl":null,"permalink":"/tags/performance/","section":"Tags","summary":"","title":"Performance","type":"tags"},{"content":"","date":"21 April 2023","externalUrl":null,"permalink":"/tags/unicode/","section":"Tags","summary":"","title":"Unicode","type":"tags"},{"content":"I\u0026rsquo;m a long time power-user of Notion. For the last couple of years I\u0026rsquo;ve been using it for all of my note-taking, work organisation and tracking, online documentation, storing cooking recipes and much more. Recently they introduced a new Buttons feature and it excited many people who thought it would be the missing piece in their workflow organisation.\nI played around with it for a while and I can see the current functionality is a great starting point, but it needs a bit more to actually be useful (to me)\u0026hellip;\nBut first, what can the buttons currently do? When you click a button, it will execute one or more of the \u0026ldquo;steps\u0026rdquo; you can configure for it:\ninsert blocks add a page to a database edit pages in a database show confirmation open a page The insert blocks is a glorified \u0026ldquo;copy and paste\u0026rdquo; button, which will add some content before or after the button itself. The add a page will add a page to a database and can set some fields to values hardcoded within the button. The edit pages will bulk-edit pages in a database, setting their fields to some hardcoded values if they fulfil some (hardcoded) filtering criteria. Show confirmation just adds a confirmation step to guard you against unfortunate misclicks.\nIt seems like the functionalities cover most of the CRUD operations, but you might already see what the weak point is. All of the data for the operations is hardcoded and non-interactive. One exception is date/time related operations, where you can set the field to @Today for example, or use a relative time window for the filtering step.\nI can see some scenarios where this could be useful, but most of them are contrived and could be performed almost as easily without the buttons. With the many things I use Notion for (and some I\u0026rsquo;d like to use it for), I couldn\u0026rsquo;t find a single scenario where I\u0026rsquo;d find the buttons useful.\nHowever, with some relatively small changes, the buttons could gain a lot of power:\nInteractive input # The tool already has a notion (ha-ha) of types. The type of the input could be bound to a specific column of a database (a number, a relation, a \u0026ldquo;select\u0026rdquo; value) and used in the filters/values in the following steps.\nThe input steps from a button could be combined into a modal/popup form, similar to the confirmation step.\nReference to the current page # This is a big one for me.\nIn a CRUD-type application one of the most following operations is \u0026ldquo;do this to the current row\u0026rdquo;. This is not possible in Notion, because the buttons are not context-aware, they have no reference to the page they\u0026rsquo;re put in.\nThis could be as simple as another filtering option for the edit pages step.\nExposing the buttons in page properties # This is an extension of the previous point - once the buttons can perform actions specific to the page they\u0026rsquo;re put in, it\u0026rsquo;s a question of exposing them in a convenient way. If buttons can only appear in the page contents, they\u0026rsquo;re not exposed in the table or card view and they need to be copy-pasted to each of the pages in a database, or otherwise the pages need to be created using templates or other buttons. That\u0026rsquo;s not a robust workflow.\nThere could be another column type for just collections of buttons. You could edit it in a similar way you edit formulas. And voila! You have convenient per-row operations.\nConditional logic # One operation I could use is \u0026ldquo;create if doesn\u0026rsquo;t already exist\u0026rdquo; and it isn\u0026rsquo;t really something you can do in Notion. The best approach that comes to mind is a button that does \u0026ldquo;create a new row and try to remove duplicates\u0026rdquo;.\nThe conditional logic could be a combination the filters and aggregations you can do at the bottom of each table view plus a predicate for the results.\nDisclaimer # I know the Notion people aren\u0026rsquo;t dumb and they build the product iteratively - I\u0026rsquo;m fully expecting to see some of these functionalities get built sooner or later. I\u0026rsquo;m just worried they might see the buttons functionality isn\u0026rsquo;t getting much engagement and axe further development.\nI also know that developing a tool as flexible as Notion isn\u0026rsquo;t an easy thing and requires some compromises. Some of the deliberate compromises were even explained to me by the great customer support team when I made a convoluted enough rollup/formula combination that broke the database semantics :D. But I still believe the functions I\u0026rsquo;m proposing could be implemented within how Notion works right now and the UX of the Buttons.\nI guess we\u0026rsquo;ll just wait and see :)\n","date":"12 April 2023","externalUrl":null,"permalink":"/2023/04/12/notion-buttons-and-what-they-need-to-be-truly-useful/","section":"","summary":"I’m a long time power-user of Notion. For the last couple of years I’ve been using it for all of my note-taking, work organisation and tracking, online documentation, storing cooking recipes and much more. Recently they introduced a new Buttons feature and it excited many people who thought it would be the missing piece in their workflow organisation.\nI played around with it for a while and I can see the current functionality is a great starting point, but it needs a bit more to actually be useful (to me)…\n","title":"Notion Buttons and what they need to be truly useful","type":"posts"},{"content":"","date":"2 February 2020","externalUrl":null,"permalink":"/tags/database/","section":"Tags","summary":"","title":"Database","type":"tags"},{"content":"","date":"2 February 2020","externalUrl":null,"permalink":"/tags/ecto/","section":"Tags","summary":"","title":"Ecto","type":"tags"},{"content":"Below is a reprint of the article I wrote for the Rekki Medium page.\nREKKI builds tools that help people along the restaurant supply chain do their jobs better.\nWe have a free mobile app that lets restaurants order and chat with suppliers, and a web-based tool for suppliers that helps them process orders, manage product codes and catalogues, and communicate more easily with their customers. The majority of REKKI’s backend is written in Elixir, working hand in hand with services written in Go and Node. The Elixir services handle most of what the user sees in the app like the real-time communication with the supplier and the status of the orders.\nWe use Ecto to talk to our databases, which is the de facto standard. It provides a nice, composable data querying and manipulation DSL and an ORM-like way to map information stored in the database to Elixir structs using Schemas. In this post, we’re going to focus on advanced usage of one of its features: \u0026ldquo;preloading\u0026rdquo; of the related data.\nIf you’re already an Ecto power user and know how we usually use preloads, you might want to skip ahead to the “Non-trivial scenario,” or straight to the “Using preload functions” section.\nsetting the scene # Let’s say we’re building a blogging platform, a bit like Medium, where users can write posts and comments. We’d probably model posts and comments as separate tables in a relational db, where each comment belongs to a post, a post can have multiple comments, and both posts and comments belong to individual users.\nIn that scenario, if we wanted to load all comments written by a certain user, we’d write something like this:\nWhen presenting those comments to the user, it might make sense to also display some information about the post they were written under. In that case we’d want to preload the post information like so:\nThere’s more than one way of going about it, and you can also use preloads in more complicated scenarios. The folks at Thoughtbot wrote a helpful blog post about different ways to execute nested preloads using the same classic “posts and comments” example that is also worth reading. But, even the simple case above has some nice properties:\nIt’s not implemented the naive way, where the related Post is fetched for each of the comments, which would be the definition of the n+1 query problem. Instead, all of the related posts are fetched in a single query.\nThe resulting SQL query also isn’t a simple join like\nSELECT * FROM comments c INNER JOIN posts p ON c.post_id = p.id That approach could be sub-optimal in many cases — if we were retrieving 500 comments under the same post, Postgres would give us 500 copies of the post information, one per each of the returned comments. Instead, Ecto splits it into two queries — one for the comments and one for the related posts.\nYou don’t need to execute the preload at the same time you’re performing the original query. If you have the Comment structs already fetched, you can preload the corresponding Posts afterwards:\nNon-trivial scenario # We’ve seen how we’d use preloads to get data from related tables in the same database. We had foreign key relationships documented in the schemas and Ecto supported our use-case out of the box. However, we don’t always have that luxury. For legacy reasons or because of how we decided to separate concerns in our system, the related data could be in a different database, or even provided to us by a different service!\nLet’s say (even if the example is a bit contrived) that each of the Posts (optionally) belongs to a Category, identified by its name. There also exists a service that for a given category name can give us all the relevant category information, such as description, statistics, notable authors and so on. What’s a flexible, efficient and idiomatic way of loading the category information for our Posts?\nOne thing we could do would be to create a virtual field in our Post schema and a function that would populate it for the Post struct:\nThis is a good solution, but it does have some disadvantages. Firstly, we can’t really use it when building our composable queries. Secondly, if we had a collection of Posts we wanted to add the category information to, we might be tempted to do something simple, like this:\nThis would work, but if the collection had multiple posts belonging to the same category, we’d be doing redundant work and fetching the same information multiple times. Plus it’s starting to feel like we’re solving a problem that should already be solved.\nThis is where the preload functions come in.\nUsing preload functions # There is a section of the Ecto documentation that’s easy to miss, that gives a brief explanation of preload functions. They give us the flexibility we want. Let’s see how we’d use them in our case.\nIt seems in the \u0026ldquo;posts in categories\u0026rdquo; scenario, the type of relationship we’re looking for would be belongs_to, where the foreign key is category_name. Let’s model this in our schema:\nNow the only thing we need is the preloading function itself. It will accept a list of category names and fetch the category info from the api. If we want to, we can also parallelize the fetching from the get-go using Tasks.\nThe belongs_to(:category, …) in the Post schema informs Ecto what fields we use to identify the categories (on both the post and category side), allowing it to do the matching for us. The categories don’t necessarily need to be Ecto schemas, a plain map with the right name field would do the trick.\nThe cool thing about this solution is that Ecto takes care of the deduplication for us (so we don’t do redundant fetching). Even better, we can use what we wrote for preloading inside Ecto queries, for prefetching information for individual structs or collections of structs, or nested prefetching, as per the Thoughtbot blog post.\nEver since we discovered preload functions at REKKI, we’ve been using them with good results. While we store most of our data in Postgres, we still have a CouchDB instance we rely on for some things. Preload functions makes working with both of them together much more streamlined. I imagine we’re going to use them even more as the amount of different services that make up the REKKI backend grows.\nAt the same time, I haven’t seen the preload functions used widely by the community. If you weren’t aware of them before, I hope they become a useful tool in your toolbox. I’m also curious if there’s any other use-cases they are good for, feel free to leave some examples in the comments.\nThe examples in this post were written with Ecto 3.x. Take a look at the repo with the example code — all of the code snippets in this post were taken from the repo’s code or tests. Running the tests prints all the SQL queries Ecto is running, which is nice for reference.\nedit: unfortunately the repo got deleted and I don\u0026rsquo;t have a copy of it.\n","date":"2 February 2020","externalUrl":null,"permalink":"/2020/02/02/how-to-use-data-spanning-multiple-data-sources-in-elixir/","section":"","summary":"Below is a reprint of the article I wrote for the Rekki Medium page.\nREKKI builds tools that help people along the restaurant supply chain do their jobs better.\nWe have a free mobile app that lets restaurants order and chat with suppliers, and a web-based tool for suppliers that helps them process orders, manage product codes and catalogues, and communicate more easily with their customers. The majority of REKKI’s backend is written in Elixir, working hand in hand with services written in Go and Node. The Elixir services handle most of what the user sees in the app like the real-time communication with the supplier and the status of the orders.\n","title":"How to use data spanning multiple data sources in Elixir","type":"posts"},{"content":"","date":"2 February 2020","externalUrl":null,"permalink":"/tags/patterns/","section":"Tags","summary":"","title":"Patterns","type":"tags"},{"content":"","date":"1 September 2019","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"1 September 2019","externalUrl":null,"permalink":"/tags/talks/","section":"Tags","summary":"","title":"Talks","type":"tags"},{"content":"Earlier this year I gave a talk at Code BEAM STO about a proposed solution to the ever more real risk of hidden malicious code in our library dependencies. You can watch the whole thing here:\nUPDATE: I have since dropped active development of the Hoplon project, but I hope something like it will become reality when the tech community is ready for it :)\n","date":"1 September 2019","externalUrl":null,"permalink":"/2019/09/01/trust-issues-code-beam-sto-talk/","section":"","summary":"Earlier this year I gave a talk at Code BEAM STO about a proposed solution to the ever more real risk of hidden malicious code in our library dependencies. You can watch the whole thing here:\nUPDATE: I have since dropped active development of the Hoplon project, but I hope something like it will become reality when the tech community is ready for it :)\n","title":"Trust issues: trouble in package paradise - Code BEAM STO 2019 talk","type":"posts"},{"content":"","date":"4 December 2018","externalUrl":null,"permalink":"/tags/gotchas/","section":"Tags","summary":"","title":"Gotchas","type":"tags"},{"content":"I\u0026rsquo;d argue Elixir has relatively few gotchas. It\u0026rsquo;s a simple and consistent language and when you first learn it there\u0026rsquo;s only a few things that are genuinely counter-intuitive and catch you by surprise.\nOne of the examples could be the difference between binaries and charlists and why iex sometimes seems to do weird things to your lists:\niex\u0026gt; l = [19, 7, 16, 119, 97, 116] [19, 7, 16, 119, 97, 116] iex\u0026gt; Enum.drop(l, 1) [7, 16, 119, 97, 116] iex\u0026gt; Enum.drop(l, 2) [16, 119, 97, 116] iex\u0026gt; Enum.drop(l, 3) \u0026#39;wat\u0026#39; One of the other ones comes when you start working with atoms and get a little too trigger-happy with them. What you could hear from your more experienced teammates is something like this:\nYou shouldn\u0026rsquo;t really use String.to_atom/1 on user-supplied data. The BEAM has a limit on how many different atoms you can have and they\u0026rsquo;re not garbage collected!\nWith data coming from outside the system, stick to strings or use String.to_existing_atom/1 instead!\nThis is good advice and the official docs agree. It seems like an easy choice too - if you take the approach that all atoms you expect to see in the system are known at compile time and you won\u0026rsquo;t be creating any new ones during runtime, there\u0026rsquo;s no reason not to do it! You get all the safety and no problems!\nFrom my personal experience it\u0026rsquo;s true the vast majority of time. But there are situations where it could blow up when you least expect it (or just in production). Pull up a chair, let me tell you a story\u0026hellip;\nStoring atoms in Postgres # In Curl we use Postgres and Ecto for some of our data storage. There are some situations where we use the structs representing database rows almost directly, so it makes sense to have the stored data as close to the desired Elixir representation as possible.\nLet\u0026rsquo;s say we have a table representing users and we expect all users to be either \u0026ldquo;active\u0026rdquo; or \u0026ldquo;inactive\u0026rdquo; (whatever it means in the business context). In our Elixir code we\u0026rsquo;d like to see it as something like %User{status: :active} - an atom struct field value.\nPostgres isn\u0026rsquo;t aware or bothered about what Elixir atoms are, but will happily store them for us as strings. We can hide the boilerplate string \u0026lt;-\u0026gt; atom conversion code by creating a custom ecto type. The code and experience we end up with is very similar to the one described by Lew Parker in his A quick Dip into Ecto Types blog post.\nHere\u0026rsquo;s pretty much the code we ended up with:\ndefmodule MyApp.Ecto.AtomType do @behaviour Ecto.Type @type t :: atom @impl true def type(), do: :string @impl true def cast(atom) when is_atom(atom), do: {:ok, atom} def cast(string) when is_binary(string), do: safe_string_to_atom(string) def cast(_), do: :error @impl true def load(value), do: safe_string_to_atom(value) @impl true def dump(atom) when is_atom(atom), do: {:ok, Atom.to_string(atom)} def dump(_), do: :error @spec safe_string_to_atom(String.t()) :: {:ok, atom} | :error defp safe_string_to_atom(str) do try do {:ok, String.to_existing_atom(str)} rescue ArgumentError -\u0026gt; :error end end end Looks pretty reasonable to me, and again: the String.to_exsting_atom/1 should never be a problem, because we have the :active and :inactive atom literals in our codebase and we even have a database constraint to make sure those are the only two values that can be stored in the status column.\nSome time passes\u0026hellip; # Time passes, features are added, refactors happen. One day we deploy to production (code that has behaved well on staging environment for a while and passed all system and unit tests with flying colours) and AppSignal starts notifying us about errors:\ncannot load `\u0026#34;inactive\u0026#34;` as type MyApp.Ecto.AtomType for field `status` in schema (...) What gives?! I know :inactive is an atom that exists and the same code has had no problems in the staging environment! We investigate and come up with a theory that gets confirmed by a two-year-old github comment by José.\nHere\u0026rsquo;s what happened:\nThe :inactive literal was in our codebase, but not in the modules that get used the most frequently on a day-to-day basis. The atoms in them don\u0026rsquo;t get added to the atoms table until the module gets loaded, which happens lazily - when a function in it is called, for example.\nThose modules got loaded when system tests were run in the staging environment (when testing scenarios with users getting deactivated) but not straight away in production. In production the modules weren\u0026rsquo;t loaded (yet) and when an inactive user was retrieved from the database, the field loading errored out breaking some features.\nFixing it # How do we fix it then?\nFirst we make sure the production system works - we go through some user scenario which uses a Module with the atoms we need. All systems are nominal again. Now we can approach the root cause with less urgency.\nThere\u0026rsquo;s a couple of ways of fixing the problem itself. One was suggested by José: Make sure whenever there\u0026rsquo;s a chance we\u0026rsquo;ll need the atoms we\u0026rsquo;re depending on, we\u0026rsquo;ll load their modules. In our case we could be doing this in our Repo module, which always gets used whenever we talk to the database:\n# in lib/my_app/repo.ex @on_load :load_atoms def load_atoms() do relevant_modules = [ MyApp.User, MyApp.DisablingFlow ] Enum.each(relevant_modules, \u0026amp;Code.ensure_loaded?/1) :ok end You can see we\u0026rsquo;re using module\u0026rsquo;s @on_load attribute to hook into the module\u0026rsquo;s lifecycle and \u0026ldquo;cascade\u0026rdquo; the module loading.\nThat\u0026rsquo;s not the only possible solution though. Another, technically simpler solution is just reverting to String.to_atom/1 when we load data from the database. That would be working under the assumption we knew what we were doing when we were storing them in the first place :) That would just be changing the AtomType.load/1 function:\n# Before: def load(value), do: safe_string_to_atom(value) # After: def load(value), do: {:ok, String.to_atom(value)} The latter approach might look a bit naive, but it saves us from the hassle of what looks like manually tracking dependencies between modules.\nThere\u0026rsquo;s more possible approaches here: We could potentially start the system in embedded mode, where all code is loaded at startup, provided we\u0026rsquo;re deploying the app using releases. While simplifying the app\u0026rsquo;s lifecycle like this sounds like a clean solution, I think it\u0026rsquo;s conservative to not to depend on some deployment details for the correctness of your app. Getting started with releases requires some extra work too.\nSo anyways, crisis averted and we learned something!\nEnd notes # This is not actually how we model our users and it\u0026rsquo;s a different entity which made the problem surface - it\u0026rsquo;s just a simplification for the sake of the blog post so I wouldn\u0026rsquo;t have to get too deep into describing how we model Curl\u0026rsquo;s domain.\nThere\u0026rsquo;s probably some even cleaner ways of solving the problem we ran into. If you have some ideas about them, leave a comment below, I\u0026rsquo;d love to hear about it!\n","date":"4 December 2018","externalUrl":null,"permalink":"/2018/12/04/string-to-existing-atom-is-a-double-edged-sword/","section":"","summary":"I’d argue Elixir has relatively few gotchas. It’s a simple and consistent language and when you first learn it there’s only a few things that are genuinely counter-intuitive and catch you by surprise.\nOne of the examples could be the difference between binaries and charlists and why iex sometimes seems to do weird things to your lists:\niex\u003e l = [19, 7, 16, 119, 97, 116] [19, 7, 16, 119, 97, 116] iex\u003e Enum.drop(l, 1) [7, 16, 119, 97, 116] iex\u003e Enum.drop(l, 2) [16, 119, 97, 116] iex\u003e Enum.drop(l, 3) 'wat' One of the other ones comes when you start working with atoms and get a little too trigger-happy with them. What you could hear from your more experienced teammates is something like this:\n","title":"String\u0026#x200B;.to_existing_atom\u0026#x200B;/1 is a double-edged sword","type":"posts"},{"content":"Earlier this year I presented my latest project - Hoplon - at the London Elixir meetup. I\u0026rsquo;m thinking of putting some more work into it over Christmas, so I figured I might gather the materials about it in one place:\nHoplon is an Elixir developer tool that helps you validate your dependencies contain no hidden malicious code. Motivated by horror stories from the JavaScript community such as this hypothetical one and this very real one.\nYou can see the details and a live demo in the recorded talk.\nHere\u0026rsquo;s the slides, with all their happy colourful diagrams:\nHoplon as it is right now is pretty much a proof of concept, but I\u0026rsquo;m thinking of making it a bit more production ready and adding some advanced herd-immunity type features. Keep your fingers crossed!\n","date":"2 December 2018","externalUrl":null,"permalink":"/2018/12/02/i-am-stealing-api-keys-from-your-site/","section":"","summary":"Earlier this year I presented my latest project - Hoplon - at the London Elixir meetup. I’m thinking of putting some more work into it over Christmas, so I figured I might gather the materials about it in one place:\nHoplon is an Elixir developer tool that helps you validate your dependencies contain no hidden malicious code. Motivated by horror stories from the JavaScript community such as this hypothetical one and this very real one.\n","title":"I'm stealing API keys from your site","type":"posts"},{"content":"Towards the end of November I gave a flash talk at the London Elixir Meetup. This time I was talking about the journey from println debugging to proper tracing and bringing Erlang tools to Elixir programmers.\nYou can watch the talk here:\n\u0026hellip;and here are the slides:\nThe resulting Rexbug project is ready to be used but there\u0026rsquo;s still some issues I could use some help on - some should even be suitable for Elixir beginners.\n","date":"10 January 2018","externalUrl":null,"permalink":"/2018/01/10/introducing-rexbug/","section":"","summary":"Towards the end of November I gave a flash talk at the London Elixir Meetup. This time I was talking about the journey from println debugging to proper tracing and bringing Erlang tools to Elixir programmers.\nYou can watch the talk here:\n…and here are the slides:\nThe resulting Rexbug project is ready to be used but there’s still some issues I could use some help on - some should even be suitable for Elixir beginners.\n","title":"Introducing Rexbug - tracing on the shoulders of giants","type":"posts"},{"content":"","date":"9 July 2017","externalUrl":null,"permalink":"/tags/concurrency/","section":"Tags","summary":"","title":"Concurrency","type":"tags"},{"content":"Last year, I saw José Valim give his keynote at the ElixirLive conference in Warsaw, where he talked about the motivation for his new Elixir libraries: GenStage and Flow. Even though I heard about those before, it was the keynote when I \u0026ldquo;got\u0026rdquo; what the libraries were good for and why they were neat - and I decided to play around with them.\nWhen I came back to London I started working on a small project that would make use of GenStage and Flow - Crawlie the crawler. Since then the project took shape and allowed me to learn a bit about concurrent event processing pipelines in Elixir. As I\u0026rsquo;ll probably be ramping down the work around Crawlie now to focus on other things (I have my eye on Riak Core and architecting distributed apps in Elixir in general) I thought it might be a good idea to share what I have learned with others who want to try GenStage and Flow.\nHere are the slides if you want to click along or copy the code:\nIt is a bit dry, but if you want to get started on GenStage and Flow anyways, it might help you bootstrap yourself quicker and avoid some pitfalls. Also: only after watching the recording of the talk afterwards I have noticed how plentiful and distracting all the \u0026ldquo;umm\u0026quot;s and \u0026ldquo;ah\u0026quot;s are - I\u0026rsquo;m going to try to work on it :)\nThere\u0026rsquo;s many more recorded talks on the meetup\u0026rsquo;s SkillsMatter site, touching a diverse set of subjects, from the basics to some really advanced Ecto Sandbox implementation details - have a look around while you\u0026rsquo;re there.\nAs always, I\u0026rsquo;m happy to hear any and all feedback :)\n","date":"9 July 2017","externalUrl":null,"permalink":"/2017/07/09/crawlie-lessons-learned-about-gen-stage-and-flow/","section":"","summary":"Last year, I saw José Valim give his keynote at the ElixirLive conference in Warsaw, where he talked about the motivation for his new Elixir libraries: GenStage and Flow. Even though I heard about those before, it was the keynote when I “got” what the libraries were good for and why they were neat - and I decided to play around with them.\n","title":"Crawlie - Elixir London Meetup presentation","type":"posts"},{"content":" There used to be dates next to each of items - without them the \u0026ldquo;Today\u0026rdquo; makes a bit less sense 😀 pico.css # TIL about pico.css - the minimalistic and zero-friction CSS framework for semantic HTML.\nIt has everything I need to quickly build a data-first site without sacrificing the UX. And I already know it’s going to integrate beautifully with Phoenix LiveView 😈\nThe origins of big- and little-endian terms # TIL that the \u0026ldquo;big-endian\u0026rdquo; and \u0026ldquo;little-endian\u0026rdquo; terms come from Jonathan Swift\u0026rsquo;s \u0026ldquo;Gulliver\u0026rsquo;s Travels\u0026rdquo; (1726), where they represent proponents of the two opposing ways of breaking eggs.\nerlang:term_to_binary/1 compatibility # TIL that Erlang guarantees for the :erlang.term_to_binary/1 format to be compatible accross the span of at least two releases - it is surely going to be possible to decode terms encoded in R18 in R20, but not necessarily in R21.\nIts seems to be common knowledge for some, but I couldn\u0026rsquo;t find any hard sources for this, the closest thing is this SO answer with messed up links. Do let me know if that\u0026rsquo;s not correct.\nYarnbombing # TIL about yarnbombing, also known as \u0026ldquo;guerrilla knitting\u0026rdquo;.\nAs far as I\u0026rsquo;m concerned, the names alone make it awesome.\nncdu # TIL about the ncdu tool - the NCurses Disk Usage cli program. It\u0026rsquo;s like (Win|k)DirStat, only much lighter and more convenient. I\u0026rsquo;ve been looking for it all my life!\nErlang Dirty Schedulers # TIL about Erlang Dirty Schedulers. They seem to be pretty cool if you need to write NIFs.\nWTFPL # TIL that the Do What the Fuck You Want to Public License has not been approved by the OSI board, not because it was vulgar, but because it wasn\u0026rsquo;t a license - it was a dedication to the public domain.\nAlso, if you do want to put your work in public domain, you\u0026rsquo;re probably better off using the CC0 license.\nA 65 byte binary is large # I knew that in Erlang/Elixir each of the processes has its own heap and sending messages between processes copies the terms sent, which is why sending large amounts of data might be inefficient. I also knew that \u0026ldquo;large binaries (strings)\u0026rdquo; live in a shared heap and processes operate on references to them, so sending those around is cheap.\nTIL that any binary longer than 64 bytes is considered a large binary, which was probably a deliberate design choice.\nElixir tuples\u0026rsquo; AST # TIL that the AST of Elixir tuples isn\u0026rsquo;t entirely consistent - the 2-tuples are literals:\nassert {:{}, _line, []} = Code.string_to_quoted!(\u0026#34;{}\u0026#34;) assert {:{}, _line, [1]} = Code.string_to_quoted!(\u0026#34;{1}\u0026#34;) assert {1, 2} = Code.string_to_quoted!(\u0026#34;{1, 2}\u0026#34;) assert {:{}, _line, [1, 2, 3]} = Code.string_to_quoted!(\u0026#34;{1, 2, 3}\u0026#34;) assert {:{}, _line, [1, 2, 3, 4]} = Code.string_to_quoted!(\u0026#34;{1, 2, 3, 4}\u0026#34;) assert {:{}, _line, [1, 2, 3, 4, 5]} = Code.string_to_quoted!(\u0026#34;{1, 2, 3, 4, 5}\u0026#34;) Apparently it is this way to make Keywords literals as well.\n","date":"10 April 2017","externalUrl":null,"permalink":"/til/","section":"","summary":" There used to be dates next to each of items - without them the “Today” makes a bit less sense 😀 pico.css # TIL about pico.css - the minimalistic and zero-friction CSS framework for semantic HTML.\n","title":"Today I Learned","type":"page"},{"content":" July 2019 update: # I have since moved to neovim for all my Linux/OSX work, and I\u0026rsquo;m very happy with it. The information here is probably very outdated, but I\u0026rsquo;m leaving it here for posterity\nYou can see my neovim configuration in my dotfiles\nEarlier this year I joined Mainframe as a backend engineer. I didn\u0026rsquo;t do any real development in elixir before and I wanted to become productive with it ASAP. When it comes to elixir there were some good books to help me understand it better, but I also needed an editor or and IDE that would give me the necessary tools without getting in my way.\nHere\u0026rsquo;s a list of criteria I had for my editor:\nErlang and Elixir syntax highlighting (duh) good in-project search vi(m) keybindings code completion \u0026ldquo;go to source\u0026rdquo; \u0026ldquo;go to documentation\u0026rdquo; tabbed editing and project tree view integration with tests (optional) Before I used mostly IntelliJ IDEs with their vim plugin and Visual Studio with its vim plugin for .NET development, but IDEA didn\u0026rsquo;t seem to be working too well with Elixir yet. I tried sublime with some plugins, but I couldn\u0026rsquo;t get its elixir support to work either and its plugin system felt a little clunky.\nI gave Atom a try and I\u0026rsquo;m glad I did.\nUseful plugins # TL;DR: You might want some plugins. Use sync-settings and use my saved settings to have Atom set up exactly as I have.\nWhile Atom is a great general-purpose editor out of the box, it really shines if you adapt it to your needs using plugins. Unlike some other editors out there, the plugins are all in one central repository and can be easily installed and purged from within Atom interface, without messing with any config files or cryptic commands.\nSome of them are no brainers:\nlanguage-elixir - elixir syntax support language-erlang - Erlang syntax support atom-elixir - elixir autocomplete, go to definition, documentation, \u0026hellip; sync-settings - keeps all your configuration and installed plugins backed up to Gist and synced between machines. My settings backup available here. minimap, minimap-find-and-replace, minimap-git-diff - minimap and some useful extensions for it. vim-mode, ex-mode - vim emulation. Honorable mentions # git-diff-details # Marks edited/inserted/deleted lines to be committed and the modified files themselves in the tree view.\nGreat for keeping track of your changes and making sure there\u0026rsquo;s no weird leftovers for when you do commit.\nprocess-palette # While very simple, helps turn Atom into an IDE. Enables you to create console based tasks to be run in an Atom pane. The tasks can be context aware (for example you can pass path to the current file as an argument), can be integrated with Atom notifications and can make all path/to/file.ext:line references clickable.\nI set it up to run elixir tests in the project (either all or a subset) and recompile the whole project and show generated warnings.\ntodo-show # Enables you to bring up a table with all TODOs in the project, with filtering/search.\nhighlight-line # Highlights the line the cursor is in.\nhighlight-selected # Double-clicking a word in the code highlights it the whole file. Helps spot typos, track where a variable was bound, makes delicious coffee.\nMost used shortcuts # While most plugins are ready to be used out of the box, they can be configured in their settings. I remapped some of my most used shortcuts to be more easily accessible. The shortcuts can be viewed and edited in Settings -\u0026gt; Keybindings.\nHere\u0026rsquo;s a list of shortcuts I find myself using the most:\nOriginal shortcut Custom shortcut Description shift-cmd-p cmd-l Command Palette - a all in one search field for all Atom commands - built-in and custom likewise. cmd-t Fuzzy Finder - toggle file finder. Go to any file in the project, with fuzzy name search. alt-g down alt-j git-diff:move-to-next-diff - move to next modified line in the file alt-g up alt-k git-diff:move-to-previous-diff - move to next modified line in the file cmd-r Symbols View - a fuzzy search accross all functions and macros in the current file cmd-k cmd-n cmd-k Focus next pane. Useful for going mouseless most of the time. cmd-/ (un)comment out current line, regardless of the language There\u0026rsquo;s some commands that I still use all of the time but I didn\u0026rsquo;t feel like creating and learning shortcuts for them was worth it. All those can easily be reached by using Command Palette - I just hit cmd-l and some part of the command description. This way cmd-l -\u0026gt; mix file -\u0026gt; enter runs the tests in currently open file (provided by Process Palette):\nSome other commands are: Sync Settings: Backup, Grammar Selector: Show (for enabling code highlighting in files not saved to disk) or Todo Show: Find In Project.\n","date":"14 November 2016","externalUrl":null,"permalink":"/2016/11/14/atom-as-an-elixir-ide/","section":"","summary":"July 2019 update: # I have since moved to neovim for all my Linux/OSX work, and I’m very happy with it. The information here is probably very outdated, but I’m leaving it here for posterity\nYou can see my neovim configuration in my dotfiles\nEarlier this year I joined Mainframe as a backend engineer. I didn’t do any real development in elixir before and I wanted to become productive with it ASAP. When it comes to elixir there were some good books to help me understand it better, but I also needed an editor or and IDE that would give me the necessary tools without getting in my way.\n","title":"Atom as an elixir IDE","type":"posts"},{"content":"","date":"14 November 2016","externalUrl":null,"permalink":"/tags/editor/","section":"Tags","summary":"","title":"Editor","type":"tags"},{"content":"","date":"14 November 2016","externalUrl":null,"permalink":"/tags/productivity/","section":"Tags","summary":"","title":"Productivity","type":"tags"},{"content":"I\u0026rsquo;ve been a semi-active board game nerd for quite a while now and I find myself playing a wide variety of games, ranging from Jungle Speed to Battlestar Galactica and go. Board games are a huge universe to explore, but there\u0026rsquo;s been one game that me and my friends have been playing for years now and it\u0026rsquo;s still a crowd favourite. It can accommodate virtually any number of players, it\u0026rsquo;s cheap (all you need is some dice), simple to explain, and - last but not least, drunk-people-friendly. If you can still count, you can still play and even if the table is all covered in beer, the dice couldn\u0026rsquo;t care less.\nBut as with most things, the devil\u0026rsquo;s in the details. Most people know a version of the game, but there is one particular one that is both fun and perfectly fair - there\u0026rsquo;s a winning move in every situation. I\u0026rsquo;ve recently been asked to write it up and share, soo\u0026hellip;\nPrerequisites # You need between 2 and 20 people and five 6-sided dice per person. You can use dice cups to conceal your rolls but if the dice aren\u0026rsquo;t too big it\u0026rsquo;s not vital.\nRules # Each player starts with 5 dice. They all roll their dice, look at the result while concealing it from the other players. Next, starting from a randomly chosen player (or the owner of the dice), they take turns, bidding on the \u0026ldquo;state\u0026rdquo; of all the dice on the table. When the bidding war ends one of the players loses one of their dice. If after losing a die they have no more left they get knocked ouf the game. The last person standing wins the game.\nBidding # Each bid consists of a die face (from 1 to 6) and a quantity of dice (between 1 and the count of all the dice on the table). A bid of \u0026ldquo;two threes\u0026rdquo; means that the person claims there\u0026rsquo;s a minimum of two dice with three pips on the face pointing up, between all players - under the cup of the person bidding and everyone else around the table. It translates to \u0026ldquo;There are at least two dice with threes facing up on the table.\u0026rdquo;\nThe next bidding person is the one to the left of the starting one. To continue the bidding, they have to name a bid that is \u0026ldquo;higher\u0026rdquo; than the previous one. The bid is \u0026ldquo;higher\u0026rdquo; if:\nthe quantity of the dice is the same (2, in this scenario), but the named face is higher (4, 5, or 6 in this scenario). \u0026ldquo;Two fours\u0026rdquo; or \u0026ldquo;Two sixes\u0026rdquo; are such bids. the quantity of the dice is larger (3 or more in this scenario) and any face they choose (1 - 6). Examples: \u0026ldquo;three ones\u0026rdquo;, \u0026ldquo;six sixes\u0026rdquo;. The bidding continues clockwise around the table until someone decides to \u0026ldquo;call\u0026rdquo; the previous player\u0026rsquo;s bid.\nCalling a bid # To call a previous player\u0026rsquo;s bid, you literally call them a liar. When that happens all of the players reveal their dice and see if the previous bid was accurate or not. If the bid was accurate, the calling player loses the round, if it wasn\u0026rsquo;t, the bidding player loses the round. Whoever loses the round gives away one of their dice. If they lose all their dice, they get knocked out of the game.\nLet\u0026rsquo;s see an example: There\u0026rsquo;s 5 of us playing, all of us have 5 dice and the bids went as follows:\nA: \u0026ldquo;two threes\u0026rdquo; B: \u0026ldquo;two sixes\u0026rdquo; C: \u0026ldquo;three threes\u0026rdquo; D: \u0026ldquo;four ones\u0026rdquo; E: \u0026ldquo;five sixes\u0026rdquo; A: \u0026ldquo;you\u0026rsquo;re a liar!\u0026rdquo; So what happened here: A saw he didn\u0026rsquo;t have any sixes in his dice and thought amongst all 25 dice of all the players there weren\u0026rsquo;t five or more sixes. All players reveal their dice and lift as many fingers as many sixes they had, to help the counting. Turns out B had two sixes and E had two more, but none of the other players had any - there\u0026rsquo;s only 4 in total. E\u0026rsquo;s bid was too high and they lose the round and one of their dice.\nIf there actually were five sixes on the table, A would have lost the round instead and they would have lost one of their dice.\nAfter a call the next round starts and the first bid is made by the person who lost the last round. If the person who lost the last round got knocked out, the player who knocked them out has the first bid instead.\n\u0026ldquo;Spot-on\u0026rdquo; # As you might have noticed, with just the above rules, there are situations where a player doesn\u0026rsquo;t have a good move. Let\u0026rsquo;s say there\u0026rsquo;s just two people playing and their dice rolls are as follows: A: {1, 3, 6, 6}, B: {2, 4, 6}. The last bid was A saying \u0026ldquo;three sixes\u0026rdquo; - and it\u0026rsquo;s a very good bid indeed. But it seems like B is out of luck: if they call they will lose the round and there is no \u0026ldquo;higher\u0026rdquo; bid that will be successful if A calls - and they probably will. What now?\nThat\u0026rsquo;s why the \u0026ldquo;spot-on\u0026rdquo; was introduced. If you think that the last person\u0026rsquo;s bid is accurate, but there is no higher bid that would work, instead of calling or bidding, you call \u0026ldquo;spot-on\u0026rdquo;. That means you assert the last person\u0026rsquo;s bid was exactly accurate. In the example above, when B says \u0026ldquo;spot-on\u0026rdquo;, they assert that there are exactly three sixes on the table - no more, no less.\nIf the person saying \u0026ldquo;spot-on\u0026rdquo; is right, they win the round and they regain one more die if they have fewer than five dice. Nothing happens to the last person bidding - their bid was correct. If the person saying \u0026ldquo;spot on\u0026rdquo; was wrong, they lose a die instead.\nBeginner/kid-friendly variation # Instead of starting with 5 dice and losing them one by one, everyone starts with 1 die and they get one more every time they lose a round (instead of giving one away). If you find yourself getting your sixth die, you get knocked out of the game. In this version the result of a successful \u0026ldquo;spot-on\u0026rdquo; is also reversed - you lose one die instead of gaining one.\nThis way the worse you\u0026rsquo;re doing, the more information you have about the \u0026ldquo;global table state\u0026rdquo; - you can make your bids with more confidence.\nThat\u0026rsquo;s about it! Give the game a try and see how you like it. Let me know if any part of the explanation is unclear - the game is much simpler to demonstrate than it is to put on paper\u0026hellip;\nAnd remember: \u0026ldquo;three sixes\u0026rdquo; is always a good starting bid and \u0026ldquo;six sixes\u0026rdquo; is a good bid regardless of how many dice there are on the table ;)\n","date":"12 December 2015","externalUrl":null,"permalink":"/2015/12/12/liars-dice-best-rules-variation/","section":"","summary":"I’ve been a semi-active board game nerd for quite a while now and I find myself playing a wide variety of games, ranging from Jungle Speed to Battlestar Galactica and go. Board games are a huge universe to explore, but there’s been one game that me and my friends have been playing for years now and it’s still a crowd favourite. It can accommodate virtually any number of players, it’s cheap (all you need is some dice), simple to explain, and - last but not least, drunk-people-friendly. If you can still count, you can still play and even if the table is all covered in beer, the dice couldn’t care less.\n","title":"Liar's dice (common hand) - best rules variation","type":"posts"},{"content":"I spent the last three months in Dublin, on an internship with Microsoft. The experience was great and I could recommend it to anybody, but that’s not what I wanted to talk about this time. Whenever you move from one place to another there’s a certain amount of know-how that makes your new life easier/better/more predictable and that knowledge usually comes with time. By the end of my internship I felt at home in Dublin and now I’d like to share some tips with you.\nGetting to and from the airport # The airport is about 10 km north from the city centre, but travelling there and back is no hassle. As listed on the airport’s website, there are multiple bus services you could use. I have been using Aircoach‘s services – I don’t think they are the cheapest of the lot, but the buses come frequently, are really reliable, convenient, and hard to miss :) One-way ticket from the city centre costs €7, a return one €12. You don’t need to book the tickets in advance, there’s always a lot of room left. There are also multiple routes, so if you want to go straight from/to Microsoft building 3 or 4 in Sandyford it’s also possible. It takes 40 minutes to get from the airport to the city centre, and another 40 to get to Sandyford.\nTaking the taxi is of course also an option and it costs about €60 to get from the airport to Sandyford and under €30 to get to the city center.\nCommuting # In Dublin you have buses, the Luas, the DART, and dublin bikes at your disposal. Luas and the buses are in the same “ecosystem”, so you can use same tickets for both. Apart from ordinary types of tickets (single, return, monthly etc.) you can also buy a leap card, which is a pre-paid card you can use both for Luas and the buses. You can buy most of the tickets (and top-up your leap card) in machines at Luas stations, but you have to buy the leap card wherever you can buy newspapers.\nSidenote: to be eligible for the student fares you have to get a Student Travel Card, your university ID isn’t enough! The travel card costs €15 and the easiest way to get it to go to Trinity College, house 6, just off the main entrance and have your university ID with you. You should be able to get your card in no more than 10 minutes.\nLuas # Luas is a tram/S-Bahn hybrid in Dublin and it has two unconnected lines: the red and green. It is by far the best way to move around Dublin, so if the place you’ll be working at is relatively close to any of the Luas stations it’s really worth it to look for an apartment close to the Luas line as well. I was commuting from the Harcourt station to Central Park every day and it was as convenient as it gets.\nIf you feel tempted to risk and travel without a valid ticket – I wouldn’t recommend it. The ticket controls are really frequent and you are very likely to encounter one multiple times a day :)\nBuses # I found the Dublin Bus website rather unhelpful – the connection finder doesn’t work too well and you have to know the city very well to determine where the buses of particular routes actually go, since they don’t give you the maps, just list the neighborhoods the routes cross. Additionally, the arrival times of the buses on particular stops (rather than the starting ones) are also impossible to find. To add insult to injury they don’t always stop on the bus stops – I once chased a bus (sober, in daytime) for 3 stops before I acknowledged the driver won’t stop to pick me up.\nBut when you figure out what route you want to use and finally catch a bus, you just tell the driver where you’re going (the fares differ depending on your destination) and pay with cash (coins only!) and find a nice seat at the upper level ;)\nDublin Bikes # There are multiple Dublin Bikes rental stations in the city centre. In some of them you can get a 3-day temporary Dublin Bikes card using your debit or credit card. You can use the card to rent any bike from any of the stations – if you return it within 30 minutes, you won’t be charged any more if not, there is additional cost. You can also apply for a yearly card, all the details on the Dublin Bikes website.\nI wouldn’t really recommend using Dublin Bikes for everyday commute to work – the stations are only downtown and they are all empty in the morning and full in the evening ;). But for running random errands or sightseeing they are convenient and fun to use – great stuff.\nRenting an apartment # This is the biggest issue you (usually) have to face at the beginning of your internship. Your best bets for apartment hunting are daft.ie, adverts.ie and gumtree. Some of the adverts have a minimum lease time (which is more than 3 months, your standard summer internship), a preferred gender or even sexual preference (!) of the tenant. Combine this with the fact most of the apartments are overpriced and not in the area you are looking for and you’re in for a difficult mission. I started searching intensively a week and a half before my temporary hotel room run out and barely found something I liked in time.\nWhen it comes to “good” and “bad” neighborhoods – I didn’t have much experience in that field, but there is some info local people gave me. Historically northern side of Dublin was the “working class” side, and southern was the more exclusive or modern and this still is reflected in the prices. There are also neighborhoods that aren’t always the safest, especially if you were to wave your smartphone around in the evening – I don’t know many details, but I remember the name “Tallaght” came up frequently ;)\nIf you wanted to live close to the green Luas line you would be looking for a place in any of the following districts: Dublin 2, Dublin 6, Dublin 14, Dublin 16, Dublin 18, but still make sure on the map – the districts aren’t too small.\nWhen it comes to price: depending on the location, apartment’s condition and many other factors, the monthly lease could cost anywhere between €450 and €650 / month plus bills. Usually, when moving in, you have to pay a deposit equal to the month’s lease so it might be a good idea to make sure you have some money before you get your first paycheck or two.\nOther tips:\nYou might want to think about sharing an apartment with other people, there are lots of ads for it, especially on adverts.ie. It’s usually a little cheaper than renting a place just for yourself. If you can find a 3 bedroom flat and share it with other interns you know from work – it’s even better ;)\nI wouldn’t recommend renting an apartment to share with the landlords – I haven’t done it, but most of the people who did – didn’t recommend it.\nIf you can choose between UPC and eircom as internet providers – I would choose UPC without a second thought. Other living costs\nThe electricity doesn’t cost much and you can eat healthily for under €40/week. If I were to sort the common general stores from the least to most expensive it would be: Aldi \u0026lt; Tesco \u0026lt; Tesco Express \u0026lt; Spar \u0026lt; Dunnes. A beer in a pub costs €5 and it’s usually good ;) You can get a very nice burger or a doner for €6.\nPlaces to go # Your fellow interns should fill you in on this one, but just to kickstart the pub/club life I would try anything in the Temple Bar area (especially the Porterhouse), South Great George’s Street, Dawson Street or the general area. There are more pubs than you might suspect and on Saturday evening they are all full and very lively – it’s a thing to experience.\nThat’s all that come’s to mind right now, if I remember something I’ll add it here. Feel free to ask questions if you have any!\n","date":"19 October 2012","externalUrl":null,"permalink":"/2012/10/19/interns-guide-to-dublin/","section":"","summary":"I spent the last three months in Dublin, on an internship with Microsoft. The experience was great and I could recommend it to anybody, but that’s not what I wanted to talk about this time. Whenever you move from one place to another there’s a certain amount of know-how that makes your new life easier/better/more predictable and that knowledge usually comes with time. By the end of my internship I felt at home in Dublin and now I’d like to share some tips with you.\n","title":"Intern's guide to Dublin","type":"posts"},{"content":"","date":"10 February 2012","externalUrl":null,"permalink":"/tags/learning/","section":"Tags","summary":"","title":"Learning","type":"tags"},{"content":"","date":"10 February 2012","externalUrl":null,"permalink":"/tags/machine-learning/","section":"Tags","summary":"","title":"Machine-Learning","type":"tags"},{"content":"In the last three months of the last year I had the pleasure of taking part in an online machine learning course, taught by prof. Andrew Ng of the Stanford University. The course is already over, so it might seem old news, but next edition should start any time now. If the topic of machine learning seems interesting to you in any way I can really recommend it.\noverfitting the data As I mentioned, the course is a public version of the Stanford CS229A course, also taught by prof. Ng. The course consists of 18 chapters distributed among 10 weeks. Each chapter contains 1,5 h worth of lectures in short parts, a set of graded review questions and usually a graded programming exercise. The course gives you comprehensive information about techniques practically used in machine learning, allowing the “graduates” to really use the knowledge in their own projects.\nThat’s what I knew before I started the course, but afterwards, despite my high expectations I was surprised by its high quality.\nThe technical side # The e-learning platform, as well as the prepared course materials are top notch – clear and well planned video lectures (with the controls to speed up the replay speed if you get things fast) and their print-ready counterparts. The well-designed review questions and the automatic programming exercise grading system made the course pure pleasure for me.\nThe knowledge # The course was desinged so that as many people as possible can benefit from it. You need basic linear algebra knowledge to understand most of the concepts, so the course provided one review lecture. Similarly, the equations the following problems were based upon (i.e. logistic regression) were explained in a way approachable by people with little mathematical background, but at the same time it was in-depth enough to satisfy the nimble-mined folk’s curiosity. At the same time prof. Ng made sure you understood and got the feeling for the covered topics – which is an advantage in my book.\nOctave # The language you use to solve programming exercises is GNU Octave – a scripting language designed for numerical computations. I haven’t had the opportunity to use it before, but Octave is a language you learn to love fast – it is convenient and fun to use, especially for matrix computations and 2D and 3D plotting. Each programming exercise consisted of a script that solved a real-life problem (like hand-written symbols recognition) step by step using the building blocks filled in by the student.\nOctave using gnuplot to draw pretty nifty 3D plots Octave using gnuplot to draw pretty nifty 3D plots\nAll in all – I really recommend the course: a nice peace of interesting and practical knowledge. If you want to take full advantage of it (watch all the lectures, answer all the review questions and write the programming exercises), it might take a couple of hours weekly, but I’m sure it’s time well spent. The next “semester” should start within a month, but I don’t know the exact date.\nI guess the only disadvantage of the course is that the mailing system and the certificate system don’t handle the polish characters very well:\n","date":"10 February 2012","externalUrl":null,"permalink":"/2012/02/10/ml-class-org-coursera-machine-learning-course/","section":"","summary":"In the last three months of the last year I had the pleasure of taking part in an online machine learning course, taught by prof. Andrew Ng of the Stanford University. The course is already over, so it might seem old news, but next edition should start any time now. If the topic of machine learning seems interesting to you in any way I can really recommend it.\n","title":"ML-class.org course ","type":"posts"},{"content":"","date":"9 February 2012","externalUrl":null,"permalink":"/tags/tdd/","section":"Tags","summary":"","title":"TDD","type":"tags"},{"content":"For my classes I recorded screencasts about TDD, and, because lately I’m focused mostly on c++, I decided to dive into google test instead of the regular jMock and Mockito. Initially I wanted to make it a tutorial showcasing all the tools within the library, but it ended up being a TDD Kata solving example with a short introduction about how to set up the development environment.\nThe narrative is kind of slow, but I guess the main issue for people reading this particular page is that it’s not in English.\n","date":"9 February 2012","externalUrl":null,"permalink":"/2012/02/09/tdd-in-cpp/","section":"","summary":"For my classes I recorded screencasts about TDD, and, because lately I’m focused mostly on c++, I decided to dive into google test instead of the regular jMock and Mockito. Initially I wanted to make it a tutorial showcasing all the tools within the library, but it ended up being a TDD Kata solving example with a short introduction about how to set up the development environment.\n","title":"TDD in C++ (screencast in Polish)","type":"posts"},{"content":"","date":"9 February 2012","externalUrl":null,"permalink":"/tags/testing/","section":"Tags","summary":"","title":"Testing","type":"tags"},{"content":"This thursday, in the lecture hall of the Biology Departament of University of Warsaw, Google organized a meetup with their engineers, celebrating the official launch of their new office in Warsaw. The event started with Joshua Bloch’s, lecture, which was a treat for the attendees, most of whom were MIM UW students, almost filling the room.\nJoshua presented code snippets that don’t do what you might expect them to, by invoking constructs that may lead to unforseen behavior. Majority of those constructs weren’t Java-exclusive and could have been presented in C++ or even python. The well known issues were covered, e.g. String comparison, operator precedence, working with floating-point variables, implicit conversion. But there also were topics I haven’t ever thought about, like regular expressions that match same patterns but differ hugely in their efficiency.\nIt wouldn’t be right for me to try to recreate his lecture, especially that all the “puzzlers” that were presented are also covered in Joshua’s book, which he kept shamelessly plugging;) I’d just like to give you an example of a sick, twisted piece of code you aren’t likely to encounter in real life situation, but you might as well learn from it. Here is the last puzzler of the lecture\n…it seemed pretty straightforward: the first println is a decoy, and in the second one, the 012345 is an (more precisely 5349) integer written in octal, so this program should print:\n66666 548559 Well it doesn’t. The second number in the third line is not an ” int 54321″ but a “long 5432″. And believe me, the “l” at the end looked even more like a “1″ on the slides ;)\nHere are the conclusions of the lecture\nWhen creating a long primitive, use the 123L format instead of 123l. Not everyting is as it seems, so it’s good to understand the tools you’re using. Good IDEs and tools like FindBugs are definitely worth using. You should look into TDD. You should buy Josuha Bloch’s books ;) Maybe I should add, that the 4th item on the list wasn’t mentioned in the lecture but seems like a logical conclusion to me ;)\nThe next lecturer was Walfredo Cirne, who touched the surface of good practices in the design and usage of “cloud” type systems, and said a surprising amount about Agile software development methodologies e.g. – code review, pair programming, and TDD. Pretty interesting, especially the parts the readers could relate to.\nThe lecture ended with Onufry Wojtaszczyk telling us about what he does as a Google engineer and he made it perfectly clear how much he loves it.\nThe subject of Google recruiting and jobs wasn’t really covered, except for the “a Google employee should be smart and get things done” part, so here’s an infographic about getting hired by Google:\n","date":"18 October 2011","externalUrl":null,"permalink":"/2011/10/18/google-engineers-meetup/","section":"","summary":"This thursday, in the lecture hall of the Biology Departament of University of Warsaw, Google organized a meetup with their engineers, celebrating the official launch of their new office in Warsaw. The event started with Joshua Bloch’s, lecture, which was a treat for the attendees, most of whom were MIM UW students, almost filling the room.\nJoshua presented code snippets that don’t do what you might expect them to, by invoking constructs that may lead to unforseen behavior. Majority of those constructs weren’t Java-exclusive and could have been presented in C++ or even python. The well known issues were covered, e.g. String comparison, operator precedence, working with floating-point variables, implicit conversion. But there also were topics I haven’t ever thought about, like regular expressions that match same patterns but differ hugely in their efficiency.\n","title":"Google engineers meetup","type":"posts"},{"content":"Some time ago I described a script you could use to see who’s using your laptop when you’re not around. I sadly (?) didn’t catch any robbers using it, but the whole setup already took nearly 4000 photos, some of which might be a little interesting:\nI’ve got a series or two of one person sitting in front of the computer for quite some time and I could use it to create a timelapse similar to those I made using my crude blackberry app, but on the other hand maybe I shouldn’t be picking on my friends…\n","date":"4 October 2011","externalUrl":null,"permalink":"/2011/10/04/laptop-monitoring-the-aftermath/","section":"","summary":"Some time ago I described a script you could use to see who’s using your laptop when you’re not around. I sadly (?) didn’t catch any robbers using it, but the whole setup already took nearly 4000 photos, some of which might be a little interesting:\nI’ve got a series or two of one person sitting in front of the computer for quite some time and I could use it to create a timelapse similar to those I made using my crude blackberry app, but on the other hand maybe I shouldn’t be picking on my friends…\n","title":"laptop monitoring - the aftermath","type":"posts"},{"content":"I was talking to a friend of mine and we came up with the idea of “America’s next top model” and Top Coder crossover. The “top model” shows are slowly losing their popularity, but our idea is still scary on the wallpaper of my desktop, motivating me to hard work:\nHalf of the idea and the whole design is courtesy of Jakub Rostkowski, and the wallpaper comes in three different sizes: 1440×900, 1600×1200, 1900x1200,\n","date":"21 September 2011","externalUrl":null,"permalink":"/2011/09/21/next-top-coder-wallpaper/","section":"","summary":"I was talking to a friend of mine and we came up with the idea of “America’s next top model” and Top Coder crossover. The “top model” shows are slowly losing their popularity, but our idea is still scary on the wallpaper of my desktop, motivating me to hard work:\nHalf of the idea and the whole design is courtesy of Jakub Rostkowski, and the wallpaper comes in three different sizes: 1440×900, 1600×1200, 1900x1200,\n","title":"Next Top Coder wallpapers","type":"posts"},{"content":"Some time ago, inspired by…\n…also known as the “Do not fuck with a hacker’s machine” clip, and the fact I started using Debian exclusively on my netbook, I decided to make preparations in advance for a dire situation in which my laptop is stolen/captured by insurgents, and retrieve it easier or even play a prank on them. These were my postulates:\nI should always know the IP of my netbook, assuming it’s connected to the internet If someone other than me is using my computer, he should be well (and discretely) photographed. The pictures of people misusing my computer should be quickly and seamlessly transmitted to my server. Finding the computer # This part is obvious and was even covered in the Defcon clip – simply using dyn-dns. Which is why we create an account on their website, choose a free domain and set up your computer to report its IP to dyndns’ servers.\ntaking photos and uploading them to the server # The idea here is as follows: as soon as the system loads the laptop should take pictures of the user every x seconds, until the user logs on to my account. Otherwise (if user doesn’t log in or uses the especially prepared “guest” account), it takes the pictures until the computer is turned off. After each snapshot the script tries to synchronize the pictures with those already uploaded to the server – just in case the internet connection wasn’t available before.\nWe’re going to use the fswebcam (also available in debian packages) and rsync programs, and the magic of the /etc/init.d directory.\nFor our convenience the sript was divided into 4 parts:\nnietaki@dblue:~$ sudo touch /etc/init.d/monitoring.sh nietaki@dblue:~$ mkdir -p ~/.webcam/sav nietaki@dblue:~$ cd .webcam nietaki@dblue:~$ touch monitor.sh killer.sh webcontrol.sh The monitor.sh file contains all the interesting stuff:\nAs you can see, my server (my_server_host.dyndns.info) has a dedicated “uploader” account, just for receiving the uploaded photos and saving them in the right place. In my case rsync doesn’t need the uploader’s password, because on my server i edited the /home/uploader/.ssh/authorized_keys and added the public key of my laptop’s “nietaki” account. More information on that here. The rest is one infinite loop and parameters for rsync and fswebcam. Additionally you might want to make sure the file mode bits of monitor.sh are set up right, so that only your account can read it, using chmod;)\nThe killer.sh is there only to enable you to kill the monitor.sh when the time is right:\n#!/bin/bash # killing the monitor ps aux | grep monitor.sh | grep bash |awk '{print $2}' |xargs kill 2\u0026gt; /dev/null exit 0 …and I’m almost sure you can do it in a much nicer way;)\nNow let’s see /etc/init.d/monitoring.sh\n#!/bin/bash su -c \u0026quot;/home/nietaki/.webcam/webcontrol.sh $1\u0026quot; nietaki \u0026amp; exit 0 You can see right away this script just invokes webcontrol.sh with the same argument it itself gets, but for added safety, using the “nietaki” account already. I won’t delve into how the /etc/init.d directory works, it’s very well covered in the very same tutorial I have used, the important thing is, using update-rc.d command you can ask the system to run any given file located in /etc/init.d, with the “start” argument during system startup and “stop” during system shutdown.\nAnd the last file – webcontrol.sh:\nIf all our .sh files have the ‘chmod +x’ for the appropriate users, the whole system should be functional by now;) One last thing we have to take care of is having our privacy protected when we ourselves log on to the computer. In Gnome you can do it by clicking through System-\u0026gt;Preferences-\u0026gt;Startup Applications, where you can add a new “startup program”:\nAnd that is all! You can also make sure the “guest”‘s account our hypothetical thief would be logging onto isn’t password protected, is fully operational and has all sorts of interesting stuff on the Desktop: funny images, videoclips, porn. This way we can get more shots of the current owner of our beloved machine;).\nMy eee netbook has a little navy blue diode which lights up when a picture is taken, but you could easily mask it using black paint or some electrical tape.\nLast by not least, a sample photo taken by the script:\nOne thing i feel is missing from the whole setup is being able to ssh to the laptop even if it’s in somebody elses hands, behind their firewall. But right now I don’t have an idea how I can do it well…\nPS. I know macs have a program that does all that and much more, but the more interesting (and cheaper) thing is to write it yourself.\nPS2. If you are going to be viewing the pictures more frequently you might want to modify the script so that the pictures get a new directory every now and then – any graphical interface will be loading their previews much faster this way. But I’ll leave it as an exercise to the reader (as many of my university’s textbooks say) and leave the script in an “almost done” state ;)\n","date":"24 April 2011","externalUrl":null,"permalink":"/2011/04/24/defensive-photobooth/","section":"","summary":"Some time ago, inspired by…\n…also known as the “Do not fuck with a hacker’s machine” clip, and the fact I started using Debian exclusively on my netbook, I decided to make preparations in advance for a dire situation in which my laptop is stolen/captured by insurgents, and retrieve it easier or even play a prank on them. These were my postulates:\n","title":"defensive photobooth a.k.a Do Not Fuck With a Hacker's Machine","type":"posts"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]