The Ramblings of Ken
Ken Andersen's blog about technology related subjects.
Wednesday, October 21, 2026
PostgreSQL for Radio Data System in Low Power FM Radio
This is a continuation from my previous post about PostgreSQL in LPFM.
In December 2023, KVWJ installed a new Nautel VX300 transmitter. You can read more about that at these places if you want:
This new transmitter enabled the station to display the song title and artist information on RDS equipped radios. We were excited to start doing this. I was afraid that this was a very complicated process, but it is quite simple. You send a netcat command to the transmitter with the text information you want. No problem. I originally accomplished this with a simple bash script. Here's a snippet from that:
SONGNAME=$(</mnt/zara/ZaraLogs/CurrentSong.txt)
NAMELENGTH=${#SONGNAME}
if [ "$NAMELENGTH" -gt 64 ]; then
SONGNAMECUT=$(echo $SONGNAME | cut -c 1-64)
echo "TEXT=$(echo $SONGNAMECUT )"|nc -i 2 -w 2 192.168.0.108 7005
echo "TEXT?"|nc -i 2 -w 2 192.168.0.108 7005 &
fi
if [ "$NAMELENGTH" -lt 65 ]; then
echo "TEXT=$(echo $SONGNAME )"|nc -i 2 -w 2 192.168.0.108 7005
echo "TEXT?"|nc -i 2 -w 2 192.168.0.108 7005 &
fi
I was surprised how easy it was to send RDS commands.
In 2026, however, we decided it was time to take RDS to the next level. One minor problem we had was that the CurrentSong.txt from ZaraRadio is reading the filename. If we had something wrong with the filename then the information would be displayed incorrectly on RDS. One common thing we run into is that filenames do not include "The" in them. So a song by "The Turtles" is simply labeled "Turtles" in the filename. This means that you see on RDS "So Happy Together - Turtles." This is not ideal.
I realized we could use the PostgreSQL database for this. I extended the database to include artist_display_name and track_display_name fields. I then added a template view with a form so you could edit the artist_display_name and track_display_name.
With the new artist_display_name and track_display_name fields and the template view in place, we were ready to move on from the bash script to something more sophisticated. My original implementation was a Python script watching for the Zara daily log to change and then looking up the song in the database to see if it had a display_name field populated. This was problematic because the file would change and sometimes the song in the log wouldn't be in the database yet. That's when I realized I could use a PostgreSQL database trigger and a Python listener for this. Here's a snippet from the Python:
# Wait for a Postgres notification, up to one second.
ready, _, _ = select.select([listener_conn], [], [], 1)
if ready:
listener_conn.poll()
while listener_conn.notifies:
current_song_category = None
notification = listener_conn.notifies.pop(0)
database_song_text, current_song_category = process_notification(
listener_conn,
notification,
)
So the Python code is waiting for a trigger to fire which then send a notification with a new playback ID when a new record is inserted into the playback table. More database queries are executed to then get the proper artist and track information:
SELECT CONCAT_WS(
' - ',
COALESCE(NULLIF(t.track_display_name, ''), NULLIF(t.track_name, '')),
COALESCE(NULLIF(a.artist_display_name, ''), NULLIF(a.artist_name, '')),
NULLIF(y.year::text, '')
)
FROM playback p
JOIN tracks t
ON t.id = p.trackid
LEFT JOIN artists a
ON a.id = t.artistid
LEFT JOIN years y
ON y.id = t.yearid
WHERE p.id = %s
LIMIT 1
The COALESCE database command allows the display_name field to be selected if it exists, otherwise it falls back to the regular name field.
With the right information we can build a string that can be sent to the transmitter. Instead of using netcat from the shell, we use a Python socket:
def send_rds_command(command: str) -> None:
with socket.create_connection((RDS_HOST, RDS_PORT), timeout=2) as sock:
sock.sendall((command + "\n").encode("utf-8"))
This architecture blew away my expectations. It's a foolproof way to control what information actually gets sent to the transmitter.
More is yet to come from my PostgreSQL in LPFM series...
Tuesday, October 20, 2026
PostgreSQL for Low Power FM Radio
Confession: I'm into terrestrial radio.
I volunteer my time doing IT projects for the local LPFM. If you want to read all about this station head on over to kvwj.org. It's pretty cool what has been done with next to zero budget.
Back in 2024 the director and chief engineer was wondering how he could get a list of the songs that have played on the air over the last four weeks. He was wanting to analyze just how often songs are playing. This station uses the free version of ZaraRadio. It gives you playback logs for every day. Going through the logs and figuring out what played is a big hassle though. Here's a small sample of what a daily log looks like:
06:32:39 start C:\Users\KVWJ\Desktop\ye olde new gold\playback\clockwheel rotation\B medium oldie\A Well Respected Man - Kinks - 1965.mp3
06:35:16 start C:\Users\KVWJ\Desktop\ye olde new gold\playback\clockwheel rotation\C slow oldie\It's Just A Matter Of Time - Brook Benton - 1959.mp3
06:37:41 start C:\Users\KVWJ\Desktop\ye olde new gold\playback\clockwheel rotation\D extra slow oldie\Your Mama Said You Cried - Kenny Dino - 1961.mp3
I thought it shouldn't be too bad to write a Python script that can scrape these logs and throw them into a SQL database. I took a few hours over a week or so and got a proof of concept working. I had a script running in a loop and grabbing lines from the log that matched the pattern above. I started with a SQLite database with a flat one table structure.
The next thing I worked on was a web interface to interact with the SQLite database. I have some experience in Django, so I cobbled together a project based on django-tables2 and got the data displaying on a web page. I then threw a download button on it so that our director could have a nice csv of the data to take with him.
I eventually realized that this flat SQLite database would not be sustainable.
I then got to work restructuring the database so it wasn't basically a large Excel spreadsheet. I broke it off into artist, track, category, year, and playback tables. The rows in the playback table corresponding to the log lines above now looks like this:
id,track,date,timestamp
4244293,2511,2026-10-20,06:37:41
4244292,1659,2026-10-20,06:35:16
4244291,1207,2026-10-20,06:32:39
This makes the database much more efficient.
I was at this point fighting with Django. Randomly it would get really slow. I decided it was time to move from the SQLite database to a more powerful PostgreSQL database. I created tables with the same name in the new database and then dumped the SQLite tables and imported them into the new PostregSQL tables. Much better.
And this is just part 1 is a series of posts I will be making about this.
Thursday, April 14, 2022
Is Ben Cool?
The 2021-2022 Utah Jazz
Thursday, November 2, 2017
Yahoo Finance API Deprecation
Years ago I wrote a series of scripts that would download a CSV file filled with stocks of local interest through the Yahoo! Finance API. The scripts would then parse the data and make it available on a private webpage for on-air announcers to be able to read off the local stock data. The simple query allowed you to tack on your desired symbols and looked like this:
wget http://download.finance.yahoo.com/d/quotes.csv\?s\=\^GSPC\,\^IXIC\,CLAR\,PG\,CLRO\,CAG\,FC\,HUN\,MMSI\,NUE\,NUS\,OA\,OSTK\,D\,SKYW\,USNA\,ZION\&f\=nl1c1p2\&e\=\.csvAt about 4:00 p.m. MDT on November 1, 2017, any call to this quotes.csv service started responding with the following:
Sorry, Unable to process request at this time -- error 999.As of this writing, calls to quotes.csv respond with an HTTP 403 status code and this text:
It has come to our attention that this service is being used in violation of the Yahoo Terms of Service. As such, the service is being discontinued. For all future markets and equities data research, please refer to finance.yahoo.com.It was sure nice of them to give the users a warning. Luckily, I found a method to to scrape the data from their webpages when they stopped allowing the Dow Jones Index to be downloaded through the quotes.csv API.
All I needed was the basic price data that came from the quotes api. I don't need historical data to go along with it. I noticed that the data I need is in the title of the page.
There had to be a way to scrape this data and store it in my own CSV. I tried simple wget queries to the desired symbol URLs, but that doesn't work. There's a lot of JavaScript on the Yahoo! stock pages which manipulates elements on the page. The desired data wasn't showing in the title of the page with wget queries. It simply shows this for the title:
OSTK : Summary for Overstock.com, Inc. - Yahoo FinanceI needed to find a way to parse JavaScript from a script. Research pointed me directly to PhantomJS. The description on their site says that "PhantomJS is a headless WebKit scriptable with a JavaScript API." That's exactly what I needed. What PhantomJS does for me is that it goes to a page, parses the JavaScript, and saves the resulting HTML into a file. What you do is setup what you want it to do in a .js file and call it with the .js file as a parameter. To get the Overstock html from Yahoo! I simply setup a .js file like this:
//render_ostk.jsYou then simply call PhantomJS with render_ostk.js as the single parameter.
var page = require('webpage').create(),
address = 'http://finance.yahoo.com/q?s=OSTK',
output = '/var/www/localhost/htdocs/ostk.html',
fs = require('fs');
page.open(address, function (status) {
if (status !== 'success') {
console.log('Unable to load the address!');
phantom.exit(1);
} else {
window.setTimeout(function () {
page.render(output);
fs.write(output,page.content,'w');
phantom.exit();
}, 20000);
}
});
phantomjs render_ostk.jsThe html page is then saved where you requested it in the output variable. Now use Python to parse the html. I came across a "beautiful" Python library called BeautifulSoup. It makes screen scraping very easy. It turns elements on the page into objects that can be called within Python. Here's my Python code for scraping the ostk.html page:
#yahoo_stock_ostk.pyThis Python code simply finds the title of the webpage, splits the text of the title into separate variables, and then saves those. These values come back as strings from BeautifulSoup, so that is why you see the type casting. I needed to do comparisons on the values to get the plus or minus signs tacked on to the strings when exporting to a CSV file. There's probably a better way to do this, but it worked this way.
import codecs
import datetime
page_name = '/var/www/localhost/htdocs/ostk.html'
local_page = codecs.open(page_name, encoding='utf-8')
from bs4 import BeautifulSoup
soup = BeautifulSoup(local_page, "html.parser")
title = soup.find('title')
titlet = title.text
values = titlet.split()
pricet = values[1]
pricet = pricet.strip()
pricet = pricet.replace(',', "")
change = values[2]
change_flt = float(change)
pchange = values[3]
pchange = pchange.strip()
if change_flt > 0:
change = '+' + change
pchange = '+' + pchange
daystring = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print('"Overstock.com, Inc.",',pricet,""",""",change,""",""",'"',pchange,'"',""",""",'"',daystring,'"')
I made a script which calls PhantomJS for each symbol I want and then runs a Python script to parse the data for each symbol and puts it in the CSV.
#!/bin/bashAt this point I got a cron job setup to run every hour to keep the CSV file updated. From here, the CSV is imported into a private webpage and the values are all together on one convenient page for our announcers.
phantomjs render_oa.js &
phantomjs render_ostk.js &
phantomjs render_d.js &
wait
python yahoo_stock_oa.py >> quotes.csv
python yahoo_stock_ostk.py >> quotes.csv
python yahoo_stock_d.py >> quotes.csv
Hopefully someone can benefit from this as they work through what to do about the Yahoo! Finance API deprecation.
Monday, March 6, 2017
Dell OMSA Email Alerts via PowerShell
The SMTP server at my work has become quite unreliable. This is why I wasn't receiving email alerts from my Dell servers in my previous post. Also, in a web application that we host, students weren't getting email alerts that they should have been receiving. We changed that web application to use Office 365's SMTP server and email alerts have become reliable again. On the Dell servers, I was using an application I found called DellSMTPNotify.exe It worked great when our own SMTP server was reliable. It was time to take advantage of the more reliable SMTP server of our hosted Office 365 Exchange setup, but you have to use TLS encryption over SMTP to use Office 365. The DellSMTPNotify program does not support SSL or TLS encryption over SMTP as far as I can tell. It was time to investigate other solutions.
I found a PowerShell script called OMSA-Notify that someone had already written. You can, indeed, use PowerShell to send email over an encrypted connection. I just needed to figure out how to modify the OMSA-Notify script to suit my needs.
I got everything configured, so I thought, but the script just wasn't working. PowerShell just seemed to be sitting idle and would eventually give a timeout error. That's when I realized I was trying to connect to an SMTP server on the public internet, but I was running all of this on a server that was behind a proxy server. I thought I would just try and configure PowerShell to use the proxy server to send my SMTP commands, but that didn't work either. My research led me to the conclusion that SMTP commands can't be send over an HTTP proxy without installing some sort of relay program. I was at a dead end.
Later on, however, it hit me that I could setup a remote PowerShell session to one of our servers that is using a public IP address in front of the proxy server. My co-worker had already setup something similar in a PowerShell script that connects to our Microsoft Data Protection Manager server to setup automatic backups from individual client machines. To setup a server to accept remote PowerShell sessions, you simply need to run "enable-psremoting" from a PowerShell session running as Administrator. That should do it.
After getting remote PowerShell setup, my script was still failing. The smtp.send command wouldn't work because the mail message wasn't being passed to the remote session properly. I then had to setup the passing of parameters into the remote PowerShell session and actually construct the mail message in the remote session. You simply use the "-ArgumentList" parameter to send local variables to the remote PowerShell, as seen in the Invoke-Command line in the script.
Invoke-Command -Session $RoutableServer -ScriptBlock $ScriptBlockContent -ArgumentList $body, $AlertType, $env:COMPUTERNAMEThis was a fun project to figure out. I'm really happy to have server alerts coming to my inbox again. Here's a pastebin of the full modified OMSANotify.ps1: http://pastebin.com/wvvkNaVk
© The Ramblings of Ken Powered by Bootstrap , Blogger templates and RWD Testing Tool




