I have been learning Ruby by reading books and tutorials, and practising some problems on Codewars to level-up my new skills, when I had an idea for a little Ruby code project. It will store programmes you like watching on the BBC iPlayer website, and list upcoming episodes, using an API (link is in 2nd line of code). For those unaware with iPlayer, it lets you watch most BBC TV programs, but only for a set time period after they were shown on TV. Sometimes I get behind in watching shows so in this example it will list episodes in order of soonest-to-expire, so I know what I need to watch first.
I decided to create a class which is an extension of a Hash, thinking that people can then do normal operations on it like they would a hash. I remain unsure if this is a good idea. Then there's a simple CLI to access it. I've written it to be extendable.
Two extensions I had in mind were:
- A GUI could use this class as-is
- A list of programs could be stored in an external file and loaded when the program is run.
So, let me know what you think. The main thing I thought of is error-trapping. It does a degree of assuming that the API will do what you expect it to.
class Iplayer_data < Array
#for iPlayer API documentation, see http://mermade.github.io/swagger/index.html?url=https://raw.githubusercontent.com/Mermade/bbcparse/master/iblApi/swagger.json#!/Search/Search
require 'httpclient' #I had difficulty installing a cUrl gem on windows, so I used this instead
require 'JSON'
attr_reader :categories, :regions
def initialize
@http = HTTPClient.new
@lang = "en"
@rights = "web"
@version = "v1"
# load categories and regions straight away, for future use, and so we have a local copy of the data
@categories = load_standing_data("categories")
@regions = load_standing_data("regions")
end
def i_search(query)
url = "http://ibl.api.bbci.co.uk/ibl/#{@version}/search?q="
url += URI.escape(query)
url += "&lang=#{@en}&rights=#{@rights}&availability=available"
get_http_content(url)["search"]["results"].each do |c|
#don't bring back all the attributes. Basically just a test to see if I could do it
#plus it's less data to display, which helps with testing.
#there's no real good programmatic reason to limit what we bring back
c.select! { |k,v| ["id", "title", "synopses"].include?(k) }
end
end
def add_programme(name)
if self.select {|x| x["title"].downcase==name.downcase}.count > 0
return "Programme {name} is already in your list of programmes"
end
s = i_search(name)
return "No program found with a name like that" if s.count==0
if s[0]["title"].downcase == name.downcase
self << s[0]; "Successfully added #{s[0]['title']}"
else
"Could not find a program of the name #{name}. Did you mean #{s[0]['title']}?"
end
end
def delete_programme(name)
if self.select {|x| x["title"].downcase==name.downcase}.count == 0
return "Programme #{name} is not in your list of programmes"
end
self.delete_if {|x| x["title"].downcase==name.downcase}
"#{name} has been deleted"
end
def get_episodes
episodes = []
self.each do |programme|
pid = programme["id"]
url = "http://ibl.api.bbci.co.uk/ibl/#{@version}/programmes/#{pid}/episodes?rights=#{@rights}"
url += "&availability=available&initial_child_count=2&per_page=200"
episodes += get_http_content(url)["programme_episodes"]["elements"]
end
episodes
end
def get_http_content(url);JSON.parse(@http.get(url).content);end
private
#categories and regions have pretty much the same API so we can load them both through 1 method
def load_standing_data(name)
raw = get_http_content("http://ibl.api.bbci.co.uk/ibl/#{@version}/#{name}?lang=#{@lang}")
# from an array of hashes, return a hash of hashes, with the id field as the key
output = Hash.new
raw["#{name}"].each { |x| output[x["id"]] = x }
output
end
end
##################### END OF CLASS #####################
###### NOW CREATE A SIMPLE COMMAND-LINE INTERFACE ######
def display_help
puts 'Iplayer_data helps you keep track of your favourite programs.'
puts 'It can store the programs you want to follow, then display information regarding episodes of those programs.'
puts 'You can (a)dd, (d)elete or (v)iew your favourite programs...'
puts 'Display (e)pisodes, (c)ategories, or (r)egions, or e(x)it.'
puts 'Some instructions require extra input - just put a space after the first letter than type it in.'
puts 'eg to add Top Gear, input "a top gear" (case insensitive)'
#(d)elete, (p)rogrammes
end
require 'pp' # pretty print
ip = Iplayer_data.new
re=''
until re[0] == 'x' do
print '>'
re = gets.chomp
case re[0]
when '?' then display_help
when 'a' then puts ip.add_programme(re[2..-1])
when 'd' then puts ip.delete_programme(re[2..-1])
when 'v' then pp ip
when 'c' then pp ip.categories
when 'r' then pp ip.regions
when 'e' then
#list of episodes, ordered by soonest to latest to expire
ep = ip.get_episodes.sort_by { |episode| episode["versions"][0]["availability"]["end"]}
ep.each do |e|
e["time_left"] = e["versions"][0]["availability"]["remaining"]["text"]
puts e.select {|k,v| ["id", "title", "subtitle", "time_left"].include?(k) }
end
puts "No episodes found" if ep.count==0
when 'x' then puts "Exiting..."
else puts "Sorry, I did not recognize that input. Type \"?\" for help."
end
end