The Problem

I have been known to have something of an eclectic music taste. I can go from TOOL to Billy Eilish to Big K.R.I.T. to Nelly Furtado to Armin van Buuren without much consideration. While I occasionally end up going down a bit of a genre rabbit hole from time to time, generally I like the mix. I get bored if I listen to too much of the same genre. With that said, I find most ways of having music picked for me to be frustrating. Listening to the radio you end up only hearing top singles from a generally narrow genre band, so you’re changing stations often. The same thing happens with most of the dynamic playlists I’ve experienced from Spotify and similar streaming services - they take an artist or two that you’re in to and play you a bunch of music that sounds like them. It just always feels too narrow to me, and just because I like Sigrid and Lorde doesn’t mean I’m interested in Katy Perry and Justin Bieber, you know?

The Fix

First Step

I have a large library of music at home and I use the Logitech Media Server (also known as SqueezeboxServer) to manage and play it across all my devices. This allows me to do cool things like play the same music, synchronized, in each room of the house, without being locked into a specific vendor or format like Airplay. This software has a robust selection of features and third party plugins that enable it to do just about anything I could possibly need.

I started by rating my music. With over 25,000 tracks in the library, this is a long, boring, and time consuming process, but it needs to be done. I created a scheme for myself to help with the ratings.

Rating Meaning
5☆ I pretty much always want to listen to this. DNA by Kendrick Lamar fit in here.
4.5☆ Great music, but maybe not every day. Time by Jungle is one example for me on this one.
4☆ A little less than 4.5☆ but still music I know well and enjoy. Bullet with Butterfly Wings by The Smashing Pumpkins
3.5☆ Music I enjoy when it comes on the radio, but don’t go out of my way to put on. Just a Girl by No Doubt
3☆ Music that’s adjacent to the higher ☆ levels, but isn’t otherwise a big draw for me. Don't Carry it All by The Decemberists
2.5☆ Other music that doesn’t stand out as bad. Everything Will Be Alright by The Killers
<2.5☆ Music I have because it’s a part of an album, but don’t want to listen to randomly like intros, outros, and skits

Second Step

Next I needed to find a way to generate playlists from this data. I started by installing the TrackStat and Dynamic Playlists plugins for Logitech Media Server, which was great in that it gave me a way to play a dynamic playlist based on the ratings. Unfortunately, it didn’t give me much flexibility beyond selecting a rating I wanted to listen to, or just saying I want to listen to something like the “Top rated not played recently.” This didn’t quite fit the bill for me, I want a wider experience than that.

In my younger years I would listen to the A State of Trance podcast by Armin van Buuren quite regularly. One of the things that always stood out to me, both on this podcast itself but also in DJ sets overall, was that while I would enjoy the set overall, not every song was a ‘banger’. In fact, when there would be a set made up of ‘top songs’ I would generally tune out, it was too much. Sets needed breathing room. You have a really exciting track or two that pulls the crowd in and gets the energy up, and then you lighten up or slow down a little to give everybody a breather before bringing the energy back. This is what I wanted from my playlists, to have lots of great music but intersperse it with unfamiliar or ‘less great’ music so it didn’t get tiring. This also gives the benefit of helping to explore music I don’t know as well, without getting too tired of completely unknown music. In comes the SQL Playlist plugin.

SQL Playlist Plugin

In short, this plugin does exactly what it says, lets you use custom SQL for generating playlists. It comes with several defaults to try and a creator to make some of your own if you don’t know SQL yourself. By using this I can create a single playlist that meets all of my needs by combining ratings, how recently a song was played, and how recently a song was added, and every time I click it I will end up with a completely unique playlist of music I love, like, sort-of know, and have yet to hear, mixed together.

Generating the SQL

I started by creating one of the predefined playlist types and reviewing the SQL. This gives me a starting point showing what columns I can use and what needs to be returned for the playlist to work.

-- PlaylistName:Cams Playlist Generator
-- PlaylistGroups:
create temporary table randomweightedratingslow as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating<50
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-86400)
	order by random()
	limit 30;


create temporary table randomweightedratingshigh as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=50
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-86400)
	order by random()
	limit 70;

create temporary table randomweightedratingscombined as
	SELECT * FROM randomweightedratingslow
	UNION
	SELECT * from randomweightedratingshigh;

SELECT * from randomweightedratingscombined
	ORDER BY random()
	limit 10;

DROP TABLE randomweightedratingshigh;
DROP TABLE randomweightedratingslow;
DROP TABLE randomweightedratingscombined;

This is a perfect example for my needs. Basically what this SQL does is it creates two separate lists; the first pulls 30 low-rated tracks and the second pulls 70 high-rated tracks; then combines them to make a 100 track list and pulls 10 at random from it. Perfect, this is exactly what I was going for. Now to create my own version.

My Custom Playlist

First, lets get the 5☆ tracks. You’ll remember I described these as music I pretty much always want to listen to. Let’s grab 10 of the ones that haven’t been listened to in 24hrs.

