# LAN Radio for 2025

8 min read
Table of Contents

I’ve done my fair share of dabbling when it comes to serving up any video or audio that I have on my servers, using all kinds of server-side and client-side software. But there is one piece of software that I’ve never really understood or wrangled until recently: IceCast. IceCast is a server-side software that acts as a relay and “broadcasts” incoming audio streams out to the world. The idea is that you create a source, i.e. an audio file or a playlist file, and send it to IceCast, so that you can listen to it with a client like VLC Media Player from anywhere on your LAN or in the world. In fact, you can have many streams coming from a single IceCast server, as it may have many streams coming to it.

[!WARNING] My server is running Ubuntu 24.04 LTS, YMMV if you are following along.

My Obscure Music CollectionLink to heading

I love music. Plain and simple. But over the years, I’ve discovered that even a subscription to Spotify or Apple Music and having any song of my desire to listen to, there is still an obscure “collection” of music that I can’t get anywhere except for YouTube. This could be anything from live concerts and recordings, podcast clips, or even YouTube videos, in the form of audio only. YouTube in 2025 is not the greatest platform due to the overkill amount of ads and the annoying “Youtuber” culture, but it does still serve a purpose of sourcing this type of media. There is also the discussion of data preservation but that’s another discussion for another time.

But probably the biggest genre of music that I download from YouTube has to be ambient sounds and soundtracks. Think spooky Halloween ambiance, rainy autumn days with thunder, dark ambient mixes, or jazz music by the fire in a coffee house. Just fun, random stuff to have playing in the background (it gets quiet in the house during the day sometimes).

The WorkflowLink to heading

My current workflow is using 2 Docker instances of Pinchflat. In case you don’t know, Pinchflat is an app that allows you to provide a YouTube channel or playlist, and download those videos in many different formats (1080p, 720p, audio only, etc). It will check these sources periodically for new content and notify you via AppRise for your platform of choice.

Why do I use 2 instances instead of just 1? I’ve recently figured out that Jellyfin does not like to have 2 libraries pointed to the same folder, i.e. a parent folder for one library source and a child folder for another library source. Pinchflat also does not support download locations to different folders, so I just run 2 instances: 1 for video and 1 for audio. It’s a crappy solution but it works.

There are a lot of “one off” tracks that I find, so the best way to accomplish the goal is to create a “Random Music” playlist in YouTube, then add the playlist URL as a source in Pinchflat. I can then add videos to this playlist as I come across them.

Once Pinchflat has downloaded my media, it is all neatly stored and organized in my “YTMusic” folder on my server.

IceCastLink to heading

This one is pretty simple. I was trying to keep everything in Docker containers, but I found that it was simpler to just install the native packages. Some IceCast Docker containers were modified and just didn’t work well for what I wanted.

Terminal window
sudo apt install icecast2

Before the installation begins, you will be prompted with a few questions, regarding the hostname and the password for both the admin web UI, mountpoints, and relay (remember this for later). I set my host to “localhost” since I will be running this on my LAN only for now. Once installed, you can revisit ‘/etc/icecast2/icecast.xml’ for the configuration file. Verify that you can get to your web UI by visiting “http://localhost:8000/admin”. And that’s it! IceCast should accept anything incoming via port 8000 and with the credentials you set up during the installation process.

Creating a SourceLink to heading

This was the most frustrating part of the project for sure. My goal was to use LiquidSoap to search through my music files, find files that have specific keywords in them (“halloween”, “space”, “live”, etc), and then send them to IceCast for broadcast. However, I could not find a way with LiquidSoap to accomplish this; there seemed to be missing plugins and functionality. The only way I could get it to work was to create playlist files via the “find” command, and use it like that, but I’m not a fan of that solution. I may revisit it in the future or even try a Docker container at some point.

So I decided to take a step back and try something extremely simple and more direct: FFMPEG. FFMPEG has native ways to send audio directly to IceCast!

Here is the command that I used to test everything out:

Terminal window
ffmpeg -re -i INPUT.m4a -c:a libmp3lame -b:a 192k -vn -f mp3 icecast://source:SOURCE_PWD@localhost:8000/stream.mp3

This tells FFMPEG to encode the M4A audio file into a MP3 at 192K bitrate and setup a mountpoint in Icecast called “stream.mp3”. I could then open VLC, open a network stream, and put in the address “http://IP:8000/stream.mp3” to listen to my specified audio file.

Python Enters the ChatLink to heading

