Study Music Finder

I wrote a python tool that randomly chooses songs from the focus genre of Spotify. It records how focused I felt, so that I can find the best music to study to. The player itself is controlled over http get requests and the data is stored in a sqlite database.

Here's the results of using this tool for a couple of weeks.

Electro Focus        5.0
Pure Mellow Jazz     5.0
Quiet Moment         5.0
Deep Focus           4.8
Focus Now            4.714285714285714
Beats to think to    4.5
Peaceful Piano       4.5
Soft Focus           4.454545454545454
Lo-Fi Beats          4.375
Binaural Beats: Focus 4.0
Focus On The Remix   4.0
Indie Folk for Focus 4.0
Neotic               4.0
Brain Food           3.9
lofi hip hop music - beats to relax/study to 3.6666666666666665
Just Focus           3.3333333333333335
Mathy math rock for mathy math people 3.3333333333333335
daily mix 1          3.0
Midnight Overdrive   2.0
Calm Before the Storm 1.6666666666666667
Focus Flow           1.0
Nature Sounds        0.5
x86.jpeg x86.jpeg

Implementation details

The table schema is

sqlite> .schema
CREATE TABLE lookup (uri varchar(255),name varchar(255));
CREATE TABLE work (uri varchar(255), focus int, time int);

The table work tracks the Spotify uri and how focused I felt. The Spotify uri is the unique id Spotify uses to identify songs and playlists. The lookup table maps the playlist name to its uri and stores all the available playlists.

On startup it searches for the 10 most popular playlists in Spotify's focus genre and adds them to the lookup table.

# aquire database
conn = sqlite3.connect('tracks.db')
c = conn.cursor()

# get search results for focus spotify by default returns 10
for i in"focus",type='playlist')['playlists']['items']:
    # check if the playlist uri is already in the lookup table
    c.execute("select uri from lookup where uri = '" +i['uri'] + "';")
    found = c.fetchone()
    if not found:
        # get the playlist name associated with the uri
        name = getName(i['uri'])
        print("adding playlist " + name)
        # add the playlist to lookup
        c.execute("INSERT INTO LOOKUP VALUES ('"+ i['uri'] + "','" + name + "')")

When a get request is sent to /start a track is selected from the database that either hasn't been played yet or has an average rating higher than two. This is so that the user can prevent unproductive songs from being played by giving them a lower rating.

  nullOrAvgGreaterThan2 = """
SELECT lookup.uri,avg(work.focus) 
FROM lookup 
LEFT JOIN work  USING(uri) 
HAVING avg(work.focus) > 2 or avg(work.focus) is NULL 
SELECT lookup.uri,avg(work.focus) 
FROM work 
LEFT JOIN lookup USING(uri) 
WHERE work.focus IS NULL 

def start():
    global chosen,tokeninfo
    # check if our spotify token has expired
    tokeninfo = refreshtoken(tokeninfo)
    conn = sqlite3.connect('tracks.db')
    c = conn.cursor()
    uris = c.fetchall()
    # choose track from list of uris
    chosen = random.choice(uris)[0]
    # authenticate with the spotify api
    sp = spotipy.Spotify(tokeninfo['access_token'])
    # play back on laptop
    return "started music\n"

Since Sqlite doesn't have outer joins I used the union of two left joins in order to associate the song's uri with it's average rating. Querying just the work table wouldn't have given me the songs that hadn't been played yet because those songs don't have a record associated with them in work. The start method also sets the global variable chosen to the uri of the song that was picked so that we can record it later.

When a post request is sent to /stop the music is paused and the associated rating and time spent listening is recorded.

def stop():
    global tokeninfo
    # check if we need to refresh our spotify token
    tokeninfo = refreshtoken(tokeninfo)
    sp = spotipy.Spotify(tokeninfo['access_token'])
        # try to pause music
        print("Couldn't stop spotify")
    if flask.request.method == 'POST':
        time = flask.request.values.get("time")
        rating = int(flask.request.values.get("focus"))
        # check if rating is  non zero
        if rating > 0 :
            conn = sqlite3.connect('tracks.db')
            c = conn.cursor()
            c.execute("INSERT INTO work VALUES ('" + chosen+ "'," + str(rating) + "," + str(time) +")")
    return "stopped music\n"

The rating and time is inserted into the table if the rating is above zero. This gives the user a conveniant way of preventing the script recording song data if you were doing something like listening to lecture and weren't listening to music.

Since it's controlled over http requests it was easy to integrate it with Emacs orgmode. Music will start playing when I clock in to an orgmode task and music will stop playing when I clock out of it.

(defun playmus()
  (shell-command "curl"))

(defun stopmus()
  (let* ((minutes  (string-to-number (subseq (org-clock-get-clock-string) 4 6)))
         (hours    (* 60 (string-to-number (subseq (org-clock-get-clock-string) 2 3))))
         (total    (number-to-string (+ hours minutes)))
         (rating   (read-string "How focused where you from (1-5): "))
         (comm     (concat "curl --data 'focus=" rating "&time=" total "'")))
    (shell-command comm)))

(add-hook 'org-clock-in-hook #'playmus)
(add-hook 'org-clock-out-hook #'stopmus)

When I clock out of an Emacs TODO item I get prompted for a rating and the time I spent listening to music gets automatically recorded. The time is parsed from the orgmode clock-string variable automatically.


Last updated: 2019-12-16 Mon