[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fudt331erweua":3},{"coverImage":4,"description":9,"publishedAt":10,"slug":11,"tags":12,"title":19,"bodyHtml":20,"headings":21},{"alt":5,"height":6,"url":7,"width":8},"Skinamp's Spotify, Winamp, iPod and Windows Media Player 2000 skins, and two visualizers, playing demo music",929,"https:\u002F\u002Fcdn.sanity.io\u002Fimages\u002Fq2wxb7pr\u002Fproduction\u002Fd575d1b247fd5bf8f5072150e4e21e666236c5bf-1600x929.webp",1600,"Skinamp is a Spotify player for Omarchy, written in Rust. It lives in the bar, runs in about 27 MB, and can look like Winamp, an iPod, or Windows Media Player 2000.","2026-09-28T22:44:33.064Z","building-a-spotify-player-you-can-skin",[13,16],{"slug":14,"title":15},"open-source","Open Source",{"slug":17,"title":18},"side-projects","Side Projects","Building a Spotify Player You Can Skin","\u003Cp>I miss the Windows Media Player visualizers. Anyone who used a PC in the 2000s knows the ones: Alchemy, Battery, the bars pulsing along to whatever you had on. They were pointless and they were great. When I&#x27;m coding I usually have music on and a terminal open, and I wanted something like that sitting in the corner of the screen again.\u003C\u002Fp>\u003Cp>I run Omarchy, an Arch Linux setup built around Hyprland, on a M2 Pro Mac mini under Asahi Linux. That turned out to be the real problem. Before I could have visualizers, I needed a Spotify player that worked at all. So I built Skinamp.\u003C\u002Fp>\u003Ch2 id=\"what-it-does\">What it does\u003C\u002Fh2>\u003Cp>Skinamp puts a music icon in your Omarchy bar. Hover over it and a mini player shows the cover, progress, the current lyric line, and controls for play, skip, shuffle, repeat and like. Click it and the full player opens.\u003C\u002Fp>\u003Cp>The full player is a terminal app with eleven looks. Press \u003Ccode class=\"inline-code\">t\u003C\u002Fcode> to step through them:\u003C\u002Fp>\u003Cul>\u003Cli>Your Omarchy theme, a classic view, and one modeled on today&#x27;s Spotify app\u003C\u002Fli>\u003Cli>Winamp, iTunes and a click-wheel iPod\u003C\u002Fli>\u003Cli>Zune, and Windows Media Player 11 and 2000\u003C\u002Fli>\u003Cli>A lyrics skin that follows the song, with the current line highlighted\u003C\u002Fli>\u003Cli>A visualizer view\u003C\u002Fli>\u003C\u002Ful>\u003Cp>The visualizers were the whole point. There are seven terminal styles modeled on WMP&#x27;s, including bars, scope, fire storm, alchemy and battery, plus five GPU shaders drawn at full resolution. They react to the actual audio, not a fake animation. If you know a little WGSL you can write your own: drop a shader file in the config folder and it shows up in the rotation, reloading each time you save.\u003C\u002Fp>\u003Ch2 id=\"why-most-spotify-players-dont-work-on-a-mac\">Why most Spotify players don&#x27;t work on a Mac\u003C\u002Fh2>\u003Cp>Omarchy on Apple Silicon is an ARM machine, and most of the Spotify ecosystem on Linux assumes x86:\u003C\u002Fp>\u003Cul>\u003Cli>Spotify&#x27;s official Linux app has no ARM build.\u003C\u002Fli>\u003Cli>The Spotify web player needs a DRM component (Widevine) that also isn&#x27;t available there.\u003C\u002Fli>\u003Cli>Omarchy&#x27;s own music shortcut falls through to an installer that can&#x27;t work on ARM.\u003C\u002Fli>\u003Cli>Several of the Spotify plugins for Omarchy build on the official app, or on tools you install separately that often aren&#x27;t packaged for ARM.\u003C\u002Fli>\u003C\u002Ful>\u003Cp>My first attempt was glue. I set up ncspot, a terminal Spotify client, which meant building it from source and patching it so the media keys and the bar controls worked. Then I switched to another client, spotify-player, which ran as a background service with a mini player in the bar. It worked, but I was wrapping someone else&#x27;s program in shell scripts, and every feature I wanted ran into the edges of what it exposed. I couldn&#x27;t get at the audio for visualizers, I couldn&#x27;t control how fast it responded, and I couldn&#x27;t skin it the way I wanted.\u003C\u002Fp>\u003Cp>So I started over in Rust, on the same foundation those players use underneath.\u003C\u002Fp>\u003Ch2 id=\"how-i-built-it\">How I built it\u003C\u002Fh2>\u003Ch3 id=\"plan-first\">Plan first\u003C\u002Fh3>\u003Cp>Before writing any code, I wrote a design document: every major decision with the reasoning behind it, the architecture, and milestones with acceptance criteria. A few of the decisions that shaped everything else:\u003C\u002Fp>\u003Cul>\u003Cli>\u003Cstrong>One background process owns everything.\u003C\u002Fstrong> A single daemon handles the Spotify session, audio, media keys and notifications. The bar icon, the terminal player and the visualizer window are thin clients that talk to it over a local socket. That&#x27;s why an update, or a crash in the UI, never stops the song.\u003C\u002Fli>\u003Cli>\u003Cstrong>No web API calls for playback.\u003C\u002Fstrong> Play, pause and skip go straight to the local player and never touch Spotify&#x27;s web API. The web API has rate limits, and other plugins&#x27; users kept hitting them.\u003C\u002Fli>\u003Cli>\u003Cstrong>Audio through PipeWire, never raw ALSA.\u003C\u002Fstrong> On Apple Silicon, the speakers depend on a tuned processing chain that raw ALSA would bypass.\u003C\u002Fli>\u003C\u002Ful>\u003Ch3 id=\"prove-the-hardest-part-first\">Prove the hardest part first\u003C\u002Fh3>\u003Cp>The first milestone was a spike with one job: prove the core works on the Mac, measured against numbers I set in advance. Asahi has quirks x86 Linux doesn&#x27;t. For example, its kernel uses 16 KB memory pages instead of 4 KB, and jemalloc, a popular memory allocator, crashes on it when built on a normal 4 KB machine. So I avoided it from the start.\u003C\u002Fp>\u003Cp>I measured everything: CPU while paused (zero), CPU while playing (about 1% of one core), memory, and how long each command takes to show up as an event. Two results needed work.\u003C\u002Fp>\u003Cp>\u003Cstrong>Memory.\u003C\u002Fstrong> The first version used 47 MB while playing. About 20 MB of that was glibc&#x27;s allocator keeping a separate memory pool for each thread. Capping it at two pools, and running two worker threads instead of one per CPU core, brought it to 25 MB with no change in latency. With everything since added, the background player sits at about 27 MB.\u003C\u002Fp>\u003Cp>\u003Cstrong>Pause.\u003C\u002Fstrong> My target was a pause that goes quiet within 100 ms. With the standard audio output it took about 200 ms, because stopping waited for all buffered audio to play out first. That&#x27;s long enough to feel. I wrote a custom audio output that drops the buffer instead of draining it, and made pause, skip and seek interrupt a write already in progress. Typical pause dropped to under 30 ms.\u003C\u002Fp>\u003Ch3 id=\"learn-from-the-competitions-bug-reports\">Learn from the competition&#x27;s bug reports\u003C\u002Fh3>\u003Cp>Before building the player UI, I went through the other Spotify plugins on the Omarchy marketplace and read their issue trackers. The most popular one had 108 issues in six weeks, and they clustered: installs that broke after an Omarchy update, playback that stopped after a reconnect, one shared Spotify app ID so every user hit the same rate limit, and settings that reset every restart. I turned that list into requirements. Skinamp&#x27;s installer doesn&#x27;t depend on shell internals that change between versions. The session supervises and restarts itself. Your settings live in your own config files.\u003C\u002Fp>\u003Ch3 id=\"then-make-it-fun\">Then make it fun\u003C\u002Fh3>\u003Cp>With the core solid, I built the skins and visualizers. Each skin is a layout over the same library, playlist and search views, so adding one doesn&#x27;t mean rewriting the player. I also built a demo mode that plays made-up music without signing in. It&#x27;s how the marketplace screenshots were made, and how anyone can try Skinamp without an account.\u003C\u002Fp>\u003Ch3 id=\"ship-it-properly\">Ship it properly\u003C\u002Fh3>\u003Cp>Getting listed on the \u003Ca href=\"https:\u002F\u002Fplugins.omarchy.org\u002Fplugin.html?id=io.github.idrewlong.skinamp\" target=\"_blank\" rel=\"noopener noreferrer\">Omarchy plugin marketplace\u003C\u002Fa> meant a review, and the reviewer was thorough, in a good way. The installer now:\u003C\u002Fp>\u003Cul>\u003Cli>Downloads prebuilt binaries only if they match a checksum pinned in the plugin itself, and the release builds come with GitHub build attestations\u003C\u002Fli>\u003Cli>Installs everything in your home folder, with no \u003Ccode class=\"inline-code\">sudo\u003C\u002Fcode>\u003C\u002Fli>\u003Cli>Records every file it puts in place, and never overwrites or deletes a file it didn&#x27;t put there\u003C\u002Fli>\u003C\u002Ful>\u003Cp>GitHub Actions builds releases for both x86_64 and ARM, so Skinamp works on regular PCs too, not just Macs.\u003C\u002Fp>\u003Ch2 id=\"by-the-numbers\">By the numbers\u003C\u002Fh2>\u003Cul>\u003Cli>About 15,000 lines of Rust across four crates\u003C\u002Fli>\u003Cli>27 MB for the background player, 20 MB for the player window\u003C\u002Fli>\u003Cli>Pause in under 30 ms, down from about 200 ms\u003C\u002Fli>\u003Cli>Zero web API calls to play, pause or skip\u003C\u002Fli>\u003Cli>From the first Rust commit to a marketplace listing in three days\u003C\u002Fli>\u003C\u002Ful>\u003Ch2 id=\"standing-on-someone-elses-work\">Standing on someone else&#x27;s work\u003C\u002Fh2>\u003Cp>The hard part of any third-party Spotify player is talking to Spotify at all, and I didn&#x27;t write that. Skinamp is built on librespot, the open-source Spotify Connect library. Without it, this project doesn&#x27;t exist. Synced lyrics come from LRCLIB, an open lyrics database. And the skins are love letters to software that Nullsoft, Apple and Microsoft shipped twenty years ago.\u003C\u002Fp>\u003Ch2 id=\"what-it-doesnt-do-yet\">What it doesn&#x27;t do (yet)\u003C\u002Fh2>\u003Cp>It needs Spotify Premium. That&#x27;s Spotify&#x27;s rule for every third-party player, not mine. There&#x27;s no podcast or audiobook support, and the full-resolution visualizers need a terminal that can show images. Foot, Omarchy&#x27;s default terminal, can.\u003C\u002Fp>\u003Ch2 id=\"try-it\">Try it\u003C\u002Fh2>\u003Cp>Skinamp is on the \u003Ca href=\"https:\u002F\u002Fplugins.omarchy.org\u002Fplugin.html?id=io.github.idrewlong.skinamp\" target=\"_blank\" rel=\"noopener noreferrer\">Omarchy plugin marketplace\u003C\u002Fa>. If you run Omarchy, on a Mac or a PC, install it with:\u003C\u002Fp>\u003Cfigure class=\"code-block\">\u003Cpre class=\"shiki plain\">\u003Ccode>omarchy plugin add https:\u002F\u002Fgithub.com\u002Fidrewlong\u002Fskinamp --enable\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Ffigure>\u003Cp>Then hover over the music icon and click \u003Cstrong>Set up\u003C\u002Fstrong>. If you want to look around first, \u003Ccode class=\"inline-code\">skinamp tui --demo\u003C\u002Fcode> opens the player with made-up music, no account needed. Press \u003Ccode class=\"inline-code\">t\u003C\u002Fcode> and flip through the skins until you reach the visualizers.\u003C\u002Fp>\u003Cp>\u003C\u002Fp>\u003Cfigure class=\"body-image\">\u003Cimg src=\"https:\u002F\u002Fcdn.sanity.io\u002Fimages\u002Fq2wxb7pr\u002Fproduction\u002F23c7836892f4c7a6d9a1eff60cc838b5cc222446-880x495.gif?w=1400&amp;fit=max&amp;fm=webp&amp;q=80\" alt=\"gif skinamp\" width=\"880\" height=\"495\" loading=\"lazy\" decoding=\"async\" \u002F>\u003C\u002Ffigure>\u003Cp>The code is on \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fidrewlong\u002Fskinamp\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub\u003C\u002Fa>. If you make a visualizer, I&#x27;d genuinely love to see it.\u003C\u002Fp>",[22,26,29,32,36,39,42,45,48,51,54,57],{"id":23,"text":24,"level":25},"what-it-does","What it does",2,{"id":27,"text":28,"level":25},"why-most-spotify-players-dont-work-on-a-mac","Why most Spotify players don't work on a Mac",{"id":30,"text":31,"level":25},"how-i-built-it","How I built it",{"id":33,"text":34,"level":35},"plan-first","Plan first",3,{"id":37,"text":38,"level":35},"prove-the-hardest-part-first","Prove the hardest part first",{"id":40,"text":41,"level":35},"learn-from-the-competitions-bug-reports","Learn from the competition's bug reports",{"id":43,"text":44,"level":35},"then-make-it-fun","Then make it fun",{"id":46,"text":47,"level":35},"ship-it-properly","Ship it properly",{"id":49,"text":50,"level":25},"by-the-numbers","By the numbers",{"id":52,"text":53,"level":25},"standing-on-someone-elses-work","Standing on someone else's work",{"id":55,"text":56,"level":25},"what-it-doesnt-do-yet","What it doesn't do (yet)",{"id":58,"text":59,"level":25},"try-it","Try it"]