But what about searching files based on keywords? What about any other actions besides sending a single audio file to the Icecast server? Python to the rescue!

I wrote a Python script, with the help of AI (I was lazy today, don’t judge), to “walk” my folder of YouTube Music, find the files based on keywords, shuffle them, and stream them. Once all files are done playing, it will do it all over again.

import os
import subprocess
import time
def stream_audio_to_icecast(directory, keywords, icecast_url, source_password, mountpoint):
"""
Searches for audio files containing keywords in their filenames and streams them to Icecast.
Args:
directory (str): The directory to search for audio files.
keywords (list): A list of keywords to search for in filenames.
icecast_url (str): The Icecast server URL (e.g., "hostname:port").
source_password (str): The Icecast source password.
mountpoint (str): The Icecast mountpoint (e.g., "/stream.mp3").
"""
found_files = []
for root, _, files in os.walk(directory):
for file in files:
if file.lower().endswith(".m4a") and any(keyword.lower() in file.lower() for keyword in keywords):
found_files.append(os.path.join(root, file))
if not found_files:
print(f"No M4A files found with keywords {keywords} in directory {directory}")
return
for m4a_file in found_files:
print(f"Streaming: {m4a_file}")
command = [
"ffmpeg",
"-re", # Read input at native frame rate, useful for live streaming.
"-i",
mp3_file,
"-c:a",
"libmp3lame",
"-b:a",
"192k", # Adjust bitrate as needed
"-vn",
"-f",
"mp3",
f"icecast://source:{source_password}@{icecast_url}{mountpoint}",
]
try:
subprocess.run(command, check=True)
print(f"Successfully streamed {m4a_file}")
except subprocess.CalledProcessError as e:
print(f"Error streaming {m4a_file}: {e}")
except FileNotFoundError:
print("FFmpeg not found. Please ensure it's installed and in your PATH.")
if __name__ == "__main__":
# Customize these variables
search_directory = "/path/to/music" # Replace with the actual directory.
search_keywords = ["jazz", "ambient", "ambience", "cyberpunk"] # Replace with keywords in filenames
icecast_server_url = "localhost:8000" # Replace with your Icecast server URL and port
icecast_source_password = "hackme" # Replace with your Icecast source password.
icecast_mountpoint = "/stream.mp3" # Replace with your Icecast mountpoint.
while True:
stream_audio_to_icecast(search_directory, search_keywords, icecast_server_url, icecast_source_password, icecast_mountpoint)
time.sleep(5)

After getting this script into action and testing it out manually, I thought it would be important to make sure that I could control it from SSH, so I created a simple SystemD service:

Terminal window
[Unit]
Description=Icecast MP3 Streamer
After=network.target
[Service]
ExecStart=/usr/bin/python3 /path/to/stream.py
WorkingDirectory=/path/to
User=user # Replace with the user that should run the script
Group=group # Replace with the group that should run the script
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

Reload the SystemD Daemon:

Terminal window
sudo systemctl daemon-reload

Enable the service:

Terminal window
sudo systemctl enable icecast-streaming.service

Start and verify the service:

Terminal window
sudo systemctl start icecast-streaming.service
sudo systemctl status icecast-streaming.service

And just like that, you have a working radio station that runs 24/7, based on your keywords!

Honorable MentionLink to heading

If you want something that is more feature full but definitely more involved, I would recommend checking out Azuracast. I’ve ran this before via Docker and although it worked, getting multiple streams to work was a little bit of a pain. I don’t plan to include any jingles or stations IDs into my mix, so I really don’t need the advanced tools. However, you might.

Also, depending on how much control you want, a simple IceCast + LiquidSoap setup might be for you. I did get LiquidSoap working just pointing it to the parent music folder, but I couldn’t filter based on keywords.

Future PlansLink to heading

I have no idea how much I will use this, but now that I’m home alone during the day a little more than usual (school season), I like to have background noise going on. I would like to have a little bit of remote control and maybe even play things based on a schedule (play Halloween stuff between September 1st-November 1st, Jazz stuff around the holidays, etc). And all of this can be done via Python, so I’m pretty excited to get some traction on that project.

Also, my son listens to his “sounds” at night while he sleeps, so this might be a good way to setup a simple radio station with his tunes and stream to his Raspberry Pi.

Hopefully this might help someone who has been curious about creating their own simple radio station, or just how FFMPEG, IceCast or LiquidSoap works.

My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts

Comments