Skip to content

Instantly share code, notes, and snippets.

@thcyron
Last active November 28, 2015 18:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thcyron/6e025f8bf92f43ad49b9 to your computer and use it in GitHub Desktop.
Save thcyron/6e025f8bf92f43ad49b9 to your computer and use it in GitHub Desktop.
Ruby script to generate .ics files for the Staatstheater Nürnberg schedule
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