#!/usr/bin/env stack > -- stack script --resolver lts-22.6 --package http-client --package http-client-tls --package bytestring Icecast → Gopher relay. Always streams; emits no gophermap and no terminator. Pair this with a sibling icecast.txt that explains how to pipe it (`nc gopher.someodd.zip 70 <<< /applets/icecast.lhs | mpv -`). The default upstream is `https://radio.someodd.zip/` (port 443 implicit). Override with the GOPHER_RADIO_URL env var if needed without editing. Important details inside `main`: - hSetBinaryMode stdout True → audio bytes; no UTF-8 translation - Icy-MetaData: 0 → Icecast must not interleave metadata into the audio (would corrupt MP3 for non-icy-aware players) - responseTimeoutNone → http-client's default ~30 s timeout would kill the unbounded stream - withResponse → bracket-style; on stdout broken-pipe (gopher client disconnects) the exception unwinds and the upstream TLS connection closes. venusia's child-process bracket then reaps. > {-# LANGUAGE OverloadedStrings #-} > module Main (main) where > > import qualified Data.ByteString as BS > import Data.Maybe (fromMaybe) > import Network.HTTP.Client (brRead, parseRequest, > requestHeaders, responseBody, > responseTimeout, > responseTimeoutNone, > withResponse) > import Network.HTTP.Client.TLS (newTlsManager) > import System.Environment (lookupEnv) > import System.IO (BufferMode (..), hSetBinaryMode, > hSetBuffering, stdout) > > defaultUrl :: String > defaultUrl = "https://radio.someodd.zip/" > > main :: IO () > main = do > hSetBinaryMode stdout True > hSetBuffering stdout NoBuffering > url <- fromMaybe defaultUrl <$> lookupEnv "GOPHER_RADIO_URL" > mgr <- newTlsManager > req0 <- parseRequest url > let req = req0 > { requestHeaders = ("Icy-MetaData", "0") : requestHeaders req0 > , responseTimeout = responseTimeoutNone > } > withResponse req mgr $ \res -> pump (responseBody res) > > -- | Read chunks from the upstream body and write them to stdout until > -- either upstream closes (empty chunk) or stdout breaks (gopher client > -- disconnects → exception propagates out, withResponse cleans up). > pump :: (IO BS.ByteString) -> IO () > pump body = go > where > go = do > chunk <- brRead body > if BS.null chunk > then pure () > else BS.hPut stdout chunk >> go