Updating my youtube music player

Recently the youtube search results page changed slightly; this caused my youtube music player to stop working properly. I’ve rewritten parts of it; not only to work with the new youtube search, but I’ve also taken the opportunity to improve a few other areas.

The biggest change is the integration of an automatic pause feature based on bluetooth proximity sensing. If a bluetooth device’s MAC address is entered, the music will automatically pause when that device leaves visible range; once it returns, the music will resume automatically. I have also added the ability to use 3 basic commands at the search prompt: /skip /pause and /play; they do exactly what you’d expect.

The shuffle feature, that was originally implemented as an afterthought with a bash script, has now been integrated into the main ruby script. It works in the same way- it keeps playing random mp3s from the current working directory until something gets put on the playlist. The actual playing of the tracks I’ve moved off to a separate method, it now attempts to look up the title and artist and prints them as the track starts playing. The filename is also printed as it makes it easy to delete undesired tracks (bad quality live versions, clean radio edits, your brother adding the radio 4 shipping forecast etc.). I’ve also improved the error handling such that it can now survive most accidental bad inputs.

youtube_play.rb:

#Written by Mike Worth, http://www.mike-worth.com/2012/06/24/updating-youtube-music-player/
require 'hpricot'
require 'open-uri'

#You`ll have to look this up manually by using the hcitool command
#leave it blank to disable the autopause feature
BLUETOOTH_ADDRESS='00:26:5F:C6:30:A3'

#returns whether or not the bluetooth device can be seen as a boolean
def isPresent(address)
  #do it twice to double check that we didn`t just momentarily loose contact
  if (IO.popen('hcitool name '+address).read=='' && IO.popen('hcitool name '+address).read=='')
    return false
  else
    return true
  end
end

#picks a random mp3 from cwd and plays it
def shuffle
  tracks=Dir['*.mp3']
  if tracks.length==0
    sleep 1
  else
    track=tracks[rand(tracks.length)]
    play(track)
  end
end

#plays the track with mpg321 and tries to look up artist/title details form youtube to print
def play(track)
  begin
    page=Hpricot(open('http://www.youtube.com/watch?v='+track))
    begin
      artist=page.search('span.metadata-info')[0].inner_text.gsub('Artist:','').strip
      title=page.search('span.metadata-info')[1].inner_text.split('"')[1].strip
      track_title=artist+': '+title+'('+track+')'
    rescue
      track_title=page.search('[@id=eow-title]').inner_text.strip.gsub("\n",'')+'('+track+')'
    end
  rescue OpenURI::HTTPError
    track_title=track
  end
  puts 'Playing '+track_title
  $player_pid=IO.popen('mpg321 -q '+ track).pid
  Process.wait($player_pid)
end

#assuming that pid is an instance of mpg321 this will check if it has already been paused and return a boolean
def playing(pid)
  return File.open('/proc/'+$player_pid.to_s+'/stat','r').gets.split(' ')[2]!='T'
end

playlist=Array.new
#This thread actually plays the tracks in the playlist then removes them.
Thread.new{
  while true
    #If the playlist is empty, play random mp3s until someone adds something
    if playlist.length==0 then
      shuffle
    else
      play(playlist.shift)
    end
  end
}

#If the bluetooth device leaves range, this thread pauses the music until it returns
autopause=Thread.new {
if BLUETOOTH_ADDRESS!=''
    $autopaused=false
    previous_state=true
    while true
      if isPresent(BLUETOOTH_ADDRESS)
        #if the phone wasn't here before but is now, it's just arrived back. If we autopaused the music, resume it
        if !previous_state and $autopaused
          Process.kill('CONT',$player_pid)
          $autopaused=false
        end
        previous_state=true
      else
        #if the phone was here before but isn't now, it's just left. If music is playing, pause it.
        if previous_state and playing($player_pid)
          Process.kill('STOP',$player_pid)
          $autopaused=true
        end
        previous_state=false
      end
    end
  end
}

#While the main thread handles the user interface:
while true
  puts 'What would you like to listen to?'
  search_string= gets.chomp

  #allow the use of a few basic commands
  if search_string=='/pause'
    Process.kill('STOP',$player_pid)
  elsif search_string=='/skip'
    Process.kill('KILL',$player_pid)
  elsif search_string=='/play'
    Process.kill('CONT',$player_pid)

  else
    puts'Searching for '+search_string+'...'
    #Use Hpricot to load the youtube search page and scrape the titles and ids of the top 5 results
    begin
      doc = Hpricot(open('http://www.youtube.com/results?search_type=search_videos&search_query='+search_string.gsub(' ','+')))
      result_divs=doc.search('li.result-item-video')
      results=Array.new

      #Ask the user which one they want
      for i in 0..4 do
        #keep the a object as it has both bits of data
        results[i]=result_divs[i].search('a.result-item-translation-title')
        puts (i+1).to_s + ') ' + results[i].inner_text
      end
      choice=gets.chomp.to_i  
      if (1..5).include?(choice) then
        choice-=1
        youtube_id = results[choice].attr('href').split('&')[0].split('=')[1]
    
        #Download/extract the audio in parallel so that more tracks can be added in the meantime
        Thread.new(youtube_id,results[choice].inner_text) {|id,title|
          if ! File.exist?(id+'.mp3')
            puts 'Downloading ' + title
            system('youtube-dl --extract-audio -w -q -f 5 --no-part '+id)
          end
          playlist<<id+'.mp3'
          puts title+' added to playlist'
        }
      end
    rescue
      puts 'Error fetching search page'
    end
  end
end

One thought on “Updating my youtube music player

  1. Pingback: Playing youtube music from the command line | Products of Mike's Mind

Leave a Reply