312 episodes
- This show has been flagged as Explicit by the host.
Links to a more in depth way to do the musique concrete method and
harsh noise wall (they don't use audacity, but it all can be done
on audacity, just gives you an idea what I'm talking about)
visual of generating noise on audacity (for harsh noise, you want
to have a larger amplitude or any thing that makes the noise
louder)
https://youtu.be/CkZS1C7JUmM?si=qGe7g94lrBE-fAh6
Musique Concrete
https://youtu.be/hvI0URwiCJc?si=l_7N3DASLu4Cw7qQ
Harsh Noise Wall
https://youtu.be/_zwgleyfBgQ?si=IgtK3ydM0queuHBX
Provide feedback on this episode. - This show has been flagged as Clean by the host.
C++ is a high-level, general-purpose programming language created
by Danish computer scientist Bjarne Stroustrup. First released in
1985 as an extension of the C programming language, adding
object-oriented (OOP) features, it has since expanded
significantly over time adding more OOP and other features
- from https://en.wikipedia.org/wiki/Main_Page
Serenity OS - https://serenityos.org
The C Programming Language, Kernighan and Ritchie -
https://colorcomputerarchive.com/repo/Documents/Books/The%20C%20Programming%20Language%20%28Kernighan%20Ritchie%29.pdf
C++ - https://en.wikipedia.org/wiki/C%2B%2B
The Ghost of Unix SVR4 - https://github.com/macsplit/unix_doc
Provide feedback on this episode. - This show has been flagged as Clean by the host.
In our sample game we look at playing as Austria and aiming for a Diplomatic Victory. And our focus is on puppeting City-States, but I misunderstood and instead of making a Diplomatic Victory easier, it makes it harder. I still managed to get my Diplomatic Victory, but a Science or Domination Victory would definitely have been easier in this scenario.
Playing Civilization V, Part 14
Now I am at turn 340, I just puppeted another City-State, and I have one more on the way. I have sent a large number of military units, both land and sea, to the City-States that are on Japan’s border. If he tries anything now he will likely regret it. Now I am starting to build things I had not bothered with before, like Granaries and Amphitheaters. I got a couple of Great Engineers, and used them to rush build Wonders. I am nearly finished puppeting all of the City-States, and my Happiness and Income are both quite healthy. I am in such good shape I started to build the World’s Fair, which I initally proposed then ignored.
At this point I only have one more City-State to puppet, then I will have puppeted every City-State in the game. So it is time to put my large income to use in other ways by giving gifts to the other players. I may want to get a few more votes on my side to get over the top in the World Leader vote when it happens. On turn 357 I completed the puppeting of all of the City-States in the game. Meanwhile, I am building up my Science and building Wonders as they become available. I didn’t build any in the early game because I had other priorities, but now I have a strong military, high income, and no other pressing needs. My score on the Leader Board is now double that of my nearest competitor which is still Japan. And their denunciation me of just expired with them taking no action.
I used a Great Engineer to rush build the Porcelain Tower, earning me another Great Scientist, which I then used to build another Academy. At this point it is mostly a slog to the finish. Since I was so dominant and so wealthy I could afford to pursue a couple of other options just in case. One was a Science victory, which looked like a possibility, though it would be close. I had to be very focused in my Science Research. The other option was a Culture victory, and I had dominated two other empires culturally without half trying, and I was generating Great Artists, Great Musicians, and Great Writers pretty rapidly. So I built Museums, then Archeologists, and grabbed every artifact I find to stuff into the Museums. But the final Victory came when I researched Globalization. This tech allows you to get an extra vote in a World Council for every Spy you send to another empire’s capital and designate as an Ambassador. I immediately sent my spies out (they had previously been in my cities doing counter-espionage since I had a tech lead). And a few turns later a vote for World Leader came up and I won.
This was my first time using Maria Theresa of Austria as my leader, and it was fun, but in the end puppeting all of the City-Staters actually made it harder to win a Diplomatic victory. In other games I have played, every City-State that was an ally was an extra vote in the World Council, and I assumed that making them puppets would mean I would get that many votes. But that didn’t happen. Of course, if I had read more carefully in the Civilization Wiki I would have realized this, because it says there very clearly that Austria is one of the worst Empires to pick if you are going for a Diplomatic victory. So the fact that I managed it anyway is nice. I think I actually could have done a Science victory as I had my Spaceship half-built when I won the Diplomatic Victory. But I hope this analysis of how I actually played this game will be helpful to someone else.
Links
https://www.palain.com/gaming/civilization-v/playing-civilization-v-part-14/
Provide feedback on this episode. - This show has been flagged as Clean by the host.
Hello, again. This is Trey.
Welcome to part 8 in my Cheap Yellow Display (CYD) Project series.
If you wish to catch up on earlier episodes, you can find them on my
HPR profile page
https://www.hackerpublicradio.org/correspondents/0394.html
It is hard to believe that I started this project and the HPR series to document it more than a year ago. Time flies. Life happens. I spent the last 8 months so focused on work related activities that I had to set the project aside. And once I set it aside, it was difficult to get back to again. The one time I tried, I found that my son's old Windows laptop, which I had commandeered to use for the project, was once and truly dead.
We live in a different world now than we did when I began this project. Today, everything is about AI – how it is changing our world, increasing efficiencies, and even displacing certain types of jobs. "Vibe coding" is transforming the way we make software, and now everyone is a developer.
Within my organization, we are all being strongly encouraged to learn more about AI and apply it in our daily work. We are blessed to have access to a wide range of training and to powerful tools which support the process. Several colleagues within my organization and outside my organization have recommended Claude Code -- for development, for organization, for brainstorming, and for much more. My role is not that of a developer, and I have had no need for Claude Code at work. There are plenty of other tools for me to use.
But at home, I thought... I could install Claude Code at home to experiment with and to learn. And then it hit me. I wonder if I could use Claude Code to help me with my stalled CYD project.
"Hello, my name is Trey, and I am a fraud."
OK. I don't think I am a fraud, but having never used such a powerful tool to help me code, I feel a little bit like a fraud, with Claude doing the work for me. Let's talk through what we did.
As I mentioned, I was unable to use the laptop on which I created the original GUI code. But no worries, because It was all on GitHub, right?
So, I began by trying to install Claude Code on one of my Ubuntu machines. That failed miserably, and all the instructions I found online recommended running it on Windows. So I transitioned to installing Claude Code on a freshly built Windows 10 system using some old hardware which will not support Windows 11. Surprisingly, It seems that Claude wants to run within Ubuntu on Windows Subsystem for Linux (WSL).
Once I went through all the hoops for installing and setting up my basic subscription, I got to work. I created a new directory to use for all my project files, and another one for Claude to write all its output files to.
Everything I have read and heard about using Claude Code for a project like this recommended beginning by using Claude to create the project plan. So my first prompt was as follows:
"I am building a project using an ESP32 cheap yellow display. The final goal is to have a device with a graphical user interface which will allow me store several text messages, each assigned to their own specific button. Then, when one of the buttons is pressed, the message will be converted into morse code, and the message will be sent by blinking an onboard LED. I already have the initial design for the GUI coded in my github repository. Please construct a detailed step by step plan for building this application"
I wanted to keep it simple at the beginning, so even though I plan to use an opto-isolated relay for the output, I am starting out using the built in LED.
Claude asked me for the URL for my Github repository, and I provided it. After thinking for a bit, it produced a project plan.
CYD Morse Sender — Step-by-Step Build Plan
What You Already Have
- 3 fully designed LVGL screens (Send, Config, Keyboard)
- 7 send buttons with green styling on the Send Screen
- Speed slider (5–30 WPM) and program slot dropdown on Config Screen
- Text area + LVGL keyboard widget on Keyboard Screen
- Display driver and touchscreen hardware initialized (but touch read was stubbed out)
---
Phase 1: Fix Touchscreen Input
Initialize the XPT2046 on VSPI, replace the hardcoded touched = false with real touch data, calibrate raw ADC coordinates to screen pixels, and verify all three nav buttons transition screens correctly.
Phase 2: Persistent Message Storage
Use Preferences.h / NVS to store 7 messages ("msg0" – "msg6") that survive power cycles. Load on boot and populate send button labels.
Phase 3: Config Screen — Slot Selection & Speed
Wire the Program dropdown to g_selectedSlot, wire the Speed slider to g_wpm with a live WPM label, and wire the Set button to clear the text area and navigate to the Keyboard Screen.
Phase 4: Keyboard Screen — Capture and Save Text
On keyboard OK (LV_EVENT_READY): read the text area, save to NVS, update the send button label, navigate back. On Cancel: navigate back without saving.
Phase 5: Morse Code Engine
Build morse.h with the full A–Z / 0–9 lookup table, a textToMorse() converter, and Paris-standard timing (unit_ms = 1200 / wpm).
Phase 6: Non-Blocking LED Transmission
State machine (IDLE / LED_ON / LED_OFF) driven by millis() in a morseUpdate() function called every loop — no delay(). LED on GPIO 17 (blue).
Phase 7: Wire the Send Buttons
Each button calls startMorse(getMessage(slot)) if not already transmitting. Disable all send buttons during TX; re-enable when the state machine reaches the end.
Phase 8: Integration & Polish
Stop button, WPM persistence, empty-slot guard, serial debug output, final touch calibration, end-to-end test checklist.
---
Recommended order: Phase 1 → 2 → 7 (partial) → 5 → 6 → 7 (full) → 3 → 4 → 8
This was wild! Claude determined exactly how far along I was in the project, and also alerted me to the fact that the "touch read was stubbed out". Ugh. This means that the files I had uploaded to GitHub were my original GUI files (Episode 05 – HPR4532 -
https://hackerpublicradio.org/eps/hpr4532/index.html
) and not the ones that I finally got working properly (Episode 07 – HPR4624 -
https://hackerpublicradio.org/eps/hpr4624/index.html
). That was my own fault. Did I mention that I don't get Git? I REALLY need to learn to properly use Git!
But, we have a plan, broken down by eight numbered phases. And they seem to address all the functionality I wanted with a few additional things I had not thought about. Interestingly, even though these phases are sequentially numbered, Claud recommended that we approach them in a bizarre order: Phase 1 → 2 → 7 (partial) → 5 → 6 → 7 (full) → 3 → 4 → 8 .
Alright. Let's see what we can do. The first phase is to fix the touchscreen input.
Claude took me through it step-by-step, asking as it needed to read specific project files.
Finally, it wrote a new ui.ino code file to my speficied output directory for me to test. I copied it into the correct file location, said a quick prayer, compiled in Arduino IDE, and downloaded to the CYD.
Well, that is... interesting. The display looked nothing like it was supposed to. There were vertical green bars with smaller dashed green vertical stripes in them. I will include a picture in the show notes so that you can see what it looked like and why it was so difficult to describe.
I spent the next hour or so trying to explain what I was seeing to a chat bot. Claude recommended potential fixes which either did nothing or made the situation worse. I began questioning whether this was a good idea, how people actually gained efficiencies talking to a bot, and even several life choices.
Then I had a thought. I prompted Claude:
If I were to take a picture of the screen on the cheap yellow display and copy it into the output folder, would you be able to analyze it to better determine what is wrong and how to fix it?
Shockingly, Claude answered in the affirmative, and told me to copy the picture to the output folder and let it know when to proceed. It analyzed the picture and more of the supporting files it had copied from my GitHub, asking each time if it could access that file. It determined that my original code was written for a flavor of LVGL version 8 and I was now using LVGL 9.5.
It recommended changes, and then asked permission to make those changes, file by file. .h files & .c files, Finally, I just gave it permission to edit the files in the project folder without asking for permission for each file each time. Claude was still explaining each change, showing me exactly what would be changed, and asking for permission, so that I could review all of the changes. But now it was not asking additional permission to write to each of the impacted files.
Next, Code compiled and downloaded. Different screen, but not right. Again, I took a picture and gave it to Claude to analyze. So, Claude paused and altered the code to generate a specific test pattern overtop of the GUI.
The test pattern was supposed to cover the entire rectangular screen. But parts of the pattern were in a square on the screen and parts were not. Another photograph and analysis, told Claude that there were some rotation/screensize issues.
We repeated this several times. Some resulted in improvement, and others did not.
This is the point where I noticed something interesting. Not about Claude, specifically, or about the app. But I noticed something interesting about myself and about the process.
Previously, when I was working through some of these challenges without Claud, I found myself becoming more and more stressed, frustrated, and angry, until I found a solution. Then another problem would repeat the cycle. Success in the end was great, but the emotional extremes during the process were not always pleasant.
Now, I was effectively managing the project, and relaying information to the resource responsible for fixing the problems -- a very different experience.
But I also ran into another issue. Claude became absolutely certain that the problem revolved around the device not accurately knowing where the 4 corners of the screen were. But in reality, the output of the test pattern was rotated 90 degrees from the actual screen. It took several iterations of me insisting that the problem had to do with screen orientation and not corner coordinates. It was interesting to experience the tool doubling down on an obvious mistake, but we finally resolved that.
Again, while it was frustrating, it was much less stressful.
We proceeded to
Phase 2: Persistent Message Storage
where we ensured that the button labels on the send screen were stored in the devices persistent storage, so that, when they are edited to contain the message they should send, that information would survive a reboot.
Next, we combined elements of
Phase 5: Morse Code Engine
,
Phase 6: Non-Blocking LED Transmission
, and
Phase 7: Wire the Send Buttons
together. Building the morse code engine was an area I had been thinking about for a while. I already had working parts of something similar in the Arduino practice oscillator I have referenced a few times in this series. The code for the practice oscillator may be found on my GitHub, but it was all based on original code from jmharvey1, with my only contribution being making pin assignments variables so that the code could easily be ported to different devices.
So, I was happy that we were building the morse code engine directly. The code for it may be found in morse.h, which uses a constant character lookup table to define each character. Without any specific direction from me, Claude used the PARIS timing methods I have already described within Episode 6 of this series. It defines timing for DOT, DASH, LETTER_GAP, and WORD_GAP, and all are based on a simple calculation of 1200 ms / the number of words per minute (WPM) we wish to transmit.
Along the way, we discovered that, if we tried to use the delay() function, it would crash the program due to a conflict with the LVGL timer used for touchscreen inputs. Claude altered all the delays accordingly.
Then,
Phase 3: Config Screen — Slot Selection & Speed
allowed us to configure the WPM we wished to use in addition to selecting a specific Send button to reconfigure. This forced us to work on
Phase 4: Keyboard Screen — Capture and Save Text
which is used to type the entries for each Send button. At this point, I also decided that we would want to also use the Keyboard Screen to send ad hoc morse as we typed it.
During this phase we discovered several bugs which seemed to cause random freezes. Careful troubleshooting with messages output to the Arduino IDE's serial console helped us narrow down the causes and remedy them.
Finally all the tests worked and I am able to merrily pre-configure macro buttons with custom messages and use the CYD to send the morse code for those messages to the on-board LED at whichever rate I specify.
I have noticed in my presentation of this narrative that I repeatedly slip into the first person plural terms "we" and "us" instead of the first person singular terms "I" and "me". I have unconsciously personified Claud and recognized it as an integral part of my (formerly one person) development team.
I finally configured Claude to connect to my GitHub repo and upload all the files and documentation. We additionally created a CYD-Narrative.md file which describes in more detail all the work which was done on the project. I still do not 100% get git, but we are successfully using it.
You can find all these files in my GitHub repo (
https://github.com/jttrey3/CYD_MorseSender
) where they are shared under a GPL 3.0 license.
There are still several additional steps I plan to complete in the next few months.
1. I will be integrating an opto-isolated relay which will allow me to plug the device into the straight key input on any amateur radio. This will require a battery power source, charge controller, and more hardware.
I... make that "We" (Claude & I) will be modifying the code to support an audio side tone through an attached speaker when sending code
We will add an output selection switch to the config page to choose any combination of speaker, relay, or LED as output.
We will develop a downloadable firmware which I hope to share with the Cheap Yellow Display community.
If you can think of any additional features you would like to see integrated, please drop me an email using the address in my HPR profile.
I may also work with a friend to attempt to 3d print a case for the entire contraption, and I will be sure to record additional episodes sharing the process.
I have learned so much throughout this project, about the CYD, ESP32, GUIs, Claude Code, GitHub, and most of all, about myself.
Does using AI to develop this code make me a fraud? It still feels like it in some ways.
Does it make me more productive? ABSOLUTELY! I made consistent forward progress when I only had 30-60 minutes each day to work on it, and everything discussed in this episode was completed in less than a week. If I had been able to work on it for a few hours uninterrupted, it may have only taken me 3-5 hours.
Does it empower and inspire me to do more projects like this? 100% I feel like I had support working with me the whole way. I was less stressed overall, and it had less of an impact on the amount of and quality of time I spent with my family.
I will be wrapping up this series soon, without any more 6 month gaps, I hope.
Until next time...
Provide feedback on this episode. - This show has been flagged as Clean by the host.
01 Introduction
In this episode I will describe techniques for downloading podcasts using basic shell commands such as wget.
I will illustrate this using a bash script that can be used to download HPR podcasts.
Even if you do not have any interest in downloading your podcasts using this method, you may find some of the methods useful or interesting.
It is the principles that are discussed here that are important, rather than the implementation.
02
I realize that there are already a number of different podcast download programs available, including at least one written in bash.
However, you may feel that none of these suit how you wish to do things and want to create your own system tailored to your specific needs.
If so, then I hope the following is of some use to you.
If not, then you may still find some of the things discussed here to still be of interest.
Some of the subjects I cover include
wget to a user defined file name.
parsing xml with xmllint.
using inotifywait to trigger an action when a file is created or modified.
using notify-send to send a message to the notification area.
and
a way of allowing a cron job to send a message to the user interface.
03 Background
There has been an ongoing discussion in comments to some HPR episodes about problems downloading HPR podcast episodes.
Apparently some people have been experiencing problems with the way the episode URLs are structured.
04
I am afraid that I don't fully understand the nature of these problems, so I won't be addressing that problem directly.
Instead, I will present a bash script that I have written which can be used to download HPR podcasts.
This bash script can be run using cron to automatically fetch new HPR podcasts and save them to a designated directory.
This is a simplified version of a script that I have used for years to download HPR and other podcasts.
05
I won't try to read the full bash script out in this podcast, as that would be a bit dull to listen to.
I will instead describe what each section does and why I chose to do things that way.
Perhaps other people can offer suggestions of better ways to do things.
I will post the full bash script in the show notes.
06 Fetching Podcasts
The standard way of distributing podcasts is to publish an RSS feed containing URL links to the audio files.
RSS is a very long established and widely supported mechanism for this and other purposes.
An RSS feed is basically an XML document which can be accessed over HTTP.
These URLs contained in the RSS XML document can then be used to download the actual audio files, such as MP3 or OGG files.
07
Basically what we need to do is the following
• Download the RSS XML document.
• Extract the URL links to the audio files.
• Compare the list of these links to a previously saved list to see which ones are new and which ones are ones that we previously downloaded.
08
• Make a list of the new URLs.
• Go through this list of new URLs and download each of the new audio files.
• Check to see that we actually received the new audio file.
• Add the URLs of the files we successfully downloaded to our saved list of podcast URLs
09
In addition to this, we would like to have the above happen automatically in the background without our having to take any action on our own.
We may wish to receive a notification of when a new podcast has arrived however.
We would probably also wish to receive notification of any errors or failures.
10 Fetching Podcasts - The Preliminaries
Our desire to be able to run the script automatically imposes some requirements on our solution.
To schedule the script we will use cron.
Cron is a Linux facility to run scripts on a schedule.
11
One of the side effects of using cron however is that we need to specify the full path to the locations where we intend to keep any data files, plus also the full path to where we intend to put the downloaded podcasts.
12
So the first thing we need to do in our script is to specify a number of different values for things like file location, the URL for the HPR RSS feed, and several other things as well.
I will skip over the details of these, although I may make reference to them later.
13 Get the RSS Data
The first thing of real substance to do is to fetch the current RSS feed data.
I have put this in a bash function called getrssurldata
The contents of this function are a one liner, but with a number of elements chained together through pipes.
14 Downloading the RSS XML Document
• First we use wget, which is a standard command on most Linux distros.
• We specify four things.
• First we set a timeout. I have chosen 20 seconds.
• Next we set the retry limit. I have chosen 3.
15
• Then we specify that the output of wget is sent to stdout rather than saved as a file.
• This is done by using the -O option followed by a space and then a dash.
• The O option is usually used to specify a file to save the output to, but when used with a dash causes output to go to stdout.
• Then we specify the URL of the HPR RSS feed.
16 Contents of the XML Document
This gives us the HPR RSS XML document.
There are about 5,000 lines in this RSS document.
Most of those lines are the show notes which are also included in the feed.
17 Extracting the Podcast Episode URLs
There are only 10 lines of the document that contain information that we are interested in however.
These lines are enclosed in "enclosure" XML tags.
We just need to find those lines and separate out the URLs
18 Standard Command Line Tools
There are two ways that we can do this.
One is to use a combination of grep, sed, and cut.
Grep can find the lines containing the enclosure tags.
Sed and cut can extract the URL from the surrounding extraneous data.
19
However, this method does not discriminate between real enclosure tags in the data portion of the RSS feed and enclosure tags in the show notes which are included in the feed from episodes such as this one.
This may be an acceptable problem in practical terms, but we can do better.
20 Using an XML Parser
The other method is to actually parse the XML document.
there are at least two command line XML parsers that I am aware of.
These are "xmllint", and "xlmstarlet".
I have used xmllint in this example.
I have not used xmlstarlet, so I can't offer any comment on how easy or difficult to use it is.
21
I won't give a detailed explanation of all the things that xmllint can do.
It has many features, most of which, as the name suggests, have to do with finding formatting problems with the XML itself.
Describing everything it can do would be at least one episode in itself.
I will instead just give the particular command used and explain each element of it.
22
In this example assume that we are piping the output of wget directly into xmllint.
The complete command is
xmllint --xpath "//channel/item/enclosure/@url" - | cut -d'"' -f2
23
In this example,
xmllint is the name of the command.
--xpath tells it to parse the document according to the string which follows.
"//channel/item/enclosure/@url" tells it to find a series of tags in the hierarchy of channel, followed by item, followed by enclosure, and then extract the url attribute from the enclosure tag.
The "-" which follows tells it to look for input from stdin rather than from a file.
24
The result is a string which has the url attribute name, an equal sign, and the URL that we want enclosed in quotes.
To get just the URL itself, we pipe the output from xmllint into cut, using the doublequote characters as delimiters.
We then save the result in a temporary file.
25 Finding the New Episodes
Next we wish to find the new podcast episodes.
Each HPR episode is identified by a unique URL.
This means that if we save the URLs of episodes that we have already downloaded, we just have to look for the URLs that do not appear in this saved list.
https://hub.hackerpublicradio.org/ccdn.php?filename=/eps/hpr4659/hpr4659.mp3
26
The easiest way to do this is to take our two lists of URLs, sort each into temporary files, and then compare the sorted URLs using the "comm" command.
27
This is simple, but has a drawback.
Some podcasts occasionally change distributors.
When they do this, the old podcasts are re-published with new URLs and you end up downloading a lot of old episodes over again.
28
With HPR we could get around this by extracting just the file name and looking for that instead of the full URL.
I will however leave that problem as an exercise for the student and just accept that if the URL format changes we may end up downloading old episodes over again.
Since the feed has a maximum of only 10 episodes in it however, that isn't really that big of a problem.
It would be more of a problem with podcasts which have very large numbers of episodes in their feed, but the solutions to those will be feed specific.
29 Downloading the New Podcasts
We should now have a list of URLs for the new podcasts we do not already have.
Typically this should be only one file, but there could be several, or even as many as 10, if we have not turned on our computer in a while.
Therefore, we need to iterate through the file of new podcast URLs and download each one.
30
Before we do that however, we should check to see if there is in fact anything new to download.
To do this, simply use "wc -l" to count the number of lines in the list of new URLs and save the resulting number.
31
If this number is zero, there is nothing to download, we can skip the download step.
As an additional check, we should see if the number of downloads exceeds some threshold value that we wish to set.
This is not a major problem with HPR, but some podcasts have hundreds of files in their RSS feed rather than just the most recent ones.
If we do exceed our download limit, then we need to log an error and skip downloading.
32
Assuming there are no problems so far however, the first thing we need to do is to extract the name of the audio file from the URL.
We can do that using the "basename" command.
We will use this to specify the name that we use when we save the audio file.
33
HPR has a very well formed file name.
Some podcasts do not however, and for those you would need to construct some sort of suitable name either using information found in the URL or simply creating a name using a time stamp.
34
Next we download the audio file using wget.
This is similar to how we downloaded the RSS feed, but with a few changes.
One is that I have increased the timeout to 90 seconds.
This may not have been necessary, but seemed like a good idea.
35
The next is that when specifying the output file name using -O, we use the file name we extracted from the URL.
The third is that we specify a destination directory using the -P option.
36
After wget has finished, including any retries that it had to do, we next check that the expected new file is both present and not empty.
We did this using an "if" statement with the "-s" option.
If the file was found and not zero, then we add that URL to a temporary list of downloaded URLs.
37
If the file was not present, or was zero length, we output an error message to an error log.
I will come back to this point later.
38
Next, if there is more that one podcast to download we sleep for 3 seconds.
While not strictly necessary, it is considered to be "polite" to not hammer a server repeatedly, but rather to put a small delay between file downloads..
39
After we have downloaded all the audio files in our list, we can add the list of URLs for the files downloaded to the permanent list.
While we are at it, we should use "tail" to trim the permanent log to keep it from growing indefinitely.
This limit should be several times bigger than the number of files in the RSS feed.
In this case I selected 50.
40
Finally we write any errors to the permanent error log, and also write these same errors to another file used to signal errors for display to the user.
We have now successfully downloaded at least one HPR podcast.
41 Notify the User of Events
It would be convenient to be informed of new podcast downloads when they occur, and also be notified of any errors.
One of the limitations of cron jobs is that they cannot access the user interface.
This means that we cannot readily send a message directly to the notification system to inform the user of the presence of new podcasts or of errors.
42 inotifywait
The solution to this is to use "inotifywait" to monitor particular files and directories for changes.
The man page for inotifywait states the following -
43
inotifywait efficiently waits for changes to files using Linux's inotify(7) interface. It is suitable for waiting for changes to files from shell scripts. It can either exit once an event occurs, or continually execute and output events as they occur.
End of quote.
44
In many Linux distros, inotifywait is provided by the "inotify-tools" package.
I won't go over all the features of inotifywait.
Instead, I will just describe how to use it for our purposes here.
45 inotifywait Modes
I should point out first though that inotifywait operates in two different modes.
In the normal default mode, it exits after being triggered by an event and must be re-established again in order to resume monitoring.
In monitor mode, which is enabled by using the "-m" option, it runs indefinitely, responding to events.
I will use the default mode here.
46
The man page for inotifywait provides a simple example that we could copy and modify for our purposes.
A great many examples that you will find are based on this example.
However, it doesn't quite do what we want, so we need to change a few things.
47 podfetchnotify
The first shell script is one which monitors for the arrival of new podcasts and sends a notification to the user.
I will call this "podfetchnotify".
The complete scripts are in the show notes, I will just provide a brief description here.
48 Setting Up Event Watches Using inotifywait
The script is enclosed in a while loop which run indefinitely.
In the first line inside the while loop, we call inotifywait.
inotifywait will then block until the event it is told to look for occurs.
In short, execution of the script will wait there until an event occurs.
49
The names of the events are listed in the man file.
In this case we are looking for "modify", "create", and "moved_to".
Each of these does pretty much as you would expect, reacting to modifying an existing file, creating a new file, or moving a file to that directory.
50 Problems When Testing Using Text Editors
I should point out that if you are testing a script which uses inotifywait, then modifying a file with a text editor may not produce the results that you may think it would.
Instead it treats this as a new file with the same name, with the original file being erased.
Since inotifywait attaches itself to the inode rather than the filename, it sees the file that the text editor changed as being a new file.
If you wish to test this realistically, then use "echo" to overwrite the file by using I/O redirection.
51 Capturing Output
In my example I capture the output from standard out into a variable, but I don't do anything with it.
If you wish to for example display the name of the newly downloaded podcast file, then use the --format option along with an appropriate formatting code.
There are details about this in the man page.
On the next line we capture the exit code using "$?"
52 Responding to Exit Codes
If the exit code was zero, then a monitored event was triggered and there should a new podcast in the directory.
In this case we display a message indicating that a new podcast has arrived.
I will describe how to send notifications shortly.
If the exit code was not zero, then an error occurred.
An example of such an error would be if the directory were not present when monitoring was started.
In this case we display a message indicating that a fatal error has occurred and then exit.
53 Delay for More Podcasts
Finally, we use "sleep" to wait for some arbitrary period of time to prevent notifications from being triggered multiple times if several podcasts were being downloaded in succession.
In this case I chose to wait for 60 seconds.
54
We have now completed the process and can return to the top of the loop and resume waiting using inotifywait.
55 Sending Notifications to the User
I mentioned above about sending notification messages to the user.
In the Gnome desktop, notification messages appear from the centre of the top bar in a list.
Other desktops or operating systems may have something similar.
56
To send a notification message to the notification area, you use the "notify-send" command.
Simply follow notify-send with a quoted string and it will be displayed in the notification area.
57 podfetcherrornotify
The second shell script is one which notifies the user of errors.
I will call this "podfetcherrornotify".
With this shell script we set up a watch on a file which contains any error messages from podfetch.
This script is very similar to podfetchnotify.
58
The exceptions are
With inotifywait we only monitor for "modify".
There is no sleep command at the end of the loop.
Instead we sleep for a few seconds just after getting the exit code from inotifywait.
This helps prevent problems caused by race conditions.
59
Next we check the inotifywait exit code.
If it was zero, then we read the error report file and send a notification message to the user containing that error message.
60
If it was not zero, then we check to make sure that the directory that should contain the error log exists.
If it does not exist, then we send a notification message to that effect to the user and terminate the script.
61
If the directory exists, then we check to see if the error message file used for signalling exists.
If the file does not exist, then we create it.
62
One of the reasons for an inotifywait error is that if the file that it is told to monitor does not exist, it cannot set up a watch condition.
By creating the file we correct the cause of the error and allow inotifywait to operate normally.
63
Finally we increment an error counter and check to see if the limit is exceeded.
If there are excessive errors, then send a notification message to the user and exit.
The reason for this is to give the user an indication that the error notifications are not working for some reason and there may be a problem that needs looking into.
64
The error counter is reset every time the inotifywait exit status is ok, so occasional unexpected glitches should be something that is ignored.
Of course podcast fetching errors are something that will probably happen only rarely if at all, so this final step may be seen as an unnecessary embellishment.
65 Installing the Scripts
Next I will describe how to install and prepare the scripts to run.
We need to perform the following steps.
66
• First, we need to create a directory to hold the scripts and their associated data files.
• Next we need to create a directory to hold the downloaded podcasts.
• Then we must copy the scripts to these directories and make them executable.
• Then, we must edit the scripts to have the file path in the script match the locations of the new directories that we created.
67
• Then we need to install xmllint, or alternatively modify the download script to comment out the use of xmllint and enable the alternative method using grep and sed instead.
• Then we need to run each script manually from the command line to check for errors.
• If podfetch ran correctly, it should download the most recent 10 podcasts during this test.
68 Adding podfetch to the Crontab
The above describes how to run the scripts manually.
In order to fetch podcasts automatically, we need to add the podfetch script to the cron schedule.
To do this, open a terminal.
69
Type "crontab -e", and then press return.
A text editor should open up containing the crontab file.
On Ubuntu, this editor is GNU nano.
Enter the appropriate cron parameters.
I will provide an example here for running it 12 minutes past the hour every three hours.
70
12 */3 * * * /home/username/pathtofiles/podfetch.sh
71
I won't explain cron in detail here.
The example that I have just given should be good enough for most people.
The "*/3" parameter will cause it to run every three hours.
The "12" parameter will cause it to run 12 minutes past the hour when it does run.
72
Checking every three hours should be good enough for most people, but you can adjust that as you see fit.
I would recommend however that you don't check more frequently than once per hour.
Checking more frequently than necessary puts extra load on the distribution servers.
It is very unlikely that you really do need each new episode the moment it is available.
73
I would also recommend changing the "12" parameter to some other random minute value.
I would suggest avoiding on the hour or on the half hour, as a lot of other people are probably checking at those times, and it would be better to spread the load out more evenly over time.
74
The file path parameter should of course match the actual path to wherever you have located the script, including the correct user name.
75 Making the Notification Scripts Start Automatically
The two notification scripts can be made to start automatically.
The exact method to do this may vary according to distribution or desktop.
76
On Ubuntu this is done using the Startup Applications Preferences GUI program, which should come already installed.
77
I won't go into details on this here, it should be fairly self evident how to use it once you see it.
What this program does is to create ".desktop" files in the ".config/autostart" directory in your home directory.
78
These ".desktop" files are all run automatically on start up.
Once you have added the notification scripts, you will need to log out and then log back in to make them active.
79 Conclusion
I this episode I explained how to write a set of simple shell scripts to automatically download each new episode of HPR as it comes out and to notify you of its arrival.
80
The download script described here is tailored specifically for use with HPR only.
However, it was derived from a larger script that downloaded other podcasts as well, based on information read in from a text file.
If you are feeling ambitious, you can add those features back into this to handle all of the podcasts that you listen to.
81
In a comment to another episode of HPR I had said that I would cover ID3 tags in MP3 files, but this episode is long enough now, so I will leave that subject for later.
I look forward to seeing you again later on another episode of Hack Public Radio.
# ======================================================================
podfetchdownloader
#!/bin/bash
# Fetch pending HPR podcasts listed in the HPR RSS feed.
# 8-Jun-2026
# Licensed under GPLv3 or later.
# ======================================================================
# Today's date and time as YYYYMMDDHHMMSS.
podttimestamp=$( date +"%Y%m%d%H%M%S" )
# The absolute path to the script. This is necessary when running it
# using a cron job.
podpath="/home/me/Apps/hprfetch"
# This is the absolute path to where to store the podcast files.
podfilepath="/home/me/Music/Podcasts/HPR"
# Create the full path names here for all the text files used.
podcastsfetched="$podpath/podcastsfetched.txt"
poderrorslog="$podpath/poderrorslog.txt"
poderrorsreport="$podpath/poderrorsreport.txt"
tmpoldurlssorted="$podpath/tmpoldurlssorted.txt"
tmppodsnew="$podpath/tmppodsnew.txt"
tmppodstodownload="$podpath/tmppodstodownload.txt"
tmppodserrors="$podpath/tmppodserrors.txt"
tmppodcastsfetched="$podpath/tmppodcastsfetched.txt"
tmplog="$podpath/tmplog.txt"
# The URL for the HPR RSS feed.
PodURL="http://hackerpublicradio.org/hpr_rss.php"
# Limit on number of podcasts to download.
DownloadLimit=11
# Name of the podcast.
PodName="Hacker Public Radio"
# ======================================================================
# Check if the required paths exist.
# If this path does not exist, cannot log the error.
if [[ ! -d "$podpath/" ]]; then
echo "$podttimestamp Error - Could not find $podfilepath."
exit 1
fi
# Where to store the podcast file fetched.
if [[ ! -d "$podfilepath/" ]]; then
echo "$podttimestamp Error - Could not find $podfilepath." >> $tmppodserrors
# Copy the errors log from the temporary errors file to the permanent files.
LogErrors
exit 1
fi
# ======================================================================
# Check if the podcast log exists. We read it before we write to it,
# so it must exist or we will hang on it not being present.
if [[ ! -e $podcastsfetched ]]; then
touch $podcastsfetched
fi
# ======================================================================
# Delete the specified files if they exist.
# This accepts multiple file names in a variable number of parameters.
CleanupFiles ()
{
# $@ accepts multiple parameters.
for f in "$@"; do
# Check if the file exists.
if [ -e "$f" ]; then
rm "$f"
fi
done
}
# ======================================================================
# Copy the errors log from the temporary errors file to the permanent files.
LogErrors () {
if [ -e $tmppodserrors ]; then
# The permanent log.
cat $tmppodserrors >> $poderrorslog
# This file is monitored for display by other scripts.
cat $tmppodserrors > $poderrorsreport
fi
}
# ======================================================================
# Get the URL data from an RSS feed
GetRSSURLData () {
wget --timeout=20 --tries=3 -O - "$PodURL" \
| xmllint --xpath "//channel/item/enclosure/@url" - | cut -d'"' -f2 \
| sort > $tmppodsnew
# This is an alternate method that does not use xmllint.
# However, it is not as robust. If someone were to include the
# first grep search pattern in their show notes, then it would
# look for that as a valid tag and output the following text
# as a URL.
#wget --timeout=20 --tries=3 -O - "$PodURL" | grep "<enclosure url=" \
# | sed -n 's/^.*enclosure//p' | sed -n 's/^.*url=//p' \
# | cut -d'"' -f2 | sort > $tmppodsnew
}
# ======================================================================
# Find which podcasts we do not already have.
FindNewPodcasts () {
cat $podcastsfetched | sort > $tmpoldurlssorted
comm -13 $tmpoldurlssorted $tmppodsnew > $tmppodstodownload
rm $tmpoldurlssorted
}
# ======================================================================
# Download the podcasts.
DownloadPodcasts() {
# Clear out previous temporary list of downloaded podcasts.
true > $tmppodcastsfetched
for i in $( cat $tmppodstodownload )
do
# Extract the file name from the URL.
fname=$( basename $i )
outputpodname="$podfilepath/$fname"
# Download the file.
wget --timeout=90 --tries=3 -P $podfilepath $i -O "$outputpodname"
# Check if the file exists and is not empty.
if [[ -s "$outputpodname" ]]; then
echo $i >> $tmppodcastsfetched
else
echo "$podttimestamp Error - $outputpodname was not found or is empty." >> $tmppodserrors
fi
# Delay a reasonable length of time between multiple downloads.
if (( $PodCount > 1 )); then
sleep 3
fi
done
# Add the list of files downloaded to the log.
# Check if the list exists and is not empty.
if [ -s $tmppodcastsfetched ]; then
cat $tmppodcastsfetched >> $podcastsfetched
# Trim the log file to keep it from growing indefinitely.
tail -n50 $podcastsfetched > $tmplog
mv $tmplog $podcastsfetched
fi
# Remove the tmp file now that we are done with it.
rm $tmppodcastsfetched
}
# ======================================================================
# Clean up any left over files.
CleanupFiles "$tmppodsnew" "$tmppodstodownload" "$tmppodserrors" "$tmppodcastsfetched"
# Get the RSS data.
GetRSSURLData
# Find which podcasts are new.
FindNewPodcasts
# Count how many new podcasts there are.
PodCount=$( cat $tmppodstodownload | wc -l )
# If no podcasts to download, skip this.
# If too many podcasts for this feed, then log an error and skip.
# This error will keep repeating until something is done about it.
if (( $PodCount > 0 )); then
if (( $PodCount > $DownloadLimit )); then
echo "$podttimestamp Too many podcasts for $PodName : $PodCount." >> $tmppodserrors
else
# Download the podcasts listed in the temp file.
DownloadPodcasts
fi
fi
# ======================================================================
# Copy the errors log from the temporary errors file to the permanent files.
LogErrors
# Clean up temp files.
CleanupFiles "$tmppodsnew" "$tmppodstodownload" "$tmppodserrors" "$tmppodcastsfetched"
# ======================================================================
END OF FIRST SHELL SCRIPT
START OF SECOND SHELL SCRIPT
podfetchnotify
#!/bin/bash
# Part of Podfetch.
# This monitors for new files appearing in the new podcasts directory.
# This should be run as a background task.
# Install it using the "Startup Applications" utility in Ubuntu.
# ======================================================================
# Path where new podcasts are to be stored.
podfilepath="/home/me/Music/Podcasts/HPR"
# ======================================================================
# Wait for the podcast directory to be modified.
while true; do
# Check for new files.
errmsg=$( inotifywait -e modify -e create -e moved_to $podfilepath )
result=$?
# Check if exited due to new podcast, or if some error.
if (( result == 0 )); then
# Success, signal new podcast.
notify-send "New HPR podcast available."
else
# Check to make sure the directory exists.
# If it doesn't exist, there isn't much we can do to fix it.
if [ ! -e "$poderrorspath" ]; then
notify-send "Podfetch error: Podcast directory not found $poderrorspath"
exit 1
fi
fi
# Wait a bit so that multiple new files don't keep re-triggering the notification.
sleep 60
done
# ======================================================================
END OF SECOND SHELL SCRIPT
START OF THIRD SHELL SCRIPT
podfetcherror
Created Tuesday 23 June 2026
#!/bin/bash
# Part of Podfetch.
# This monitors the Podfetch error reporting file for new errors.
# This should be run as a background task.
# Install it using the "Startup Applications" utility in Ubuntu.
# ======================================================================
# Where the Podfetch program error report file is located.
poderrorspath="/home/me/Apps/hprfetch"
# The full path and file name.
poderrorsreport="$poderrorspath/poderrorsreport.txt"
# ======================================================================
# Error counter.
errcount=0
# Wait for the poderrorsreport file to be modified.
while true; do
errmsg=$( inotifywait -e modify $poderrorsreport )
result=$?
# Wait a bit to ensure that writing to the file is complete.
sleep 3
if (( result == 0 )); then
# Get the latest error message.
# Cut out the date stamp at the start of the line and take the rest.
poderr=$( tail -n $poderrorsreport | cut -d" " -f2- )
notify-send "Podfetch error: $poderr"
# Reset the error counter every time there is a successful result.
errcount=0
else
# Check to make sure the directory exists.
if [ ! -e "$poderrorspath" ]; then
notify-send "Podfetch error: error report path not found $poderrorspath"
exit 1
fi
# Check if the file we are trying to monitor exists.
# If not, then create an empty file for error signaling.
if [ ! -e "$poderrorsreport" ]; then
echo > $poderrorsreport
fi
# Increment the error counter.
count=$(( count + 1 ))
if (( count > 3 )); then
notify-send "Podfetch error: Excessive unknown errors, exiting."
exit 1
fi
fi
done
# ======================================================================
Provide feedback on this episode.
More Education podcasts
Trending Education podcasts
About Hacker Public Radio
Hacker Public Radio is an podcast that releases shows every weekday Monday through Friday. Our shows are produced by the community (you) and can be on any topic that are of interest to hackers and hobbyists.
Podcast websiteListen to Hacker Public Radio, Made In Chaos with Ryan Curtis and many other podcasts from around the world with the radio.net app
Get the free radio.net app
- Stations and podcasts to bookmark
- Stream via Wi-Fi or Bluetooth
- Supports Carplay & Android Auto
- Many other app features
Get the free radio.net app
- Stations and podcasts to bookmark
- Stream via Wi-Fi or Bluetooth
- Supports Carplay & Android Auto
- Many other app features

Hacker Public Radio
Scan code,
download the app,
start listening.
download the app,
start listening.




























