Music Lyric Quiz Generator

Creating a Music Lyric Quiz Shiny App with the Genius API

One of my resolutions for 2023 is to spend more time coding up ideas. One recent idea I had related to music lyrics, and so I decided to create a Music Lyrics Quiz game.

To do this, I first used the API provided by Genius - an online music lyric knowledge database - to gather in some lyrics. Then, I used R Shiny to create a simple app that provided users with a random lyric and three possible answers. The app loops through 25 questions and provides the user with a score at the end.

Here are two standalone quiz apps I’ve created using this process for two of my favourite artists:

I posted these to Facebook and Twitter groups concerned with these two bands, and got some great feedback. Firstly, the users spotted a basic error in my code whereby there were songs were duplicated in the answers. Secondly, and more usefully, the users said that lryics that contained some or all of the song title made the quiz a bit too easy. I added in a filter so that lines with a high degree of similarity to the songs title were removed.

All of the code required to recreate that workflow is available in this repo on my GitHub page, from gathering Genius data to the development of the Shiny app.

My next steps with this mini-project will be to tidy up some of the Shiny code - it’s a little ‘hacky’ at present - and from there explore how the app can be extended so that users can add in the name of an artist themselves, and have the app create a quiz on the fly.

I’ve enjoyed this process, and already have a few ideas for similar, small-scale music related projects. As and when I make progress with those, I’ll post them here. In the meantime, I hope you have fun either playing with the quizzes above, or with creating your own.

1: Gather data from the Genius API

To get the lyrics required, you’ll first need to sign up for a free API key/token at Genius, and then install the packages below.

#Install required pacakges
library(geniusr)
library(dplyr)
library(tidytext)
library(spotifyr)
library(tidyverse)
library(stringr)

#1: Establish connection with Genius API
#GENIUS API
key <- "<<YOUR KEY HERE>>"
secret <- "<< YOUR SECRET HERE"
GENIUS_API_TOKEN <- "<< YOUR TOKEN HERE>>"

Then, add the name of the artist you want and search for their id.

#2 - Get Artist ID
artist_search <- "Teenage Fanclub"
artist <- search_artist(artist_search, n_results = 10, GENIUS_API_TOKEN)

#Check the artist dataframe to make sure you have the correct one.
head(artist)
#Get the correct ID - in most cases, this will be the first/only row. 
artist <- artist$artist_id[1]

Once you have the artist id, you can search for all songs of theirs on the Genius service.

#3 - Got songs
artist_songs <- get_artist_songs_df(
  artist,
  sort = c("title", "popularity"),
  include_features = FALSE,
  GENIUS_API_TOKEN
)

At this point, you may want to explore the results. The Genius service sometimes contains different versions of the same song - e.g. live versions or remixes.

#Remove duplicated rows - some songs may appear more than once in the Genius dataset
artist_songs <- artist_songs[!duplicated(artist_songs$song_name), ]

#Remove 'Live' or 'Remix' - for example, live versions of remixes
artist_songs <- subset(artist_songs, !grepl("Live", song_name))
artist_songs <- subset(artist_songs, !grepl("Remix", song_name))

Once you’re happy that the list contains the songs you want, run the following code. The process will take approximately 5-7 minutes for 100 songs. You may not get every song, but when I’ve tried this with a few artists I got the majority of those in the original list.

#4 - Get Lyrics
artist_lyrics <- data.frame() #create and empty dataframe to store lyrics
for (i in 1:nrow(artist_songs)) { #loop through each row s
  song <- artist_songs$song_id[i] #get individual song id
  print(paste("Getting lryics for", artist_songs$song_name[i])) #paste message to the console.
  try ({ #get lyrics for song, add to artist_lryics data frame.
    one_song <- get_lyrics_id(song_id = song, GENIUS_API_TOKEN)
    one_song$line_id <- 1:nrow(one_song)
    artist_lyrics <- rbind(artist_lyrics, one_song)
  })
  Sys.sleep(2) #introduce pause before attempting next row.
}

Next, perform some cleaning and housekeeping on the results. In particular here, I’m removing any lines of lyrics where there are fewer than 5 words and also remove any lines that exhibit a similarity to the song title.

#5 - Optional cleaning before Step 6 - 
#some of this could be handled in app as user input, but may be better done before
length(unique(artist_lyrics$song_name)) #compare songs returned with original list

# Remove any line of lyrics with fewer than 5 words
artist_lyrics <- artist_lyrics %>%
  filter(str_count(as.vector(line), "\\b\\w+\\b") > 5)

# Identify lyrics that are similar to the song title - if these appear as options in the quiz, it may be too easy.
library(stringdist)
artist_lyrics$diff <- stringdist(artist_lyrics$line, artist_lyrics$song_name, method = "cosine")
range(artist_lyrics$diff) #the lower the number, the higher the similarity between lyric and title

#For example, filtering on any diff value lower that .25
#You could potentially add a slider/other input to your app based on this value
# eg - a slider indicating difficulty that filters in/out similar lyrics.
artist_lyrics <- artist_lyrics %>%
  filter(diff > .25)