create temporary table fivestartracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating=100
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-86400)
	order by random()
	limit 10;

Next, let’s broaden the allowable ratings, but push out the last listened to be 3 days.

create temporary table fourhalfstartracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=90
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-259200)
	order by random()
	limit 10;

And again once more, further increasing the last listened limit and reducing the minimum rating limit.

create temporary table fourstartracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=80
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-604800)
	order by random()
	limit 10;

Now let’s open it up wide, collect everything above 2.5☆

create temporary table twopointfiveuptracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=50
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-604800)
	order by random()
	limit 10;

Music that I’ve recently added or modified is generally going to be something I’m interested in hearing. Let’s add some recently modified to the mix.

create temporary table recentlymodifiedtracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=50
    and track_statistics.added>STRFTIME("%s",DATE('NOW','-30 DAY'))
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-86400)
	order by random()
	limit 10;

Then, just like the original example, I’m going to join all of these together to create a single table, and then select 50 tracks total at random from it.

-- PlaylistName:Random rated songs
-- PlaylistOption Unlimited:1
-- PlaylistGroups:
create temporary table fivestartracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating=100
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-86400)
	order by random()
	limit 10;

create temporary table fourhalfstartracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=90
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-259200)
	order by random()
	limit 20;

create temporary table fourstartracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=80
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-604800)
	order by random()
	limit 20;

create temporary table twopointfiveuptracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=50
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-604800)
	order by random()
	limit 10;

create temporary table recentlymodifiedtracks as select tracks.url as url from tracks
	left join dynamicplaylist_history on
		tracks.id=dynamicplaylist_history.id and dynamicplaylist_history.client='PlaylistPlayer'
	left join track_statistics on
		tracks.url=track_statistics.url
	where
		audio=1
		and track_statistics.rating>=50
    and track_statistics.added>STRFTIME("%s",DATE('NOW','-30 DAY'))
		and dynamicplaylist_history.id is null
		and ifnull(track_statistics.lastplayed,0)<(unix_timestamp()-86400)
	order by random()
	limit 10;

create temporary table allselectionscombined as
	SELECT * FROM fivestartracks
	UNION
	SELECT * FROM fourhalfstartracks
  UNION
  SELECT * FROM fourstartracks
  UNION
  SELECT * FROM twopointfiveuptracks
  UNION
  SELECT * FROM recentlymodifiedtracks ;

SELECT * from allselectionscombined
	ORDER BY random()
	limit 50;

DROP TABLE fourhalfstartracks;
DROP TABLE twopointfiveuptracks;
DROP TABLE recentlymodifiedtracks;
DROP TABLE fivestartracks;
DROP TABLE allselectionscombined;

There we go, now I have a playlist generator that builds playlists dynamically based on ratings I set up, what I have listened to recently, and what I have added recently.

Final Output

Now let’s check our work and take a look at what a sample playlist might look like

Song Title Artist Album Title
Magpie Caribou Suddenly
Good Ass Intro Chance the Rapper feat. BJ the Chicago Kid Acid Rap
Like Toy Soldiers Eminem Encore
00000 Million Bon Iver 22, a Million
That Song Big Wreck Big Shiny Tunes 3
In the City Eagles Hell Freezes Over
Streets on Fire Lupe Fiasco Lupe Fiasco’s The Cool
LOVE. Kendrick Lamar feat. Zacari DAMN.
HEAD SHOTS Tobe Nwigwe feat. D Smoke CINCORIGINALS
Electric Feel MGMT Oracular Spectacular
We Go On The Avalanches feat. Cola Boyy & Mick Jones We Will Always Love You
Sunny’s Time Caribou Suddenly
Spill Vill Spillage Village, Desi Banks & Big Rube feat. Kountry Wayne Spilligion
Moon Song Phoebe Bridgers Punisher
Oblique City Phoenix Bankrupt!
Oh My Heart R.E.M. Collapse Into Now
Conventioneers Barenaked Ladies Maroon
初登場 UNKLE Rōnin I

Now I have an easy “Just Press Play” solution to listening to music around the house. Every time I press play, something different will play, and it will lean towards music I like. Perfect!

What about my wife?

My wife and I have very similar music tastes, though she tends to find I listen to a little too much rap and eletronic music. So, I created a variant of my playlist generator that would work for her.

To make this as easy as possible, I created another temporary table called reducedtracks that would select all tracks except those labelled with Rap, Hip Hop, and related genres. I then modified the subsequent tables to pull from reducedtracks instead of tracks and everything else just worked.

create temporary table reducedtracks as select * from tracks
  join genre_track ON
    tracks.id=genre_track.track
      join genres ON genres.id = genre_track.genre AND
      genres.name<>'Trance' AND
      genres.name<>'Electronic' AND
      genres.name<>'Rap' AND
      genres.name<>'Hip Hop' AND
      genres.name<>'Hip-Hop' AND
      genres.name<>'Rap/Hip-Hop' AND
      genres.name<>'Rap/Hip Hop' AND
      genres.name<>'Gangsta Rap' ;