fonecal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/f1cal.ical +860 -0
- data/fonecal.gemspec +30 -0
- data/lib/fonecal/calendar.rb +40 -0
- data/lib/fonecal/calendar_crawler.rb +12 -0
- data/lib/fonecal/calendar_spec.rb +5 -0
- data/lib/fonecal/circuit_info.rb +23 -0
- data/lib/fonecal/crawler.rb +13 -0
- data/lib/fonecal/event.rb +25 -0
- data/lib/fonecal/event_crawler.rb +60 -0
- data/lib/fonecal/fonecal_spec.rb +7 -0
- data/lib/fonecal/grand_prix.rb +70 -0
- data/lib/fonecal/practice.rb +4 -0
- data/lib/fonecal/qualifying.rb +4 -0
- data/lib/fonecal/race.rb +4 -0
- data/lib/fonecal/session.rb +10 -0
- data/lib/fonecal/util.rb +26 -0
- data/lib/fonecal/version.rb +3 -0
- data/lib/fonecal.rb +25 -0
- data/spec/calendar_crawler_spec.rb +19 -0
- data/spec/event_crawler_spec.rb +68 -0
- data/spec/event_spec.rb +4 -0
- data/spec/fonecal_spec.rb +9 -0
- data/spec/grand_prix_spec.rb +31 -0
- data/spec/race_spec.rb +6 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/util_test.rb +10 -0
- metadata +197 -0
data/fonecal.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fonecal/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fonecal"
|
8
|
+
spec.version = Fonecal::VERSION
|
9
|
+
spec.authors = ["Martin Madsen"]
|
10
|
+
spec.email = ["martin2madsen@gmail.com"]
|
11
|
+
spec.description = %q{f1cal generates an iCalendar file for the 2014 Formula 1 season.}
|
12
|
+
spec.summary = %q{Crawls the Grand Prix list at formula1.com and builds an iCalendar file with events in UTC}
|
13
|
+
spec.homepage = "https://github.com/Fapper/f1cal"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.1"
|
23
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
24
|
+
|
25
|
+
spec.add_dependency "timezone", "~> 0.3"
|
26
|
+
spec.add_dependency "activesupport", "~> 4.0"
|
27
|
+
spec.add_dependency 'nokogiri', "~> 1.6"
|
28
|
+
spec.add_dependency 'geocoder', "~> 1.1"
|
29
|
+
spec.add_dependency 'i18n', "~> 0.6"
|
30
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Fonecal
|
2
|
+
class Calendar
|
3
|
+
attr_accessor :calendar
|
4
|
+
|
5
|
+
def initialize(gps)
|
6
|
+
self.calendar = File.new('f1cal.ical', 'w')
|
7
|
+
|
8
|
+
@calendar.puts "BEGIN:VCALENDAR"
|
9
|
+
@calendar.puts "VERSION:2.0"
|
10
|
+
@calendar.puts "PRODID:-//fonecal//NONSGML v1.0//EN"
|
11
|
+
@calendar.puts ""
|
12
|
+
|
13
|
+
gps.each do |gp|
|
14
|
+
gp.events.each do |event|
|
15
|
+
addEvent(gp, event)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
@calendar.puts "END:VCALENDAR"
|
20
|
+
@calendar.close
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def addEvent(gp, event)
|
26
|
+
@calendar.puts "BEGIN:VEVENT"
|
27
|
+
|
28
|
+
@calendar.puts "SUMMARY:#{gp.grandPrix}: #{event.name}"
|
29
|
+
@calendar.puts "DESCRIPTION:#{gp.raceTitle}: #{event.name}"
|
30
|
+
@calendar.puts "DTSTAMP:#{Util.dtToUTC(DateTime.now)}Z"
|
31
|
+
@calendar.puts "LOCATION:#{gp.city}, #{gp.country}"
|
32
|
+
@calendar.puts "DTSTART:#{Util.dtToUTC(event.start)}Z"
|
33
|
+
puts "#{gp.grandPrix}: #{event.name} starts #{event.start} UTC: #{event.start.utc}"
|
34
|
+
@calendar.puts "DTEND:#{Util.dtToUTC(event.end)}Z"
|
35
|
+
|
36
|
+
@calendar.puts "END:VEVENT"
|
37
|
+
@calendar.puts ""
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Fonecal
|
2
|
+
class CircuitInfo
|
3
|
+
def initialize(info)
|
4
|
+
@info = info
|
5
|
+
end
|
6
|
+
|
7
|
+
def name
|
8
|
+
if @info['Circuit Name:']
|
9
|
+
@info['Circuit Name:'].cap
|
10
|
+
else
|
11
|
+
""
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def city
|
16
|
+
@info['City:'].cap
|
17
|
+
end
|
18
|
+
|
19
|
+
def laps
|
20
|
+
@info['Number of Laps:'].to_i
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
|
3
|
+
module Fonecal
|
4
|
+
class Event
|
5
|
+
attr_accessor :name, :start, :end
|
6
|
+
|
7
|
+
def initialize(event, timezone)
|
8
|
+
# UTC offset in hours, explicitely add +
|
9
|
+
if (timezone.utc_offset / 60 / 60) > 0
|
10
|
+
offset = "+#{timezone.utc_offset / 60 / 60}"
|
11
|
+
else
|
12
|
+
offset = offset.to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
self.name = event[:type]
|
16
|
+
self.start = DateTime.parse("#{event[:date]}T#{event[:start]} #{offset}")
|
17
|
+
|
18
|
+
if event[:end]
|
19
|
+
self.end = DateTime.parse("#{event[:date]}T#{event[:end]} #{offset}")
|
20
|
+
else
|
21
|
+
self.end = self.start + 4.hours
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Fonecal
|
2
|
+
class EventCrawler < Crawler
|
3
|
+
def gp
|
4
|
+
@site.css('div.raceResultsHeading').text.strip
|
5
|
+
end
|
6
|
+
|
7
|
+
def circuitInfo
|
8
|
+
ci = {}
|
9
|
+
|
10
|
+
@site.css('div.circuitInfoBox table tr').each do |row|
|
11
|
+
ci[row.css('td')[0].content.strip] = row.css('td')[1].content.strip
|
12
|
+
end
|
13
|
+
|
14
|
+
city = @site.css('h3.inDetailCircuitName').text
|
15
|
+
|
16
|
+
ci['City:'] = city
|
17
|
+
|
18
|
+
return ci
|
19
|
+
end
|
20
|
+
|
21
|
+
def timeTables
|
22
|
+
tables = []
|
23
|
+
@site.css('div#ctl00_ContentSub_Timetable1_subModuleContentDiv table').each do |tt|
|
24
|
+
tables << parseTimeTable(tt)
|
25
|
+
end
|
26
|
+
|
27
|
+
tables
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def sessionData(row)
|
33
|
+
inf = {}
|
34
|
+
|
35
|
+
sessData = row.css('td')
|
36
|
+
inf[:type] = sessData[0].text.strip
|
37
|
+
inf[:start] = sessData[1].css('span')[0].text.strip
|
38
|
+
inf[:end] = sessData[1].css('span')[1].text.strip if sessData[1].css('span')[1]
|
39
|
+
inf
|
40
|
+
end
|
41
|
+
|
42
|
+
def parseTimeTable(table)
|
43
|
+
info = {}
|
44
|
+
|
45
|
+
rows = table.css 'tr'
|
46
|
+
|
47
|
+
# The date is the first row in teh table
|
48
|
+
info[:date] = rows[0].text.strip
|
49
|
+
|
50
|
+
# Now extract info from each of the remaining rows
|
51
|
+
# Example: Practice 1 12:30 - 14:00 is stored in a row
|
52
|
+
info[:sessions] ||= []
|
53
|
+
rows[1..-1].each do |row|
|
54
|
+
info[:sessions] << sessionData(row)
|
55
|
+
end
|
56
|
+
|
57
|
+
info
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'geocoder'
|
2
|
+
require 'timezone'
|
3
|
+
|
4
|
+
module Fonecal
|
5
|
+
class GrandPrix
|
6
|
+
attr_accessor :events, :circuit
|
7
|
+
|
8
|
+
def initialize(eventWebsite)
|
9
|
+
@crawler = EventCrawler.new(eventWebsite)
|
10
|
+
self.circuit= CircuitInfo.new(@crawler.circuitInfo)
|
11
|
+
self.events = []
|
12
|
+
|
13
|
+
createEvents
|
14
|
+
end
|
15
|
+
|
16
|
+
def location
|
17
|
+
if @res ||= Geocoder.search("#{@circuit.name}, #{@circuit.city}").first
|
18
|
+
@res
|
19
|
+
elsif @res ||= Geocoder.search("#{@circuit.city}").first
|
20
|
+
@res
|
21
|
+
elsif res ||= Geocoder.search("#{@circuit.name}").first
|
22
|
+
@res
|
23
|
+
else
|
24
|
+
@res
|
25
|
+
end
|
26
|
+
|
27
|
+
#@location ||= Geocoder.search("#{@circuit.name}, #{@circuit.city}").first
|
28
|
+
end
|
29
|
+
|
30
|
+
def city
|
31
|
+
@circuit.city
|
32
|
+
end
|
33
|
+
|
34
|
+
def country
|
35
|
+
location.country
|
36
|
+
end
|
37
|
+
|
38
|
+
def timezone
|
39
|
+
@timezone ||= Timezone::Zone.new :latlon => location.geometry["location"].values
|
40
|
+
end
|
41
|
+
|
42
|
+
def raceTitle
|
43
|
+
@crawler.gp.gsub(/\w+/).map(&:capitalize).join(' ')
|
44
|
+
end
|
45
|
+
|
46
|
+
def grandPrix
|
47
|
+
"#{country} GP"
|
48
|
+
# Belgian Grand Prix
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def createEvents
|
54
|
+
@crawler.timeTables.each do |day|
|
55
|
+
day[:sessions].each do |event|
|
56
|
+
type = event[:type]
|
57
|
+
event[:date] = day[:date]
|
58
|
+
|
59
|
+
if(type.include? "Practice")
|
60
|
+
@events << Practice.new(event, timezone)
|
61
|
+
elsif(type.include? "Qualifying")
|
62
|
+
@events << Qualifying.new(event, timezone)
|
63
|
+
elsif(type.include? "Race")
|
64
|
+
@events << Race.new(event, timezone)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/fonecal/race.rb
ADDED
data/lib/fonecal/util.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'timezone'
|
2
|
+
require 'active_support/all'
|
3
|
+
|
4
|
+
module Fonecal
|
5
|
+
class Util
|
6
|
+
def self.getRootUrl(site)
|
7
|
+
site.match('.+\.com').to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.dtToUTC(dt)
|
11
|
+
#dt.with_offset(0).strftime("%Y%m%dT%H%M%S")
|
12
|
+
dt.utc.strftime("%Y%m%dT%H%M%S")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
class String
|
19
|
+
def cap
|
20
|
+
self.gsub(/\w+/).map(&:capitalize).join(' ')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
Timezone::Configure.begin do |c|
|
25
|
+
c.username = 'fonecal'
|
26
|
+
end
|
data/lib/fonecal.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'fonecal/version'
|
2
|
+
require 'fonecal/util'
|
3
|
+
require 'fonecal/crawler'
|
4
|
+
require 'fonecal/event_crawler'
|
5
|
+
require 'fonecal/calendar_crawler'
|
6
|
+
require 'fonecal/calendar'
|
7
|
+
require 'fonecal/grand_prix'
|
8
|
+
require 'fonecal/event'
|
9
|
+
require 'fonecal/session'
|
10
|
+
require 'fonecal/practice'
|
11
|
+
require 'fonecal/qualifying'
|
12
|
+
require 'fonecal/race'
|
13
|
+
require 'fonecal/circuit_info'
|
14
|
+
|
15
|
+
module Fonecal
|
16
|
+
def self.create_ical
|
17
|
+
gps = []
|
18
|
+
|
19
|
+
Fonecal::CalendarCrawler.new("http://www.formula1.com/races/calendar.html").eventLinks.each do |l|
|
20
|
+
gps << Fonecal::GrandPrix.new(l)
|
21
|
+
end
|
22
|
+
|
23
|
+
Fonecal::Calendar.new(gps)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'fonecal'
|
2
|
+
|
3
|
+
describe Fonecal::CalendarCrawler do
|
4
|
+
let(:crawler) { Fonecal::CalendarCrawler.new("http://www.formula1.com/races/calendar.html") }
|
5
|
+
|
6
|
+
it "successfully connects to the website" do
|
7
|
+
crawler.site.should_not be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#eventLinks" do
|
11
|
+
it "returns 19 grand prix links" do
|
12
|
+
crawler.eventLinks.count.should eql(19)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has the full url" do
|
16
|
+
crawler.eventLinks.first.should eql('http://www.formula1.com/races/in_detail/australia_914/circuit_diagram.html')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'fonecal'
|
2
|
+
|
3
|
+
describe Fonecal::EventCrawler do
|
4
|
+
let(:crawler) { Fonecal::EventCrawler.new("http://www.formula1.com/races/in_detail/australia_914/circuit_diagram.html") }
|
5
|
+
|
6
|
+
it "successfully connects to the website" do
|
7
|
+
crawler.site.should_not be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "2014 Australian GP" do
|
11
|
+
describe "#circuitInfo" do
|
12
|
+
it "fetches a circuit name" do
|
13
|
+
crawler.circuitInfo['Circuit Name:'].should eq 'Albert Park'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "fetches a date" do
|
17
|
+
crawler.circuitInfo['Race Date:'].should_not be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "fetches a city" do
|
21
|
+
crawler.circuitInfo['City:'].should eq 'Melbourne'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "#gp" do
|
26
|
+
it "should fetch the GP name" do
|
27
|
+
crawler.gp.should eq '2014 FORMULA 1 ROLEX AUSTRALIAN GRAND PRIX'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#timeTables" do
|
32
|
+
it "should get three timetables" do
|
33
|
+
crawler.timeTables.count.should eq 3
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have data about practice 1" do
|
37
|
+
crawler.timeTables.first[:date].should eq 'Fri 14 March 2014'
|
38
|
+
crawler.timeTables.first[:sessions].first[:type].should eq 'Practice 1'
|
39
|
+
crawler.timeTables.first[:sessions].first[:start].should eq '12:30'
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should have data about practice 2" do
|
43
|
+
crawler.timeTables.first[:date].should eq 'Fri 14 March 2014'
|
44
|
+
crawler.timeTables.first[:sessions].last[:type].should eq 'Practice 2'
|
45
|
+
crawler.timeTables.first[:sessions].last[:start].should eq '16:30'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should have data about practice 3" do
|
49
|
+
crawler.timeTables[1][:date].should eq 'Sat 15 March 2014'
|
50
|
+
crawler.timeTables[1][:sessions].first[:type].should eq 'Practice 3'
|
51
|
+
crawler.timeTables[1][:sessions].first[:start].should eq '14:00'
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should have data about qualifying" do
|
55
|
+
crawler.timeTables[1][:date].should eq 'Sat 15 March 2014'
|
56
|
+
crawler.timeTables[1][:sessions].last[:type].should eq 'Qualifying'
|
57
|
+
crawler.timeTables[1][:sessions].last[:start].should eq '17:00'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should have data about the race" do
|
61
|
+
crawler.timeTables.last[:date].should eq 'Sun 16 March 2014'
|
62
|
+
crawler.timeTables.last[:sessions].first[:type].should eq 'Race'
|
63
|
+
crawler.timeTables.last[:sessions].first[:start].should eq '17:00'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
data/spec/event_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'fonecal'
|
2
|
+
|
3
|
+
describe Fonecal::GrandPrix do
|
4
|
+
describe "Getting Belgian GP info" do
|
5
|
+
let(:belgianGP) { Fonecal::GrandPrix.new("http://www.formula1.com/races/in_detail/belgium_927/circuit_diagram.html") }
|
6
|
+
|
7
|
+
it "should have the correct race title" do
|
8
|
+
belgianGP.raceTitle.should eql "2014 Formula 1 Shell Belgian Grand Prix"
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should get a country" do
|
12
|
+
belgianGP.country.should eql "Belgium"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has location object" do
|
16
|
+
belgianGP.location.should_not be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "its events" do
|
20
|
+
describe "Practice 1" do
|
21
|
+
it "should have the right name" do
|
22
|
+
belgianGP.events.first.name.should eql "Practice 1"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should have the correct start time" do
|
26
|
+
belgianGP.events.first.start.should eql DateTime.new(2014, 8, 22, 10, 0, 0, '+1')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/spec/race_spec.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
9
|
+
config.run_all_when_everything_filtered = true
|
10
|
+
config.filter_run :focus
|
11
|
+
|
12
|
+
# Run specs in random order to surface order dependencies. If you find an
|
13
|
+
# order dependency and want to debug it, you can fix the order by providing
|
14
|
+
# the seed, which is printed after each run.
|
15
|
+
# --seed 1234
|
16
|
+
config.order = 'random'
|
17
|
+
end
|
data/spec/util_test.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'fonecal'
|
2
|
+
|
3
|
+
describe Fonecal::Util do
|
4
|
+
describe "#getRootURL" do
|
5
|
+
it "extracts formula1.com from any url" do
|
6
|
+
calSite = "http://www.formula1.com/races/calendar.html"
|
7
|
+
Fonecal::Util.getRootUrl(calSite).should eql('http://www.formula1.com')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|