#Another option is to see if the completed song title is contained within the lyric
#artist_filtered <- artist_lyrics %>%
#  filter(!tolower(artist_lyrics$song_name) %in% tolower(artist_lyrics$line))

Finally, write out the results to a folder in your working directory called data. This is where the app file created in the next step will look for the data.

#6 - Write to data folder so that Shiny App can use it.
write_rds(artist_lyrics, "data/artist_lyrics.rds")

2: Create Helper Files for app

I like to keep the app.R file of my Shiny apps relatively simple. This makes them easier to edit. To do this, I create additional files and add these to a folder called helpers in the working directory.

Load_Packages.R

Create this script, save it as load_packages.R.

library(shiny)
library(shinyjs)
library(stringr)
library(tidyverse)
library(markdown)

misc.R

  • The prepQuiz function will take the lyrics data created earlier and generate a random lyric, the name of the song it came from, and the names of two further songs. This forms the basis of the quiz
  • The url object provides the text for the Share on Twitter button in the main app. You can amend this to reflect your own app.
  • Save this file to helpers as misc.R.

As I mentioned above, elements of this are a little ‘hacky’, but the main thing is the quiz works! I’ll be spending some time tidying up the script below so that the app is a little more efficient and tidy.

##DEFINE THE FUNCTION THAT GENERATES THE QUIZ
prepQuiz <- function(x) {
  y <- sample_n(x, size = 50, replace = FALSE, weight = NULL) %>%
    distinct(song_name, .keep_all = TRUE) %>%
    select(line, song_name) %>%
    rename(question = line,
           answer1 = song_name)
  y <- sample_n(y, 25)
  y$answer2 <- NA
  y$answer3 <- NA
  for (i in 1:nrow(y)) {
    song_sample <- x %>%
      filter(song_name != y$answer1[i]) %>%
      distinct(song_name, .keep_all = TRUE) %>%
      sample_n(2)
    y$answer2[i] <- song_sample$song_name[1]
    y$answer3[i] <- song_sample$song_name[2]
  }
  y$correct <- y$answer1
  return(y)
}

##TEXT FOR CREATING TWEET
url <- "https://twitter.com/intent/tweet?text=I%20just%20played%20the%20Teenage%20Fanclub%20Lyrics%20Quiz%20by%20@craigfots%20Click%20here%20to%20have%20a%20go&url=https://craigfots.shinyapps.io/tfc_quiz/"

CSS (Optional)

In a folder called www in your working directory, create this file and save it as quiz_app_style.css.

body {
  background-color: #edf5fe;
  color: #0a0d17; /* text color */
}

/* Change header text to imported font */

h1 {
  color: #063970;
  font-family: 'Judson', sans-serif;
  font-weight: bold;
}

h3 {
  font-family: 'Judson', sans-serif;
}

h6 {
  font-family: 'Judson', sans-serif;
}

h2 {
  font-family: 'Quando', sans-serif;
  color: #066e70;
  font-weight: bold;
}

h4 {
  font-family: 'Quando', sans-serif;
    font-style: italic;
}

hr {
  border-top: 1px solid #0a0d17;
}

a[href*="//"] {
color: #063970;
font-weight: bold
}
                                
a[href^="mailto:"] {
color: #063970;
font-weight: bold
}

input[type='radio']:after {
  width: 15px;
  height: 15px;
  border-radius: 15px;
  top: -1px;
  position: relative;
  background-color: #f5f3ef;
  content: '';
  display: inline-block;
  visibility: visible;
  border: 4px solid #063970;
}

input[type='radio']:checked:after {
  width: 15px;
  height: 15px;
  border-radius: 15px;
  top: -1px;
  position: relative;
  background-color: #063970;
  content: '';
  display: inline-block;
  visibility: visible;
  border: 4px solid #063970;
}

3: Create App

For ease of use, I’m providing all of the app code in one go - but I’ll point out some lines where you may want to edit/remove elements

  • In ### CSS you may want to remove all elements if you’re not using a style sheets
  • Under ### Title, amend the title of the quiz.
  • Likewise, amend the text in output$intro.
### LOAD HELPER FILES
source('helpers/load_packages.R', local = TRUE) #Load required R Packages
source('helpers/misc.R', local = TRUE)  #Functions

