DEV Community

AngryBeat
AngryBeat

Posted on Edited on

Lofi Radio for Coding: Build a Terminal Player with Python and mpv

Plenty of us keep lofi radio for coding running in a browser tab that slowly eats memory next to twelve other tabs. This weekend project replaces that tab with a tiny terminal player: about 60 lines of Python, mpv doing the heavy lifting, and now playing info pulled straight from the stream.

You will learn how internet radio streams actually expose track titles, how to deal with playlist files, and how to keep a player alive when a stream drops.

How an internet radio stream differs from a file

A radio stream is an HTTP response that never ends. Most stations run Icecast or Shoutcast and push MP3 or AAC frames continuously. Two details matter for us:

  • Playlist wrappers. Stations often publish .m3u or .pls files instead of a direct stream. These are plain text files that point to the real URL.
  • ICY metadata. If a client sends the header Icy-MetaData: 1, the server replies with icy-metaint: N, then inserts a small metadata block every N bytes of audio. That block contains something like StreamTitle='Artist - Track';.

That is all "now playing" is on most stations. No API, just bytes in the audio stream.

Reading ICY metadata in Python

Here is a minimal reader using only requests:

import re
import requests

def now_playing(url: str) -> str | None:
    headers = {"Icy-MetaData": "1", "User-Agent": "lofi-term/0.1"}
    with requests.get(url, headers=headers, stream=True, timeout=10) as r:
        metaint = int(r.headers.get("icy-metaint", 0))
        if not metaint:
            return None
        raw = r.raw
        raw.read(metaint)                 # skip audio
        length = raw.read(1)[0] * 16      # metadata length byte
        meta = raw.read(length).decode("utf-8", "replace")
        m = re.search(r"StreamTitle='(.*?)';", meta)
        return m.group(1) if m else None
Enter fullscreen mode Exit fullscreen mode

Notice that the length byte is multiplied by 16. That trips up almost everyone the first time.

Resolving .pls and .m3u files

def resolve(url: str) -> str:
    if not url.lower().endswith((".pls", ".m3u")):
        return url
    text = requests.get(url, timeout=10).text
    for line in text.splitlines():
        line = line.strip()
        if line.startswith("File1="):
            return line.split("=", 1)[1]
        if line.startswith("http"):
            return line
    raise ValueError("no stream found")
Enter fullscreen mode Exit fullscreen mode

We leave .m3u8 alone on purpose. That is HLS, a segmented format, and mpv handles it natively.

Letting mpv play, with automatic reconnects

Decoding audio yourself is a rabbit hole. mpv already does it well, and it accepts flags that keep long-running streams stable:

import subprocess, time

def play(url: str):
    while True:
        proc = subprocess.run([
            "mpv", "--no-video", "--really-quiet",
            "--cache=yes", "--demuxer-max-bytes=2MiB",
            "--stream-lavf-o=reconnect_streamed=1,reconnect_delay_max=10",
            resolve(url),
        ])
        if proc.returncode == 0:
            break
        print("stream dropped, retrying in 5s")
        time.sleep(5)
Enter fullscreen mode Exit fullscreen mode

A separate thread can call now_playing() every 30 seconds and print the title. Keep it gentle: opening a second connection every few seconds is rude to small stations that pay for bandwidth.

Picking streams and keeping CPU low

For a coding soundtrack, a 128 kbps MP3 or a 64 kbps AAC stream is plenty, and mpv with --no-video usually sits well under 2 percent CPU on a modern laptop. A few habits help:

  • Prefer HTTPS stream URLs when the station offers them.
  • Store favorites in a small TOML file, keyed by a short name, so lofi-term chill just works.
  • Respect the station: identify your client with a real User-Agent and do not scrape aggressively.

If you need real-world URLs to test against, browse these live lofi radio streams to test with. You will quickly see the mix of MP3, AAC and HLS that exists in the wild, which is exactly what makes a player robust.

Why lofi radio for coding works so well

There is a reason lofi radio for coding became its own genre of background noise. The tracks sit around a steady, unhurried tempo, they rarely have lyrics, and the mix is intentionally soft, with vinyl crackle and rounded highs. Nothing in it competes with the part of your brain that is reading a stack trace.

A live stream also removes a decision. With a playlist you are tempted to skip, reorder and curate, which is just another small context switch. With radio you press play once and stop thinking about it. That is exactly why a lightweight terminal player is worth building: it gives you the stream without a browser full of distractions sitting one tab away.

If you share your setup with teammates, the same script works on a home server or a Raspberry Pi wired to office speakers. Just keep the reconnect loop polite, with a delay between retries, so you are not hammering a station during an outage.

FAQ

What is the best format for lofi radio for coding?
Any stable stream works. AAC at 64 to 128 kbps sounds good and uses little bandwidth, and MP3 is the most compatible.

Why does my stream show no track title?
The server may not send icy-metaint, or the station does not populate StreamTitle. HLS streams usually carry metadata differently, in ID3 tags inside segments.

Can I do this without mpv?
Yes, ffplay or VLC's cvlc work too. mpv just has the cleanest flags for headless playback.

Is it legal to stream internet radio in my own player?
Listening to a publicly offered stream is normal use. Re-broadcasting or recording for distribution is a different story, so check the station's terms.

Wrapping up

Sixty lines, one dependency and your browser gets a little lighter. Fork it, add a TUI with textual, and let the beats carry you through the next refactor.

Top comments (0)