Thursday, October 22, 2015

Bash Scripts for Non-Repeating Random Playback

I have an HTPC with a huge amount of files to choose from, which can lead to analysis paralysis / the paradox of choice: when there's so much to choose from, I end up consuming the same handful of media over and over instead of consuming a proper variety. Many media players have "shuffle" functions that will pick a random item from a list but that only works for traditional media and doesn't help with, for example, emulated video games. So, I decided to play around with some bash scripting to see if I could come up with anything more universal.

The first thing I wanted to do was to create a playlist based on a recursive scan of a directory. For example, I have my TV shows separated by show and then subdivided by season/series, and I want all of these files included in a playlist, which I accomplished with this script:
#!/bin/bash
if [ -e playlist]
then
rm playlist
fi
for f in **/*
do
echo $f >> playlist
done
This script will check for any existing playlist and delete it if it finds one already, then it search recursively and adds any files (but not directories, importantly) from any subdirectories to the playlist. If I add more files (e.g., I get new episodes of a show), I can just run the playlist-maker script again and it will delete the old playlist and make a new one with the new files added.

So that's great. However, these files are all in order and I want them to be randomly sorted instead (in case my desired launch program doesn't have a built-in shuffle function), which I can do like this:
#!/bin/bash
if [ -e random-playlist ]
then
rm random-playlist
fi
for f in **/*
do
echo $f >> nonrandom-playlist
shuf --output=random-playlist nonrandom-playlist
done
rm nonrandom-playlist
This script does all of the same steps as before but it also uses the shuf command, which is a common UNIX utility that will randomize the entries in the non-randomized playlist and then output a randomized playlist. The script then deletes the non-randomized playlist.

Alright. Looking good. If my playback program has a built-in shuffle function, I can just pass this playlist to it and it will shuffle among those files. However, as anyone who uses shuffle frequently will tell you, it often feels like a small minority of songs/videos gets played more often than others (whether this is actually true or not is up for some debate but it can be annoying all the same), so I want to remove entries from the playlist once I've consumed them so there can't be any repeats until every file has been played at least once:
#!/bin/bash
line=$(head -1 random-playlist)
echo "Next up: $line"
mpv --fs "$line"
echo "I have a lot of great memories from that episode..."
sed -i -e "1d" random-playlist
echo "And now they're gone!"
This script uses the UNIX utility head, which lists any number of lines from a file, starting with the first line. In this case, I just ask for the first line, echo it (for diagnostic purposes, and it will be useful later) and then pass it to my player (in this case, mpv). After playback is finished, it echoes again and then uses the UNIX utility sed, which is a super-powerful text/stream editor, to delete the first line ("1d") from the random playlist. It echoes one more time to complete the Bender quote and let me know that the entry was successfully deleted.

Ok, this is great but I don't want to have to always run my script to consume the next file. That is, I want to just get things rolling and have a marathon of random selections without continued interaction from me. To do this, we just wrap the whole thing in a 'for' loop, like this:
#!/bin/bash
for i in {1..100}; do
line=$(head -1 random-playlist)
echo "Next up: $line"
mpv --fs "$line"
echo "I have a lot of great memories from that episode..."
sed -i -e "1d" random-playlist
echo "And now they're gone!"
done
This will go through playing and deleting 100 times (you could make this 1,000 or 62 or whatever), which is great. However, I don't usually want to watch 100 episodes of a show, and if I launched this script from a desktop environment (i.e., by double-clicking), I have no way of stopping it! That is, each time I close my playback program, either because the episode is over or I clicked on the 'close' button, the next episode is queued up and starts automatically. If this were running in a terminal window, I could just close that window and it would stop the script in its tracks but this leads to a dilemma: it's not convenient to have to launch the script from a command line each time instead of double-clicking, and if I tell Nautilus (my file manager) to launch scripts in a terminal when I double-click them, it tries to run the scripts from my home directory instead of their actual location >.<

Thankfully, there's a solution. I can write a helper script that doesn't care if it runs from the home directory, and that script can launch my marathon script:
#!/bin/bash
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
x-terminal-emulator -e ./marathon-script
The first line of the script is a handy one-liner that navigates to the directory where the script lives. The second line tells it to launch whichever terminal application is registered to your alternatives system as the 'terminal emulator,' in my case it's gnome-terminal, and the -e switch tells it to run the marathon script inside that terminal, where we can easily view our diagnostic echo messages and close it when we're done consuming media.

As I mentioned, these scripts are easily modified to suit any kind of file, so I have some set up to launch a random NES game from my collection via RetroArch. I plan to use this setup to play through the entire NES library over a period of a few weeks/months.

Analytics Tracking Footer