#UI
ui <- fluidPage(
  
  ### CSS  
  tags$head(
    tags$link(rel = "stylesheet", type = "text/css", href = "quiz_app_style.css")
  ),
  
  useShinyjs(),  # include shinyjs library
  
  # TITLE
  h1("The Teenage Fanclub Lyrics Quiz"),
  hr(),
  uiOutput("intro"),
  h4(uiOutput("question_number")),
  h2(uiOutput("question")),
  hr(),
  actionButton(inputId = "start_quiz", label = "Start Quiz", style ="color: #010203; background-color: #c7fbfc; border-color: #9c7c38"),
  uiOutput("answers"),
  actionButton(inputId = "submit", label = "Submit", style ="color: #010203; background-color: #c7fbfc; border-color: #074283"),
  h4(uiOutput("selected")),
  p(),
  actionButton(inputId = "next_question", label = "Next Lyric", style ="color: #010203; background-color: #c7fbfc; border-color: #074283"),
  p(),
  uiOutput("your_score"),
  h3(uiOutput("your_final_score")),
  p(),
  actionButton(inputId = "play_again", label = "Play Again?", style ="color: #010203; background-color: #c7fbfc; border-color: #074283"),
  actionButton("twitter_share",
               label = "Share on Twitter",
               icon = icon("twitter"),
               onclick = sprintf("window.open('%s')", url), 
               style ="color: #FFFFFF; background-color: #0955a8; border-color: #0955a8"),
  h6(includeMarkdown('app_markdown_files/quiz_app_footer_text.md'))
)

# Define server logic required to draw a histogram
server <- function(input, output, session) {
  
  #hide buttons
  show("intro")
  hide("question_number")
  hide("question")
  hide("answers")
  hide("submit")
  hide("your_score")
  hide("next_question")
  hide("selected")
  hide("your_final_score")
  hide("play_again")
  hide("twitter_share")
  
  output$intro <- renderText({
    "The lyrics for 192 songs by <b>Teenage Fanclub</b> were scraped from the Genius API. This app randomly selects a lyric from one of those songs, and will then provide the titles of three songs. Can you correctly identify which song the lyric came from? Sometimes the app will randomly select a line that will give the game away...but sometimes the lyric might be a little harder to place. You will have 25 questions, and will receive a score at the end. Click the Start Quiz button to play. Good luck! "
  })
  
  # Initialize counter to 1
  counter <- reactiveValues()
  counter$row <- 1
  counter$score <- 0
  counter$data <- NULL
  
  observeEvent(input$start_quiz, {
    hide("intro")
    hide("start_quiz")
    show("question_number")
    show("question")
    show("answers")
  })
  
  data_sample <- reactive({
    lyrics <- readRDS('data/artist_lyrics.rds')
    data <- prepQuiz(lyrics)
    data_sample <- data[counter$row, ]
  }) 
  
  output$question_number <- renderText({
    paste("Lyric ", counter$row, " of 25", sep = "")
  })
  
  output$question <- renderText({
    data_sample <- data_sample()
    data_sample$question[1]
  })
  
  output$answers <- renderUI({
    data_sample <- data_sample()
    songs <- c(data_sample$answer1[1], data_sample$answer2[1], data_sample$answer3[1])
    songs <- sample(songs)
    radioButtons("answers", "Which song does that come from?", songs, selected = character(0))
  })
  
  output$selected <- renderText({
    data_sample <- data_sample()
    if (is.null(input$answers)) {
      ""
    }
    else if (input$answers == data_sample$answer1[1]) {
      paste("<b>Correct!</b> That line is from ", "<b>", data_sample$answer1[1], "</b>", sep="")
    } else {
      paste("<b>Nope!</b> That line is from ", "<b>",data_sample$answer1[1], "</b>", sep="")
    }
  })
  
  observeEvent(input$answers, {
    show("submit")
    data_sample <- data_sample()
    current_score <- counter$score
    if ((input$answers == data_sample$answer1[1])) {
      current_score <- current_score + 1
    } else {
      current_score <- current_score
    }
    counter$score <- current_score
  })
  
  output$your_score <- renderText({
    paste("Your score is ", counter$score, " out of a possible ", counter$row, ". You are currently at ", 
          round(counter$score/counter$row*100, 2), "%.", sep="")
  })
  
  output$your_final_score <- renderText({
    paste("You scored ", counter$score, " out of a possible ", counter$row, sep="")
  })
  
  observeEvent(input$submit, {
    if (counter$row < 25) {
      hide("submit")
      hide("answers")
      show("selected")
      show("your_score")
      show("next_question")
    } else {
      hide("submit")
      hide("answers")
      show("selected")
      show("your_final_score")
      show("play_again")
      show("twitter_share")
    }
  })
  
  observeEvent(input$next_question, {
    counter$row <- counter$row + 1
    hide("next_question")
    hide("submit")
    hide("your_score")
    hide("selected")
  })
  
  observeEvent(input$play_again, {
    counter$row <- 1
    counter$score <- 0
    data_sample <- reactive({
      lyrics <- readRDS('data/artist_lyrics.rds')
      data <- prepQuiz(lyrics)
      data_sample <- data[counter$row, ]
    }) 
    hide("play_again")
    hide("your_final_score")
    hide("twitter_share")
    hide("selected")
    show("question")
    show("answers")
  })
}
# Run the application 
shinyApp(ui = ui, server = server)

4: Deploy app

Assuming all of the above has worked, you should be able to run and test your app locally. You can also open it on a browswer window to check the CSS and external links. Once you’re happy, you can delpoy the app to Shinyapps.io or an equivalent service.

Dr Craig Hamilton
Dr Craig Hamilton

My research interests include popular music, digital humanities and online cultures.