Last active
November 28, 2015 18:13
-
-
Save thcyron/6e025f8bf92f43ad49b9 to your computer and use it in GitHub Desktop.
Ruby script to generate .ics files for the Staatstheater Nürnberg schedule
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "net/http" | |
require "nokogiri" | |
require "date" | |
require "json" | |
require "digest/sha1" | |
class Schedule | |
attr_reader :genre | |
def initialize | |
@genre = :all | |
end | |
def genre=(genre) | |
if GENRE_KEYS.keys.include?(genre) | |
@genre = genre | |
else | |
raise ArgumentError, "unknown genre `#{genre}'" | |
end | |
end | |
def months | |
resp = get("/index.php?page=spielplan") | |
doc = Nokogiri::HTML(resp.body) | |
doc.css("select[name='filter[month]'] option").map { |node| node["value"].strip } | |
end | |
def events | |
[].tap do |entries| | |
hashes = [] | |
months.map do |month| | |
entries_for_month(month).each do |entry| | |
unless hashes.include?(entry.fetch(:hash)) | |
hashes << entry.fetch(:hash) | |
entries << entry | |
end | |
end | |
end | |
end | |
end | |
private | |
def entries_for_month(month) | |
resp = post("/index.php?page=spielplan", "filter[day]=1&filter[month]=#{month}&filter[genre]=#{genre_key}&filter[production]=0") | |
resp = get("/index.php?page=spielplan") | |
doc = Nokogiri::HTML(resp.body) | |
doc.css(".entry[title]").map { |entry| parse_entry(entry) } | |
end | |
GENRE_KEYS = { | |
all: "0", | |
opera: "3,11,17,13,9,21", | |
ballet: "5", | |
concert: "6,12,16,22,47", | |
play: "4", | |
guest: "18,15,54,60", | |
} | |
def genre_key | |
GENRE_KEYS.fetch(@genre) | |
end | |
DEFAULT_DURATION = 2 * 60 * 60 | |
def parse_entry(entry) | |
{}.tap do |hash| | |
if entry.attr("title") =~ /\A(\d{1,2})\.(\d{1,2})\.(\d{4}) (\d{1,2}):(\d{2})/ | |
year, month, day, hour, minute = $3.to_i, $2.to_i, $1.to_i, $4.to_i, $5.to_i | |
else | |
raise "could not parse date (#{entry.attr("title").inspect})" | |
end | |
find_first(entry, ".time") do |text| | |
case text.strip | |
when /^(\d{1,2}):(\d{2}) Uhr, (.*)$/ | |
hash[:start] = Time.mktime(year, month, day, $1.to_i, $2.to_i) | |
hash[:end] = hash[:start] + DEFAULT_DURATION | |
hash[:location] = $3.strip | |
when /^(\d{1,2}):(\d{2}) - (\d{1,2}):(\d{2}) Uhr, (.*)$/ | |
hash[:start] = Time.mktime(year, month, day, $1.to_i, $2.to_i) | |
hash[:end] = Time.mktime(year, month, day, $3.to_i, $4.to_i) | |
hash[:location] = $5.strip | |
end | |
end | |
hash[:start] ||= Time.mktime(year, month, day, hour, minute) | |
hash[:end] ||= hash[:start] + DEFAULT_DURATION | |
find_first(entry, ".title") do |text, node| | |
if node["onclick"] | |
matches = node["onclick"].match(/document.location.href='(.*)';/) | |
hash[:url] = "http://www.staatstheater-nuernberg.de/#{matches[1]}" | |
end | |
hash[:title] = text.strip | |
end | |
hash[:hash] = hash_entry(hash) | |
end | |
end | |
def hash_entry(entry) | |
Digest::SHA1.hexdigest(entry.to_json) | |
end | |
def find_first(node, selector) | |
nodes = node.css(selector) | |
if nodes.any? | |
node = nodes.first | |
yield node.text, node | |
else | |
raise "no match" | |
end | |
end | |
def http | |
@http ||= Net::HTTP.new("www.staatstheater-nuernberg.de") | |
end | |
def get(path, initheader = {}, dest = nil) | |
initheader["Cookie"] ||= @cookie if @cookie | |
resp = http.get(path, initheader, dest) | |
@cookie = resp["Set-Cookie"] if resp.key?("Set-Cookie") | |
resp | |
end | |
def post(path, data, initheader = {}, dest = nil) | |
initheader["Cookie"] ||= @cookie if @cookie | |
resp = http.post(path, data, initheader, dest) | |
@cookie = resp["Set-Cookie"] if resp.key?("Set-Cookie") | |
resp | |
end | |
end | |
class Calendar | |
def self.calendar_for_events(events) | |
"".tap do |cal| | |
cal << "BEGIN:VCALENDAR\n" | |
cal << "VERSION:2.0\n" | |
cal << "PRODID:https://thcyron.de/staatstheater-nuernberg-ical/\n" | |
events.each do |event| | |
cal << "BEGIN:VEVENT\n" | |
cal << "UID:staatstheater-nuernberg-ical+#{event.fetch(:hash)}@thcyron.de\n" | |
if event.has_key?(:location) | |
cal << "LOCATION:#{escape_newlines(event.fetch(:location))}\n" | |
end | |
cal << "SUMMARY:#{escape_newlines(event.fetch(:title))}\n" | |
cal << "DTSTART:#{event.fetch(:start).utc.strftime("%Y%m%dT%H%M%SZ")}\n" | |
cal << "DTEND:#{event.fetch(:end).utc.strftime("%Y%m%dT%H%M%SZ")}\n" | |
cal << "URL:#{event.fetch(:url)}\n" if event.has_key?(:url) | |
cal << "END:VEVENT\n" | |
end | |
cal << "END:VCALENDAR\n" | |
end | |
end | |
def self.escape_newlines(s) | |
s.gsub("\n", "\\n").gsub("\r", "") | |
end | |
end | |
if $0 == __FILE__ | |
ENV["TZ"] = "Europe/Berlin" | |
sched = Schedule.new | |
sched.genre = ARGV.first.to_sym if ARGV.size > 0 | |
puts Calendar.calendar_for_events(sched.events) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment