#!/usr/bin/perl -w # Copyright © 2000-2010 Jamie Zawinski . # # Permission to use, copy, modify, distribute, and sell this software and its # documentation for any purpose is hereby granted without fee, provided that # the above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation. No representations are made about the suitability of this # software for any purpose. It is provided "as is" without express or # implied warranty. # # Generates DNA Lounge calendar pages. # depends on "utils/menuify.pl", "cmp". # Created: 29-Nov-2000. ############################################################################ # # Syntax of the calendar.txt file: # # DOTM-MONTH-YEAR # DESCRIPTION # # Every DOTW from START-DATE to END-DATE # DESCRIPTION # # Every NTH DOTW from START-DATE to END-DATE # DESCRIPTION # e.g., "every first,third friday" of each month. # "first", "second", and "third" mean the obvious thing. # "fourth" is always the 4th in the month, even if there are 5. # "last" is always the last one in the month (4th or 5th.) # "fifth" is the second-to-last one, in months that have # 5 of them (it is the "extra friday".) # # Holiday DOTM-MONTH[-YEAR] DESCRIPTION # # Holidays whose text begins with an asterisk are assumed to be federal # holidays, meaning if it falls on a sunday or monday, we note Sunday # events as being on a 3-day weekend. # # If there is an entry for a specific date, it overrides any "Every" # entries that might also apply. If multiple "every" entries apply # to a given date, the later one in the file wins. # # Descriptions are HTML, and can be multiple lines. # Each line of a description must be indented at least one space. # # The left column (title/genre/times/price) and right column (performers) # are separated by a blank line. # # The HTML text within a column does not wrap: use
and

liberally. # # There are various custom HTML-like tags that are used in the HTML that # are converted to "real" HTML when the actual files are generated, like # to indicate DJ names. # # Holiday descriptions show up over in the "Date" box. (Really this ought # to be done more automatically, but I don't know of a command-line program # that will compute those dates for me.) # # # Awards EVENT-NAME # HTML # # If an event of that name exists, the "awards" HTML will be displayed # in the left column. # # # Groups EVENT-NAME # HTML # # If an event of that name exists, the "groups" HTML will be appended # to it's blurb (e.g., "join the Facebook group"). # # # More precisely: # # file := [ entry ]* # entry := static_event | recurring_event | holiday | # awards | groups # # static_event := day_of_month_line event_data # day_of_month_line := dotm month year '(' dotw ')' '\n' event_data # event_data := [ hspace text '\n' ]* # # recurring_event := recurrence_line [ recurring_event_data ]* range # recurrence_line := 'every' [ repeat_words ]? day_words '\n' # day_words := day_word [ ',' day_word ]* # repeat_words := nth_of_month_words | alternating_word # nth_of_month_words := nth_of_month_word [ ',' nth_of_month_word ]* # nth_of_month_word := 'first' | 'second' | 'third' | 'fourth' | # 'fifth' | 'last' # alternating_word := 'other' # (note that there is no way to express # "every 3 weeks"; one can only express # "every 3rd week of the month".) # range := 'from' range_date [ 'to' range_date ]? # range_date := dotm '-' month '-' year # recurring_event_data := hspace text '\n' # # awards := 'awards' event_name awards_data # awards_data := [ hspace text '\n' ]* # # groups := 'groups' event_name groups_data # groups_data := [ hspace text '\n' ]* # # static_event := day_of_month_line event_data # # month := 'january' | 'february' | ... | 'december' | # 'jan' | 'feb' | ... | 'dec' # dotm := '1' | '2' | ... | '31' # year := '2000' | '2001' | ... # hspace := [ ' ' | '\t' ]+ # # # Examples: # # 2 January 2001 (tue) # blah blah blah # 18 January 2001 (thu) # All work and no play makes Jack a dull boy. # All work and no play makes Jack a dull boy. # # 27 feb 2001 (tue) # something else # # Every tuesday from 3-Jan-2001 # blah blah blah # # Every other friday from 3-Jan-2001 -- NOT YET IMPLEMENTED # blah blah blah # # Every first,third wednesday from 3-Jan-2001 # blah blah blah # blah blah blah # # Every fifth saturday from 3-Jan-2001 to 3-March-2001 # blah blah blah # # Holiday 02-Feb Groundhog Day # # Awards BOOTIE # Blah Blah

# Blah Blah

# # Groups BOOTIE # Join the Bootie group on Facebook. # # # Note that, in a month that has 5 Saturdays, they are numbered "first", # "second", "third", "fourth/fifth", "last", because the 5th, "extra" # Saturday goes before the "last" saturday. I think this is a pretty # stupid way to go about it, but I'm told that's how it's done. # # Specifying the day of the week on the line for static entries isn't # strictly necessary, but it functions as a checksum (we can error if # the dotm and dotw don't match.) # ############################################################################ require 5; use diagnostics; use strict; use bytes; use Text::Wrap; # Utter foulness! Without this, [:upper:] doesn't work on Latin1 characters. use locale; use POSIX qw(locale_h mktime strftime); setlocale(LC_ALL, "en_US"); ############################################################################## # # Configuration and stuff # ############################################################################## my $progname = $0; $progname =~ s@.*/@@g; my $version = q{ $Revision: 1.605 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/; my $exec_dir = $0; $exec_dir =~ s@/[^/]*$@@; my $exec_dir2 = "$exec_dir/../utils/"; my $data_dir = $exec_dir; my $verbose = 1; my $debug_p = 0; # this means "don't alter any files, print diffs instead" my $calendar_data_file = "$data_dir/calendar.txt"; my $calendar_upcoming_file = "upcoming.html"; my $calendar_overview_file = "overview.html"; my $calendar_rss_file = "dnalounge.rss"; my $calendar_ical_file = "dnalounge.ics"; my $calendar_crontab_file = "crontab.txt"; my $calendar_weekly_file = "weekly.txt"; my $calendar_weekly_prolog = "prolog.txt"; my $calendar_topten_file = "topten.txt"; my $calendar_infoline_file = "infoline.txt"; my $calendar_ledger_file = "ledger.html"; my $calendar_names_file = "names.txt"; my $calendar_eighties_file = "1985-1999.txt"; my $calendar_tickets_file = "../tickets/index.html"; my $store_file = "../store/index.html"; my $suspension_file = "suspension.html"; my $template_file = "$exec_dir2/template.html"; my @menuify_cmd = ("$exec_dir2/menuify.pl", $template_file); my $calendar_date_block_bg = "#004400"; # for the box with the date in it my $calendar_grid_dim_fg = "#666"; # de-emphasized dotms in cal overview my $fill_column = 70; # no plain-text lines longer than this # For the calendar html pages. my $page_title = "DNA Lounge Calendar"; my $url_base = "http://www.dnalounge.com/"; my $ticket_form_url = "https://cart.dnalounge.com/order/"; my $ticket_url_base = "$ticket_form_url?item="; my $vip_price = '$295'; my $age_text = '+ with ID.'; my $age_aa_text = 'all ages.'; my $xml_link_tag = " \n"; my $tickets_vip_blurb = ("Having a birthday party?\n" . "VIP table service is" . " available every night!\n" . "

\n"); my $eighties_top_blurb = ("The list of bands who have performed at DNA Lounge since we re-opened\n" . "in 2001 is well documented on our\n" . "calendar. However, DNA Lounge has existed since\n" . "1985, and I've been making an effort to document the bands that played\n" . "here before I was running things. This is the list I've been able\n" . "to construct so far.\n" . "

If you have any updates or corrections, please let\n" . "me know!\n" ); my $rss_channel_title = "DNA Lounge"; my $rss_channel_url = $url_base; my $rss_channel_desc = "Upcoming events at DNA Lounge, San Francisco."; my $rss_channel_desc2 = ("Upcoming events at DNA Lounge:\n" . "375 Eleventh Street, San Francisco.\n"); my $rss_channel_loc = "DNA Lounge: 375 Eleventh Street, San Francisco."; my $rss_latlong = "37.771007;-122.412694"; my $rss_channel_lang = "en"; my $rss_channel_tz = "US/Pacific"; my $rss_logo_url = "${url_base}logo2.gif"; my $rss_logo_width = 100; my $rss_logo_height = 33; my $rss_max_links = 200; my $overview_max_links = 10; ############################################################################## # # Data structures and stuff # ############################################################################## my %calendar = (); # keys are "YYYY-MM-DD", values are references to lists # (since there can be either one or two events per day). # elements of the lists are hashes with these keys: # # date "dotw, dd-mmm-yy" # times (yyyy mm dd dotw # start_min end_min webcast_end_min) # event_ord overall event number, from start # day_ord event_number of_n_events_today # title short title, without "presents" # repeat "Every first Monday" # holiday "Halloween" # vacationp 1 if tomorrow is a day off # flyer "flyer-url" # photos "photos-url" # age "21", "18", or "AA" # tickets ("url" "desc" onsale offsale vip-p) # event_ids ("facebook-url", "myspace-url", ...) # webcast "main", "lounge", "off" # video "youtube-url" # videoname "title caption for video" # html_src "raw html from calendar.txt" my @repeaters = (); # elements are lists: # # (dotw, repeat_code, repeat_desc_string, # from_year, from_month, from_dotm, # to_year, to_month, to_dotm, # lineno, event-hash) # dotw: 0-6 # repeat: 0, 1-5, -1, or -666. # the event-hash is as in %calendar my %shared_flyers = (); # keys and values are "YYYY-MM-DD" or "YYYY-MM-DDb". # for when one event re-uses the flyer of another. my %awards = (); # keys are event or promoter names, values are html. my %groups = (); # keys are event or promoter names, values are html. my @future_events; # list of event hashes that lie in the future. my $yesterday_event; # the last event that didn't go on @future_events. my @event_ords; # index of events by overall event number. my %genres; # count of event genres. my %inline_flyers; # maps YYYY-MM-DDb to tags for flyer thumbs. my $total_event_count; # how many events are listed in the calendar. my $total_live_count; # how many of those were live shows. my $event_count_12mo; # how many events in the last 12 months. my $live_count_12mo; # how many of those were live shows. my $event_count_6mo; # how many events in the last 6 months. my $live_count_6mo; # how many of those were live shows. my $one_year_ago; my $six_months_ago; my $stats_cutoff; my $body_template = undef; ############################################################################## # # Date parsing and manipulation utilities # ############################################################################## my @months = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"); my @days = ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"); my @days_per_month = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31); # matches various forms of month names (in lower case.) # my $month_re = ('\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|' . 'may|june?|july?|aug(?:ust)?|sep(?:tember)?|sept|' . 'oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\b'); # matches various forms of day-of-the-week names (in lower case.) # my $days_re = ('\b(?:sun(?:day)?|mon(?:day)?|tue(?:sday)?|tues|' . 'wed(?:nesday)?|thu(?:rsday)?|thurs?|fri(?:day)?|' . 'sat(?:urday)?)\b'); # maps lower cased month names to numbers (1-12). # my %monthvals = ( 'jan' => 1, 'january' => 1, 'february' => 2, 'feb' => 2, 'march' => 3, 'mar' => 3, 'april' => 4, 'apr' => 4, 'may' => 5, 'jun' => 6, 'june' => 6, 'jul' => 7, 'july' => 7, 'august' => 8, 'aug' => 8, 'sep' => 9, 'sept' => 9, 'september' => 9, 'oct' => 10, 'october' => 10, 'nov' => 11, 'november' => 11, 'dec' => 12, 'december' => 12 ); # maps lower cased day names to numbers (0-6, sun-sat). # my %dayvals = ( 'sun' => 0, 'sunday' => 0, 'mon' => 1, 'monday' => 1, 'tue' => 2, 'tues' => 2, 'tuesday' => 2, 'wed' => 3, 'wednesday' => 3, 'thu' => 4, 'thur' => 4, 'thurs' => 4, 'thursday' => 4, 'fri' => 5, 'friday' => 5, 'sat' => 6, 'saturday' => 6 ); # Given a date, returns the day of the week on which it falls (0-6, sun-sat) # sub dotw($$$) { my ($dotm, $month, $year) = @_; # From the sci.math FAQ: # # The following formula, which is for the Gregorian calendar only, may be # more convenient for computer programming. Note that in some programming # languages the remainder operation can yield a negative result if given # a negative operand, so "mod 7" may not translate to a simple remainder. # # W == (k + [2.6m - 0.2] - 2C + Y + [Y/4] + [C/4]) mod 7 # where [] denotes the integer floor function (round down), # k is day (1 to 31) # m is month (1 = March, ..., 10 = December, 11 = Jan, 12 = Feb) # Treat Jan & Feb as months of the preceding year # C is century (1987 has C = 19) # Y is year (1987 has Y = 87 except Y = 86 for Jan & Feb) # W is week day (0 = Sunday, ..., 6 = Saturday) # # Here the century & 400 year corrections are built into the formula. # The [2.6m-0.2] term relates to the repetitive pattern that the 30-day # months show when March is taken as the first month. my $k = $dotm; my $m = 1 + (($month + 9) % 12); # (1-12) => (11, 12, 1-10) my $y = $year - ($m >= 11 ? 1 : 0); # subtract a year for jan/feb my $C = int ($y/100); my $Y = int ($y%100); my $W = (($k + int ((2.6 * $m) - 0.2) - (2 * $C) + $Y + int ($Y / 4) + int ($C / 4) + 7) % 7); return $W; } # Returns info describing a day's position in the month. # For the given date, tells you that it is, e.g., the 2nd wednesday; # and that this month contains 5 wednesdays. # sub dotw_count($$$) { my ($year, $month, $dotm) = @_; my $days = days_per_month ($month, $year); my $dotw = dotw ($dotm, $month, $year); my $index = undef; my $total = 0; my $idotw = dotw (1, $month, $year); for (my $i = 1; $i <= $days; $i++) { if ($idotw == $dotw) { $total++; $index = $total if ($i == $dotm); } $idotw = ($idotw + 1) % 7; } error ("$dotm does not occur in $month-$year?") unless ($index); error ("only $total $days[$dotw]s in $month-$year?") if ($total < 4); return ($index, $total); } # Whether the given 4-digit year is a leap year. # sub leap_year_p($) { my ($year) = @_; return ((($year % 4 == 0) && # divisible by 4 (($year % 100) || # ...unless divisble by 100 ($year % 400 == 0))) # ...and not divisible by 400 ? 1 : 0); } # Returns the number of days in the given month, considering leap years. # sub days_per_month($$) { my ($month, $year) = @_; my $days = $days_per_month[$month-1]; $days++ if ($month == 2 && leap_year_p($year)); # feb leapday return $days; } # Converts a day number to "Saturday", "Saturday Afternoon", etc. # $event_count is how many events there are on this day; # $event_number is which of those events this is. # The "evening" suffix is only used if there are multiple events, # but the "morning" and "afternoon" suffixes are used even if # there is only one event on this day. # sub describe_day($$$$) { my ($dotw, $start_minute, $event_count, $event_number) = @_; my $afternoon_threshold = (12 * 60); # noon my $evening_threshold = (17 * 60); # 5 PM my $day = $days[$dotw]; if ($event_count > 1 || $start_minute < $evening_threshold) { my $suffix = undef; if ($start_minute < $afternoon_threshold) { $suffix = "Morning"; } elsif ($start_minute < $evening_threshold) { $suffix = "Afternoon"; } elsif ($event_number > 0) { # no "Night" suffix if it's the first event (so we don't get two) $suffix = "Night"; } $day .= " $suffix" if (defined ($suffix)); } return $day; } ############################################################################## # # HTML utilities # ############################################################################## my %entity_table = ( "quot" => '"', "amp" => '&', "lt" => '<', "gt" => '>', "nbsp" => ' ', "iexcl" => '¡', "cent" => '¢', "pound" => '£', "curren" => '¤', "yen" => '¥', "brvbar" => '¦', "sect" => '§', "uml" => '¨', "copy" => '©', "ordf" => 'ª', "laquo" => '«', "not" => '¬', "shy" => '­', "reg" => '®', "macr" => '¯', "deg" => '°', "plusmn" => '±', "sup2" => '²', "sup3" => '³', "acute" => '´', "micro" => 'µ', "para" => '¶', "middot" => '·', "cedil" => '¸', "sup1" => '¹', "ordm" => 'º', "raquo" => '»', "frac14" => '¼', "frac12" => '½', "frac34" => '¾', "iquest" => '¿', "Agrave" => 'À', "Aacute" => 'Á', "Acirc" => 'Â', "Atilde" => 'Ã', "Auml" => 'Ä', "Aring" => 'Å', "AElig" => 'Æ', "Ccedil" => 'Ç', "Egrave" => 'È', "Eacute" => 'É', "Ecirc" => 'Ê', "Euml" => 'Ë', "Igrave" => 'Ì', "Iacute" => 'Í', "Icirc" => 'Î', "Iuml" => 'Ï', "ETH" => 'Ð', "Ntilde" => 'Ñ', "Ograve" => 'Ò', "Oacute" => 'Ó', "Ocirc" => 'Ô', "Otilde" => 'Õ', "Ouml" => 'Ö', "times" => '×', "Oslash" => 'Ø', "Ugrave" => 'Ù', "Uacute" => 'Ú', "Ucirc" => 'Û', "Uuml" => 'Ü', "Yacute" => 'Ý', "THORN" => 'Þ', "szlig" => 'ß', "agrave" => 'à', "aacute" => 'á', "acirc" => 'â', "atilde" => 'ã', "auml" => 'ä', "aring" => 'å', "aelig" => 'æ', "ccedil" => 'ç', "egrave" => 'è', "eacute" => 'é', "ecirc" => 'ê', "euml" => 'ë', "igrave" => 'ì', "iacute" => 'í', "icirc" => 'î', "iuml" => 'ï', "eth" => 'ð', "ntilde" => 'ñ', "ograve" => 'ò', "oacute" => 'ó', "ocirc" => 'ô', "otilde" => 'õ', "ouml" => 'ö', "divide" => '÷', "oslash" => 'ø', "ugrave" => 'ù', "uacute" => 'ú', "ucirc" => 'û', "uuml" => 'ü', "yacute" => 'ý', "thorn" => 'þ', "yuml" => 'ÿ', # HTML 4 entities that do not have 1:1 Latin1 mappings. "bull" => "*", "hellip"=> "...", "prime" => "'", "Prime" => "\"", "frasl" => "/", "trade" => "[tm]", "larr" => "<-", "rarr" => "->", "harr" => "<->", "lArr" => "<=", "rArr" => "=>", "hArr" => "<=>", "empty" => "Ø", "minus" => "-", "lowast"=> "*", "sim" => "~", "cong" => "=~", "asymp" => "~", "ne" => "!=", "equiv" => "==", "le" => "<=", "ge" => ">=", "lang" => "<", "rang" => ">", "loz" => "<>", "OElig" => "OE", "oelig" => "oe", "Yuml" => "Y", "circ" => "^", "tilde" => "~", "ensp" => " ", "emsp" => " ", "thinsp"=> " ", "ndash" => "-", "mdash" => "--", "lsquo" => "`", "rsquo" => "'", "sbquo" => "'", "ldquo" => "\"", "rdquo" => "\"", "bdquo" => "\"", "lsaquo"=> "<", "rsaquo"=> ">", ); my %entity_table2; # maps Latin1 back to Entities foreach (keys (%entity_table)) { $entity_table2{$entity_table{$_}} = $_; } my %asciify_table = ( '¡' => '!', '¢' => '[c]', '£' => '#', '¤' => '#', '¥' => 'Y', '¦' => '|', '§' => 'SS', '¨' => '_', '©' => '(c)', 'ª' => '?', '«' => '<<', '¬' => '=', '­' => '-', '®' => '(r)', '¯' => '_', '°' => '[?]','±' => '+/-', '²' => '[2]', '³' => '[3]', '´' => '\'', 'µ' => 'u', '¶' => 'PP', '·' => '.', '¸' => ',', '¹' => '[1]', 'º' => '[o]','»' => '>>', '¼' => ' 1/4 ', '½' => ' 1/2 ', '¾' => ' 3/4 ', '¿' => '?', 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'Æ' => 'AE', 'Ç' => 'C', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ð' => 'D', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', '×' => 'x', 'Ø' => '0', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ý' => 'Y', 'Þ' => 'p', 'ß' => 'S', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'æ' => 'ae', 'ç' => 'c', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ð' => 'o', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', '÷' => '/', 'ø' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'þ' => 'p', 'ÿ' => 'y' ); # Converts any Latin1 characters to HTML entities. # Also converts &, < and > to entities. # sub entitify($) { my ($s) = @_; $s =~ s/([&<>\"\177-\377])/ { my $ent = $entity_table2{$1}; error ("no entity for character: $1\n") unless (defined ($ent)); print STDERR "$progname: $1 -> $ent\n" if ($verbose > 4); "&$ent;"; }/xegs; return $s; } # Convert any HTML entities to Latin1 characters. # sub de_entify($) { my ($text) = @_; $text =~ s/(&([[:alpha:]]+);?)/ { my $c = $entity_table{$2}; print STDERR "$progname: warning: unknown HTML character entity \"$1\"\n" unless $c; ($c ? $c : "[$2]"); } /gexi; return $text; } # Converts any Latin1 characters to the closest ASCII equivalent. # Note that you probably also need to convert & to & # sub asciify($) { my ($s) = @_; $s =~ s/([\177-\377])/ { my $asc = $asciify_table{$1}; error ("no ASCII for character: $1\n") unless (defined ($asc)); print STDERR "$progname: $1 -> $asc\n" if ($verbose > 4); $asc; }/xegs; return $s; } # Convert the given HTML to plain text. # If $do_urls is true, insert the URLs of BAND HREFs on the ends of # their lines. # sub de_htmlify($$) { my ($txt, $do_urls_p) = @_; my $tabstop = 20; $_ = $txt; s@ @ @g; # handle nbsp early # convert HREFs to text -- yyy --> "yyy -- xxx" # do this for but not for or . # Also, upcase the band names. # s@<(BAND)([^>]*)>(.*?) @{ my ($u, $b) = ($2, $3); if (! ($u =~ s/^.*\bHREF=\"([^\"]+)\".*$/$1/si)) { $u = undef; } # $u =~ s/\&/&/g if ($u); $b = de_entify($b); $b =~ s/(.*)/\U$1/g; (($do_urls_p && $u) ? "$b\002$u" : $b); }@xegsi; # inside

@gsi; # no blank line before this one s@

($1@gsi; s@]*>(18|21)\s*@$1$age_text

@gsi; s@]*>AA\s*@$age_aa_text

@gsi; error ("failed to clean AGE tag: \"$1\"") if (m@()@si); s@
@\001@gi; # save linebreaks s@]*>@\001\001@gi; # save paragraphs s@<[^>]+>@@g; # delete all tags s@\s+@ @g; # compress all whitespace s@\001@\n@g; # convert lines/paras back if (m/\002/) { # hack tabstops my @lines = split(/\n/, $_); # find the longest string in column 1 my $max = 0; foreach my $line (@lines) { # only one tab per line! 1 while ($line =~ s/^([^\002]*\002[^\002]*)\002(.*)$/$1 -- $2/s); if ($line =~ m/^(.*)\002/) { my $n = length($1); $max = $n if ($n > $max); } } # now indent the text in column 2 foreach my $line (@lines) { if ($line =~ m/^(.*)\002(.*)$/) { $line = $1 . (' ' x ($max - length($1))) . " -- " . $2; } } $_ = join ("\n", @lines); } s@(http:[^\s]+) *--.*$@$1@mg # kludge for "-- performing live!" BS... if ($do_urls_p); # If there's stuff after the URL on the line, break the line. s@(http:[^\s]+)([ \t]+[^ \t])@$1\n $2@mg; s@^[ \t]+@@mg; # lose whitespace at beginning of lines s@[ \t]+$@@mg; # lose whitespace at end of lines s@\n\n\n+@\n\n@g; # compress blank lines s@^\n+@@sg; # lose leading blank lines s@\n+$@@sg; # lose trailing blank lines $_ = de_entify($_); # translate character entities return $_; } # Upcase the *text* in the HTML string, being careful not to touch # tags and to do the right thing with character entities. # sub upcase_html($) { my ($str) = @_; $str =~ s/(<[^<>]*>)/\001$1\001/gsi; my @segs = split (/\001/, $str); foreach (@segs) { next if (m/^[ \t]*\n?//gs; $html =~ s/^\s+|\s+$//gs; # leading/trailing whitespace in document $html =~ s/^[ \t]+|[ \t]+$//gm; # leading/trailing whitespace on line $html =~ s/[ \t]+/ /gm; # horizontal to single space # convert

and

to simply

1 while ($html =~ s@\s*( to simply # convert
to simply 1 while ($html =~ s@\s*(,
,

or ; sometimes SPC renders. $html =~ s@\s*(]*>)\s*@$1@igs; # This is perhaps risky. However, it looks like Hotmail converts #

to
, so let's use

instead. $html =~ s@

@

@gsi; error ("$lineno: $1 wasn't handled") if ($html =~ m@<(/?(PRES|ETITLE|STATS|FLYER|GENRE|TIME|PRICE|AGE|TICKET| DJ|BAND|OTHER|AFF|LIVE|WEBCAST|BLURB|VIDEO| EVENT|\001))@xi); return $html; } # The HTML validator generates an error if we use a tag that isn't # in this list (in order to detect typos.) # Mostly duplicated in utils/menuify.pl # my %allowed_tags = ( "A" => 1, "AFF" => 1, "B" => 1, "BR" => 2, "BAND" => 1, "DJ" => 1, "ETITLE" => 1, "FLYER" => 2, "GENRE" => 1, "I" => 1, "LIVE" => 2, "OTHER" => 1, "P" => 2, "PRES" => 1, "PRICE" => 1, "STATS" => 1, "STRIKE" => 1, "TABLE" => 1, "TD" => 1, "TICKET" => 2, "TIME" => 1, "TR" => 1, "U" => 1, "UL" => 1, "WEBCAST" => 2, "VIDEO" => 2, "BLURB" => 1, "DIV" => 1, "SPAN" => 1, "EVENT" => 2, "AGE" => 1, ); # Does some simple syntax-checking on the HTML: makes sure tags are # balanced, etc. # Mostly duplicated in utils/menuify.pl # We need our own copy here to give sane error messages about the tags: # we want to do validation *before* those are transformed to
, , etc. # sub validate_html($$$$$) { my ($html, $lineno, $year, $month, $dotm) = @_; # Note: line numbers in error messages aren't quite right, since any # lines commented out with "#" have been deleted before we get here... my $filename = (defined($year) ? "$dotm $months[$month-1] $year" : "no date"); error ("$filename: $lineno: no newline between and <$2>$3") if ($html =~ m@ \s* <(DJ|BAND|OTHER) \b [^<>]* > \s* ([^<>]*?) \s* <@six); my @stack = (); my $debug_html_p = 0; # lose text inside comments (but keep the newlines, for line numbering)... # $html =~ s@()@{ my ($a, $b, $c) = ($1, $2, $3); $b =~ s/^.*$//gm; "$a$b$c"; }@gse; $html =~ s/()//g; # lose comment tags themselves # Simpleminded check for stray ampersands and mis-typed entities. { my $ents = $html; $ents =~ s/&/\001&/gi; $ents =~ s/\bHREF=\"[^\"]+\"//gsi; # kludge my @ents = split(m/\001/, $ents); shift @ents; foreach (@ents) { if (! m/^&[a-z]+\d*;/si) { s/^([^\s]*\s+[^\s<>]*)\b.*$/$1/s; error ("$filename: $lineno: non-entity ampersand: \"$_\""); } } } $html =~ s/\n/\n\001/gs; $html =~ s/\s]+)/; next unless defined ($tag); $tag = uc($tag); $tag = 'P' if ($tag eq '/P'); # kludge... my $code = $allowed_tags{$tag}; if ($tag =~ m@^/@) { # closing a tag my $otag = pop @stack; print STDERR "$lineno: POP " . ('. ' x ($#stack+1)) . "/$otag\n" if ($debug_html_p); if (!defined ($otag)) { error ("$filename: $lineno: extranious <$tag>"); } elsif ($tag ne "/$otag") { my $t2 = $tag; $t2 =~ s@^/@@; $code = $allowed_tags{$t2}; if ($code && $code == 2) { error ("$filename: $lineno: unexpected close-tag form: <$tag>"); } else { error ("$filename: $lineno: <$otag> closed by <$tag>"); } } } elsif (! $code) { error ("$filename: $lineno: unknown tag <$tag>"); } else { my $otag = $stack[$#stack] || ''; if (($tag eq 'TD' && $otag ne 'TR') || ($tag eq 'TH' && $otag ne 'TR') || ($tag eq 'TR' && $otag ne 'TABLE') || ($otag eq 'TR' && $tag ne 'TD' && $tag ne 'TH') || ($otag eq 'TABLE' && $tag ne 'TR') || ($otag eq 'OPTION' && $tag eq 'OPTION') || ($otag eq 'LI' && $tag eq 'LI') || ($tag eq 'ETITLE' && $otag ne '') || ($tag eq 'PRES' && $otag ne '') || ($tag eq 'STATS' && $otag ne '') || ($tag eq 'GENRE' && $otag ne 'STATS') || ($tag eq 'TIME' && $otag ne 'STATS') || ($tag eq 'PRICE' && $otag ne 'STATS') || ($tag eq 'AGE' && $otag ne 'STATS') || (($tag eq 'BR' || $tag eq 'P') && ($otag eq 'GENRE' || $otag eq 'PRES' || $otag eq 'ETITLE')) ) { error ("$filename: $lineno: <$tag> not allowed inside <$otag>"); } if ($code == 2) { # ignore these -- don't insist on closing tags. print STDERR "$lineno: SKIP " . ('. ' x ($#stack+1)) . "$tag\n" if ($debug_html_p); } else { # opening a new tag print STDERR "$lineno: PUSH " . ('. ' x ($#stack+1)) . "$tag\n" if ($debug_html_p); push @stack, $tag; } } } if ($#stack >= 0) { error ("$filename: $lineno: unclosed tags: <" . join (">, <", @stack) . ">"); } } # Return a string with leading whitespace added so as to center the given # text within 72 columns. # sub center_line($) { my ($line) = @_; my $cols = 72; # $fill_column ? $line = (' ' x int(($cols - length($line)) / 2)) . $line; return $line; } # Decides which lines in the text are "headings" and which are "bodies" # and adds indentation. This is for the YYYY/MM.txt files. # sub indentify($$) { my ($text, $long_lines_p) = @_; $text =~ s/[ \t]+$//gm; # lose trailing whitespace $text =~ s/\n\n\n+/\n\n/gs; # compress blank lines $text =~ s/^\n+//s; # lose leading blank lines $text =~ s/\n\n+$/\n/s; # lose trailing blank lines my @lines = split ("\n", $text); my $level = 0; my $para_head_p = 1; my $last_indented_p = 0; foreach (@lines) { s/^/' ' x (4 * $level)/e; # increase next line's indent if this line is at the head of a paragraph # and ends in a colon. my $indent_p = (m/:\s*$/ && $para_head_p); $level++ if ($indent_p); # decrease next line's indent if this line is blank, # unless we just indented on the previous non-blank line. $level-- if (m/^\s*$/ && $level > 0 && !$last_indented_p); # next line is the first line in a paragraph if this line is blank. $para_head_p = (m/^\s*$/); $last_indented_p = $indent_p; # Finally, wrap this line if it's long. if (! $long_lines_p) { $Text::Wrap::columns = 65; $_ = wrap ('', (' ' x (4 * $level)), $_) if (length($_) > $Text::Wrap::columns); } } return join ("\n", @lines); } ############################################################################## # # Parsing the calendar.txt file # ############################################################################## sub parse_calendar_entry($$@) { my ($lineno, $fast_p, @lines) = @_; $_ = $lines[0]; if (m/^(\d\d?)[-\s]+($month_re)[-\s]/io) { # "3-Jan-2001" parse_static_entry ($lineno, $fast_p, @lines); } elsif ( m/^EVERY\s+/io ) { # "Every Nth X" parse_repeating_entry ($lineno, $fast_p, @lines); } elsif ( m/^HOLIDAY\s+/io ) { # "Holiday 2-Feb" parse_holiday_entry ($lineno, @lines); } elsif ( m/^AWARDS\s+/io ) { # "Awards NAME" parse_awards_entry ($lineno, 0, @lines); } elsif ( m/^GROUPS\s+/io ) { # "Groups NAME" parse_awards_entry ($lineno, 1, @lines); } else { error ("$lineno: unparsable calendar entry heading: $_"); } } my $prev_year = 0; # for detecting misordered entries when parsing my $prev_month = 0; # (which usually means cut-and-paste went wrong.) my $prev_dotm = 0; sub parse_static_entry($$@) { my ($lineno, $fast_p, @lines) = @_; my $slineno = $lineno; $_ = shift @lines; if (! m/^(\d\d?) [-\s]+ ($month_re) [-\s]+ (\d{4}) \s* \(?($days_re)?\)? \s* (\#2)? \s* $/iox ) { error ("$lineno: unparsable DD-MMM-YYYY line: $_\n"); } my $dotm = 0 + $1; my $month = $2; my $year = 0 + $3; my $dotw = $4; my $second_event_same_day = defined($5); $month = $monthvals{lc($month)}; $dotw = $dayvals{lc($dotw)} if defined($dotw); error ("$lineno: $dotm-$months[$month-1]-$year appears more than once!\n") if (($prev_year == $year && $prev_month == $month && $prev_dotm == $dotm) && !$second_event_same_day); error ("$lineno: $dotm-$months[$month-1]-$year should be a second event " . "but isn't!\n") if ($second_event_same_day && ($prev_year != $year || $prev_month != $month || $prev_dotm != $dotm)); error ("$lineno: year $prev_year preceeds $year!\n") if ($prev_year > $year); error ("$lineno: $months[$prev_month-1] $prev_year preceeds " . "$months[$month-1] $year!\n") if ($prev_year == $year && $prev_month > $month); error ("$lineno: $prev_dotm-$months[$prev_month-1]-$prev_year preceeds " . "$dotm-$months[$month-1]-$year!\n") if ($prev_year == $year && $prev_month == $month && $prev_dotm > $dotm); my $real_dotw = dotw ($dotm, $month, $year); if (defined ($dotw)) { error ("$lineno: $dotm-$months[$month-1]-$year is a " . "$days[$real_dotw], not a $days[$dotw]") if ($real_dotw != $dotw); } else { $dotw = $real_dotw; my $m = $months[$month-1]; $m =~ s/^(...).*$/$1/; my $d = $days[$dotw]; $d =~ s/^(...).*$/$1/; print STDERR ("$progname: $lineno: $dotm-$m-$year: " . "dotw unspecified (should be $d)\n"); } $lineno++; $prev_year = $year; $prev_month = $month; $prev_dotm = $dotm; my $entry = ""; while ($#lines >= 0) { my $line = shift @lines; $line =~ s/^\s+//; $entry .= "\n$line"; } cache_inline_flyers ($year, $month); my $key = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); my $entry_hash = parse_calendar_html ($dotm, $month, $year, $slineno, $entry, $fast_p, $second_event_same_day); my $listref = $calendar{$key}; my @list = (defined($listref) ? @$listref : ()); push @list, \$entry_hash; $listref = \@list; $calendar{$key} = $listref; if ($verbose > 3) { $_ = $entry; s/\\/\\\\/g; s/\"/\\\"/g; s/\n/\\n/g; s/^(.{20}).*$/$1.../; print STDERR "$progname: $slineno: stored $key = \"$_\"\n"; } } sub parse_repeating_entry($$@) { my ($lineno, $fast_p, @lines) = @_; my $desc = shift @lines; my $entry_html = ""; foreach (@lines) { s/^\s+//; $entry_html .= "$_\n"; } my $repeat_words = "(?:first|second|third|fourth|fifth|last|" . "1st|2nd|3rd|4th|5th)"; my $alter_words = "(?:other)"; if ($desc =~ m/^every\s+ ($alter_words)?\s* ($repeat_words(?:\s*,\s*$repeat_words)*)?\s* ((?:$days_re)(?:\s*,\s*(?:$days_re))*)\s* (?:from\s+(.*?))?\s* (?:to\s+(.*?))?\s* $/xio) { my $alts = $1 || ''; my $reps = $2 || ''; my $days = $3; my $from = $4; my $to = $5; my $from_year = -1; my $from_month = -1; my $from_day = -1; my $to_year = 9999; my $to_month = 99; my $to_day = 99; if ($alts ne '' && $reps ne '') { error ("$lineno: can't specify 'other' along with repetition: $desc\n"); } if ($from) { if ($from =~ m/^(\d\d?)\s*[-\s]\s*($month_re)\s*[-\s]\s*(\d\d\d\d)/io) { $from_day = 0 + $1; $from_month = $monthvals{lc($2)}; $from_year = 0 + $3; } else { error ("$lineno: unparsable from date: \"$from\"" . " (should be DD-MMM-YYYY)\n"); } } if ($to) { if ($to =~ m/^(\d\d?)\s*[-\s]\s*($month_re)\s*[-\s]\s*(\d\d\d\d)/io) { $to_day = 0 + $1; $to_year = 0 + $3; $_ = $2; tr/A-Z/a-z/; $to_month = $monthvals{$_}; } else { error ("$lineno: unparsable to date: \"$to\"" . " (should be DD-MMM-YYYY)\n"); } } $from_year = 0 unless defined($from_year); $from_month = 0 unless defined($from_month); $from_day = 0 unless defined($from_day); $to_year = 9999 unless defined($to_year); $to_month = 13 unless defined($to_month); $to_day = 32 unless defined($to_day); my $rdesc = "Every"; my @values = (); if ($alts ne '') { # this can only be "every other DOTW". push @values, -666; $rdesc .= " Other"; } else { my @rdescs = (); foreach my $rep (split(/,/, $reps)) { $rep =~ s/^\s+//; $rep =~ s/\s+$//; if ($rep =~ m/^(first|1st)$/i) { push @values, 1; push @rdescs, "1st"; } elsif ($rep =~ m/^(second|2nd)$/i) { push @values, 2; push @rdescs, "2nd"; } elsif ($rep =~ m/^(third|3rd)$/i) { push @values, 3; push @rdescs, "3rd"; } elsif ($rep =~ m/^(fourth|4th)$/i) { push @values, 4; push @rdescs, "4th"; } elsif ($rep =~ m/^(fifth|5th)$/i) { push @values, 5; push @rdescs, "5th"; } elsif ($rep =~ m/^last$/i) { push @values, -1; push @rdescs, "Last"; } else { error ("$lineno: INTERNAL ERROR: unknown repeater: $rep\n"); } } $_ = join (", ", @rdescs); s/^(.*), ([^,]+)$/$1 and $2/; $rdesc .= " $_"; } push (@values, 0) if ($#values < 0); { my @rdescs = (); foreach (split(/,/, $days)) { s/^\s+//; s/\s+$//; tr/A-Z/a-z/; push @rdescs, $days[$dayvals{$_}]; } $_ = join (", ", @rdescs); s/^(.*), ([^,]+)$/$1 and $2/; $rdesc .= " $_"; } $rdesc =~ s/\s+/ /gs; foreach (split(/,/, $days)) { s/^\s+//; s/\s+$//; tr/A-Z/a-z/; my $day = $dayvals{$_}; foreach my $code (@values) { my $entry_hash = parse_calendar_html (undef, undef, undef, $lineno, $entry_html, $fast_p, 0); my @day_ord = (0, 1); $entry_hash->{day_ord} = \@day_ord; my @val = ($day, $code, $rdesc, $from_year, $from_month, $from_day, $to_year, $to_month, $to_day, $lineno, $entry_hash); push @repeaters, \@val; if ($verbose > 3) { $_ = $entry_html; s/\\/\\\\/g; s/\"/\\\"/g; s/\n/\\n/g; my $d = $days[$day]; my $w = ($code == 0 ? "" : $code == 1 ? "1st " : $code == 2 ? "2nd " : $code == 3 ? "3rd " : $code == 4 ? "4th " : $code == 5 ? "5th " : $code == -1 ? "Last " : $code == -666 ? "Other " : "<>"); $d =~ s/^(...).*/$1/; s/^(.{20}).*$/$1.../; print STDERR "$progname: $lineno: stored ". sprintf ("%04d-%02d-%02d - %04d-%02d-%02d", $from_year, $from_month, $from_day, $to_year, $to_month, $to_day) . " Every $w$d = \"$_\" (\"$rdesc\")\n" } } } } else { error ("$lineno: unparsable repeating entry: $desc\n"); } } sub parse_holiday_entry($@) { my ($lineno, @lines) = @_; my $entry = shift @lines; shift @lines while ($#lines != -1 && $lines[0] eq ''); error ("$lineno: multi-line holiday?") if ($#lines != -1); $entry =~ s/\s*\#.*$//s; if (! ($entry =~ m/^[a-z]+\s+(\d\d?)[-\s]+($month_re)([-\s]+(\d{4}))?\s*(.*)$/i)) { error ("$lineno: unparsable holiday line: $entry\n"); } my $dotm = 0 + $1; my $month = $2; my $year = defined($4) ? (0 + $4) : 0; my $desc = $5; $month =~ tr/A-Z/a-z/; $month = $monthvals{$month}; my $key = sprintf ("%04d-%02d-%02d-H", $year, $month, $dotm); $calendar{$key} = $desc; print STDERR "$progname: $lineno: stored $key = \"$desc\"\n" if ($verbose > 3); } sub parse_awards_entry($$@) { my ($lineno, $groups_p, @lines) = @_; my $tag = ($groups_p ? "GROUPS" : "AWARDS"); my ($name) = ($lines[0] =~ m/^\s*${tag}\s+(.*?)\s*$/si); error ("unparsable $tag line: $lines[0]") unless $name; $name = lc($name); shift @lines; my $entry = join("\n", @lines); $entry =~ s/[ \t]*\#.*$//m; $entry =~ s/^\s+|\s+$//gs; $entry =~ s/^\s*/ /gm; if ($groups_p) { $groups{$name} = $entry; } else { $awards{$name} = $entry; } print STDERR "$progname: $lineno: $tag $name = $entry\n" if ($verbose > 3); } sub find_awards($$$) { my ($pres, $title, $groups_p) = @_; $title =~ s/<[^<>]*>/ /gsi; $title =~ s/:\s+.*$//s; $title =~ s/^\s+|\s+$//gsi; $title = lc($title); my $a = $groups_p ? $groups{$title} : $awards{$title}; return $a if $a; $pres = '' unless $pres; $pres =~ s/<[^<>]*>/ /gsi; $pres =~ s/\s+presents?:?\s*$//si; $pres =~ s/&/&/gsi; $pres =~ s/,?\s*(\band\b|&)/,/gsi; $pres = lc($pres); foreach my $p (split (/,/, $pres)) { $p =~ s/^\s+|\s+$//gsi; $a = $groups_p ? $groups{$p} : $awards{$p}; return $a if $a; } return undef; } my %ticket_dups; # for detecting duplicate IDs my %event_dups; # for detecting duplicate URLs # Parse the HTML into the various fields in the event hash; # returns a hash describing this event. # sub parse_calendar_html($$$$$$$) { my ($dotm, $month, $year, $lineno, $html, $fast_p, $second_event_same_day) = @_; my $static_p = defined ($dotm); if ($static_p) { my $key = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); parse_shared_flyer ($key, $lineno, $html, $second_event_same_day); } # these are kinda slow, and not needed in --summarize-only mode. validate_html ($html, $lineno, $year, $month, $dotm) unless ($fast_p); parse_dj_tags ($html, $dotm, $month, $year) if ($static_p && !$fast_p); error ("$lineno: non-ASCII character ($1)") if ($html =~ m/([^\s -}])/s); $html =~ s/^\s*//; my ($title_col, $body_col) = ($html =~ m/^(.*?)\n\n+(.*?)\s*$/s); $title_col = $html unless ($title_col); $body_col = '' unless ($body_col); if ($body_col =~ m/\n[ \t]*\n/s) { my $m = $months[$month-1]; $m =~ s/^(...).*/$1/; print STDERR "$progname: WARNING: " . sprintf ("%02d %s %04d: ", $dotm, $m, $year) . "$lineno: blank line\n"; } my $title = extract_event_name ($title_col); $_ = $title_col; my @tickets = (); my $junk = $_; $junk =~ s/\n/ /gs; $junk =~ s/(]*)>@si); my $args = $1; my $url_p = 0; my $this_ticket = undef; my $this_onsale = undef; my $this_offsale = undef; my $this_desc = undef; my $this_vip_p = 0; if ($args =~ m@\b((HREF)|ID)\s*=\s*(\"([^\"]*)\"|([^<>\"\s]+))@si) { $url_p = $2; $this_ticket = $4 || $5; error ("$lineno: unparsable ticket URL: $this_ticket") if ($url_p && !($this_ticket =~ m/^https?:/)); error ("$lineno: unparsable ticket ID: $this_ticket") if (!$url_p && !($this_ticket =~ m/^\d{4,}$/)); } error ("unparsable TICKETS tag") unless $this_ticket; $this_ticket = $ticket_url_base . $this_ticket unless ($url_p); if ($args =~ m@\bTEXT\s*=\s*\"([^\"<>]+)\"@si) { $this_desc = $1; } if ($args =~ m@\bONSALE\s*=\s*\"([^\"<>]+)\"@si) { $this_onsale = $1; error ("$lineno: unparsable ONSALE date: $this_onsale") unless ($this_onsale =~ m/\d\d?-[a-z]{3}-\d\d\d\d/si); } if ($args =~ m@\bOFFSALE\s*=\s*\"([^\"<>]+)\"@si) { $this_offsale = $1; error ("$lineno: unparsable OFFSALE date: $this_offsale") unless ($this_offsale =~ m/\d\d?-[a-z]{3}-\d\d\d\d/si); } $args =~ s/\s*"[^"]*"\s*/ /gsi; # lose quoted strings if ($args =~ m@\bVIP\b@si) { $this_vip_p = 1; } error ("$lineno: duplicate ticket: $this_ticket") if (defined ($ticket_dups{$this_ticket})); $ticket_dups{$this_ticket} = 1; my @P = ($this_ticket, $this_desc, $this_onsale, $this_offsale, $this_vip_p); push @tickets, \@P; } $junk = $_; $junk =~ s/\n/ /gs; $junk =~ s/(]*)>@si); my $args = $1; my ($site, $id); if ($args =~ m@\bSITE=(\"([^<>\"]+)\"|([^\s<>\"]+))@si) { $site = $2 || $3; } if ($args =~ m@\bID=(\"([^<>\"]+)\"|([^\s<>\"]+))@si) { $id = $2 || $3; } error ("$lineno: unparsable") unless ($site && $id); my $key = "$site=$id"; error ("$lineno: duplicate event ID: $key") if (defined ($event_dups{$key})); $event_dups{$key} = 1; push @event_ids, $key; } my $genre = undef; if (m@\s*(.*?)@si) { $genre = $1; $genre =~ s/<[^<>]+>/ /gs; $genre =~ s/\s+/ /gs; $genre =~ s/\s*\.*\s*$//gs; $genre = de_entify($genre); my @g = split (m/\. +/, $genre); # normalize the nine billion names of house. my %gg; foreach my $g (@g) { $g =~ s/^.* (house)$/$1/i unless ($g =~ m/\bacid\b/i); $g = "breaks" if ($g =~ m/break|broken/i); $g = "gothic" if ($g =~ m/deathrock|dark retro/i); $g = "industrial" if ($g =~ m/\bebm\b|industrialize/i); $g = "mashups" if ($g =~ m/bastard pop|remix|mashup|mash-up/i); $g = "80s" if ($g =~ m/^80s/i); $gg{$g} = 1; } @g = keys (%gg); foreach my $g (@g) { $genres{$g} = 1 + ($genres{$g} || 0); } $genre = \@g; } $_ = $html; my $webcast = undef; if (m@]*)>@si) { my $args = $1; my $url_p = 0; if ($args =~ m@\bSRC\s*=\s*(\"([^\"]*)\"|([^<>\"\s]+))@si) { $webcast = $2 || $3; } error ("unparsable WEBCAST tag") unless $webcast; } else { $webcast = 'main'; } $webcast =~ tr/A-Z/a-z/; if (! ($webcast =~ m/^(main|lounge|off)$/s)) { error ("WEBCAST SRC=\"$webcast\": " . "must be \"main\", \"lounge\" or \"off\""); } $_ = $html; my $embed_video = undef; my $embed_title = undef; if (m@]*)>@si) { my $args = $1; my $url_p = 0; if ($args =~ m@\bSRC\s*=\s*(\"([^\"]*)\"|([^<>\"\s]+))@si) { $embed_video = $2 || $3; } if ($args =~ m@\bTITLE\s*=\s*(\"([^\"]*)\"|([^<>\"\s]+))@si) { $embed_title = $2 || $3; } error ("unparsable VIDEO tag: no SRC") unless $embed_video; error ("unparsable VIDEO tag: no TITLE") unless $embed_title; } # Check for using "&foo=bar" in URLs instead of "&foo=bar". # s@(http:[^<>\"]+)@{ my ($u, $ou) = ($1, $1); $u =~ s/&//gs; error ("bogus entity in URL: $ou") if ($u =~ m/&/s); $ou; }@gsexi; my %hash = (); $hash{title} = $title; #$hash{repeat} = $repeat; # computed in parse_calendar_event_final #$hash{holiday} = $holiday; # computed in parse_calendar_event_final #$hash{vacationp} = $vacationp; # computed in parse_calendar_event_final #$hash{day_ord} = $day_ord; # computed in parse_calendar_event_final $hash{tickets} = ($#tickets >= 0 ? \@tickets : undef); $hash{event_ids} = \@event_ids; $hash{webcast} = $webcast; $hash{video} = $embed_video; $hash{videoname} = $embed_title; $hash{html_src} = $html; # fill in some more fields now, if we can. parse_calendar_html_2 (\%hash, $dotm, $month, $year) if (defined ($dotm)); return \%hash; } # Some parsing we can only do once we're sure of the exact date of the # event. We know this right away for static events, but only later for # repeating events. # sub parse_calendar_html_2($$$$) { my ($eventref, $dotm, $month, $year) = @_; my $dotw = dotw ($dotm, $month, $year); my $monstr = $months[$month-1]; $monstr =~ s/^(...).*$/$1/; my $date = "$days[$dotw], $dotm $monstr $year"; my $html = $eventref->{html_src}; $html =~ s/^\s*//; $html =~ s/\n\n.*$//s; # keep only "title" column my ($age) = ($html =~ m@(.*?)@si); my ($timestr) = ($html =~ m@@si); $timestr = '' unless $timestr; my ($start_minute, $end_minute, $webcast_end_minute) = extract_hours ($timestr, $dotw); my @time_list = ($year, $month, $dotm, $dotw, $start_minute, $end_minute, $webcast_end_minute); $eventref->{date} = $date; # altered in parse_calendar_event_final $eventref->{times} = \@time_list; $eventref->{age} = $age; return $eventref; } # After the whole calendar has been parsed, fill in some fields in the # event hashes that we can't know until we've seen everything. # E.g., we can't know if there are multiple events on the given day # until after we've seen all of them. # sub parse_calendar_event_final($$$) { my ($dotm, $month, $year) = @_; my $key = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); my $listref = $calendar{$key}; return unless defined ($listref); my @list = @$listref; my $nevents = $#list+1; my $i = -1; foreach my $event (@list) { $i++; parse_calendar_event_final_1 ($$event, $nevents, $i, 0, 1); } } sub parse_calendar_event_final_1($$$$$) { my ($event, $nevents, $event_number, $known_repeater, $real_event_p) = @_; my $title = $event->{title}; my $times = $event->{times}; my $date = $event->{date}; my ($year, $month, $dotm, $dotw, $start_minute) = @$times; my @day_ord = ($event_number, $nevents); $event->{day_ord} = \@day_ord; # These have to be after 'day_ord' was filled in. $event->{flyer} = find_flyer ($event); $event->{photos} = find_photo ($event); ####################################################################### # Update the day of the week to, e.g., "Sunday Morning" if appropriate. ####################################################################### # Compute the appropriate description of this (time of) day... my $day = describe_day ($dotw, $start_minute, $nevents, $event_number); # Splice it back in, and store the result back in the hash. $date =~ s/($days_re)/$day/i; $event->{date} = $date; ####################################################################### # Decide whether this event is a holiday, or just before a holiday. ####################################################################### my $holiday = lookup_holiday ($year, $month, $dotm); my $monday_off_p = (defined($holiday) && $holiday =~ m/^\*/); # Mark the sunday of 3 day weekends as holidays, too, if the # holiday on the sunday or adjascent monday are federal. # if ($dotw == 0 && # today is sunday !($month == 12 && $dotm == 30) # not day-before-NYE ) { if (!defined ($holiday)) { $holiday = lookup_holiday_tomorrow ($year, $month, $dotm); $monday_off_p = (defined($holiday) && $holiday =~ m/^\*/); # Tomorrow is a holiday, but not a federal holiday. Darn. $holiday = undef unless ($monday_off_p); } # Only mention "3 day weekend" on the last event of the day. $holiday = undef unless ($event_number == $nevents-1); if (defined ($holiday)) { $holiday =~ s/^\*//s; if ($monday_off_p) { $holiday = "3 Day Weekend: $holiday"; $event->{vacationp} = 1; } } } if (defined ($holiday)) { $holiday =~ s/^\*//sx; # take off Fed marker $event->{holiday} = $holiday; } ####################################################################### # determine whether this is a repeating event. ####################################################################### if ($known_repeater) { $event->{repeat} = $known_repeater; } else { my ($dotw_number, $total_dotws) = dotw_count ($year, $month, $dotm); my $repeater = lookup_repeating_entry ($year, $month, $dotm, $dotw, $dotw_number, $total_dotws); if (defined ($repeater)) { my $rtitle = $repeater->{title}; my $etitle = $title; $etitle =~ s/: +.*$//s; # lose subtitle after colon error ("unparsable event name in repeater: $repeater") unless ($rtitle); if ($rtitle eq $etitle) { print STDERR "$progname: $title repeats: $title\n" if ($verbose > 3); $event->{repeat} = $repeater->{repeat}; } } } ####################################################################### # What event number is this, and how many events are there? # # Note that this does not see "repeating" events that are visible # in the calendar, but do not yet have "real" data filled in. # So the event ordinals will change as those get filled in. ####################################################################### if ($real_event_p && !($event->{title} =~ m/^\(?CLOSED|CANCELL?ED|SUSPENDED\)?$/si)) { $event->{event_ord} = $total_event_count; $event_ords[$total_event_count] = \$event; my $dd = $year * 10000 + $month * 100 + $dotm; $total_event_count++; $event_count_12mo++ if ($dd > $one_year_ago && $dd <= $stats_cutoff); $event_count_6mo++ if ($dd > $six_months_ago && $dd <= $stats_cutoff); if ($event->{html_src} =~ m/<(BAND|LIVE)\b/i) { $total_live_count++; $live_count_12mo++ if ($dd > $one_year_ago && $dd <= $stats_cutoff); $live_count_6mo++ if ($dd > $six_months_ago && $dd <= $stats_cutoff); } } } # Try to find the short name of the given event, by digging it out of # the HTML. # sub extract_event_name($) { my ($text) = @_; my $otext = $text; my ($title) = ($text =~ m@]*>\s*(.*?)\s*@si); error ("no title: $text") unless ($title && $title =~ m/[^\s]/); $title =~ s@\s*
\s*@ @gsi; error ("tags in title: $title") if ($title =~ m/[<>]/); $title =~ s/\s+/ /gsi; $title = de_entify($title); # now do some error checking on what's left. # if ($text =~ m@.*<\1>@si) { error ("multiple <$1> tags:\n\n$otext"); } if ($text =~ m@(.*?)@si) { my $a = $1; error ("unparsable AGE tag: \"$a\"") unless ($a =~ m/^(AA|18|21)$/si); } # $text =~ s@@@gsi; $text =~ s@<(ETITLE|PRES|STATS|GENRE|TIME|PRICE|AGE)\b[^<>]*>.*@@gsi; $text =~ s@<(FLYER|TICKET)\b[^<>]*>@@gsi; # $text =~ s@<(P|BR)>@ @gsi; if (! ($text =~ m/^\s*$/s)) { print STDERR "$progname: junk in event name: $otext\n\n"; } # strip trailing punctuation $title =~ s/[-:;]+$//gsi; return $title; } # Returns text briefly describing the event: basically, just the DJ/band # names in the front room (or, if that can't be found, the back room.) # Puts a "*" in front of band names. # sub extract_event_desc($) { my ($text) = @_; return undef unless $text; my $result = ''; my $count = 0; $text =~ s/\s+/ /gs; $text =~ s/]*>\s*([^\n]+?)\s*$@is) { my $bandp = defined($2); my $who = de_htmlify($3, 0); $who = "*$who" if ($bandp); $result .= "$who\n"; $count++; } elsif (m/^.*?Lounge( DJs)?:/is) { last if ($count > 0); } } return $result; } # Extracts and returns the starting and ending time of the event from # its textual description, measured in minutes past midnight. Handles # "after hours", and adds heuristic slop based on the day of the week. # sub extract_hours($$$) { my ($text, $dotw) = @_; my ($start, $end); $text = de_htmlify ($text, 0); my $otext = $text; # Correct typo. error ("missing AM/PM: \"$1\"") if ($text =~ m@(\d\d?(:\d\d)?\s+(door|show))@si); # first look for something of these forms: # # 8pm - 2am # midnight - 1AM # 10 pm - after hours # while ($text =~ m/ \b\s* ((\d\d?(:\d\d)?)? \s* ([AP]\.?M\.?|noon|midnight|midnite)) \s*-\s* ([[:alnum:]:.]+) \b(.*) $/xsi) { my $start1 = $1; my $end1 = $5; $text = $6; $start1 = parse_hour ($start1, 0); next unless defined ($start1); $end1 = parse_hour ($end1, 1); next unless defined ($end1); if (defined($start1) && defined($end1)) { $start = $start1; $end = $end1; last; } } # If that didn't work, look for something like this: # # doors @ 7pm; show @ 8pm; # doors at 7pm; show at 8pm; # doors: 7pm; show: 8pm; # # If all we have is this "doors" form, assume end time is 2AM. # if (!defined ($start)) { $text = $otext; while ($text =~ m/ \b\s* doors:? \s+ (\@|at)? \s* ((\d\d?(:\d\d)?)? \s* ([AP]\.?M\.?|noon|midnight|midnite)) \b(.*) $/xsi) { my $start1 = $2; $text = $5; $start1 = parse_hour ($start1, 0); next unless defined ($start1); if (defined($start1)) { $start = $start1; $end = (60 * 2); # 2AM last; } } } # if that didn't work, at least try to find a start time... # if (!defined ($start)) { $text = $otext; while ($text =~ m/ \b\s* ((\d\d?(:\d\d)?)? \s* ([AP]\.?M\.?|noon|midnight|midnite)) \b(.*) $/xsi) { my $start1 = $1; $text = $5; $start1 = parse_hour ($start1, 0); next unless defined ($start1); if (defined($start1)) { $start = $start1; last; } } } $start = (60 * 22) unless defined($start); # default to 10pm $end = (60 * 3) unless defined($end); # default to 3am my $webcast_end = $end; my $weekend_p = ($dotw == 5 || # fri $dotw == 6 || # sat $dotw == 0 # sun (treat sunday as a weekend, since we # are usually only open on sunday if # it's a three day weekend.) ); if ($end eq 'LATE') { if ($weekend_p) { $end = (60 * 5) + 30; # on weekends, "after hours" means 5:30 AM } else { $end = (60 * 4) + 30; # on week nights, "after hours" means 4:30 AM } $webcast_end = $end; } else { # # If an explicit end time was listed in the calendar, add some slack onto # that for the webcast. Add half and hour on weeknights, and an hour on # the weekend. Or an hour and a half if it's a weekend set to end at 3am # or earlier. if (!$weekend_p) { # weeknight $webcast_end += 30; } elsif ($end <= (60 * 3)) { # <= 3AM on weekend $webcast_end += 90; } else { # > 3AM on weekend $webcast_end += 60; } } $end += (60 * 24) if ($end < $start); # end is "tomorrow" $webcast_end += (60 * 24) if ($webcast_end < $start); error("INTERNAL ERROR: end>webcast_end: $end, $webcast_end") if ($end > $webcast_end); error("INTERNAL ERROR: start>=end, $start, $end") if ($start >= $webcast_end); return ($start, $end, $webcast_end); } sub parse_hour($$) { my ($h, $endp) = @_; if ($h =~ m/\b(\d\d?)(:(\d\d))?\s*(A\.?M\.?)/i) { return undef if ($1 < 1 || $1 > 12); return undef if (defined($3) && ($3 < 0 || $3 > 60)); $h = 60 * $1; $h += $3 if (defined($3)); return $h; } elsif ($h =~ m/\b(\d\d?)(:(\d\d))?\s*(P\.?M\.?)/i) { return undef if ($1 < 1 || $1 > 12); return undef if (defined($3) && ($3 < 0 || $3 > 60)); $h = 60 * ($1 == 12 ? 12 : (12 + $1)); $h += $3 if (defined($3)); return $h; } elsif ($h =~ m/(\b12\s*)?\s*noon\b/i) { return 60 * 12; } elsif ($h =~ m/(\b12\s*)?\s*mid\s*-?\s*(nite|night)\b\b/i) { return 0; } elsif ($endp && $h =~ m/\b(after\b|after\s*-?\s*hours\b)/i) { return 'LATE'; } else { return undef; } } ############################################################################## # # , , and tags # ############################################################################## # Compares current time to start-date and end-date. # Either date can be undef, meaning unspecified. # Returns 0 if ticket is between dates (on sale). # Returns -1 if not on sale yet, and 1 if off sale already. # sub ticket_off_sale_p($$$) { my ($event, $on, $off) = @_; my ($dd1, $mm1, $yyyy1) = ($on =~ m/(\d\d?)-([a-z]{3})-(\d\d\d\d)/si) if $on; my ($dd2, $mm2, $yyyy2) = ($off =~ m/(\d\d?)-([a-z]{3})-(\d\d\d\d)/si) if $off; error ("unparsable ONSALE date: $on") if ($on && !$yyyy1); error ("unparsable OFFSALE date: $off") if ($off && !$yyyy2); $mm1 = $monthvals{lc($mm1)} if $mm1; $mm2 = $monthvals{lc($mm2)} if $mm2; my $times = $$event->{times}; my ($yyyy3, $mm3, $dd3, $dotw, $start_minute) = @$times; my $hh3 = int($start_minute / 60); my $min3 = int($start_minute % 60); $hh3--; # off sale an hour before doors. my $now = time(); my $on_time_t = mktime(0,0,0, $dd1, $mm1-1, $yyyy1-1900, 0,0,-1) if $yyyy1; my $off_time_t = mktime(0,0,0, $dd2, $mm2-1, $yyyy2-1900, 0,0,-1) if $yyyy2; my $end_time_t = mktime(0,$min3,$hh3, $dd3, $mm3-1, $yyyy3-1900, 0,0,-1); error ($$event->{date} . ": unparsable OFFSALE preceeds ONSALE: $on, $off") if ($on_time_t && $off_time_t && $on_time_t > $off_time_t); error ($$event->{date} . ": OFFSALE is after event date") if ($off_time_t && $off_time_t > $end_time_t); return -1 if ($on_time_t && $now < $on_time_t); # early: before on-date return 1 if ($off_time_t && $now >= $off_time_t); # late: after off-date return 1 if ($now >= $end_time_t); # late: event over return 0; # just right } # Whether "DD-MMM-YYYY" is more than N days ago. # sub ticket_on_sale_this_week_p($$) { my ($date, $days) = @_; my ($dd, $mm, $yyyy) = ($date =~ m/(\d\d?)-([a-z]{3})-(\d\d\d\d)/si); error ("unparsable ONSALE date: $date") unless ($yyyy); $mm = $monthvals{lc($mm)}; my ($tsec, $tmin, $thour, $tdotm, $tmon, $tyear) = localtime (time); $tmon++; $tyear += 1900; my ($esec, $emin, $ehour, $edotm, $emon, $eyear) = localtime (time - ($days * 24 * 60 * 60)); $emon++; $eyear += 1900; my $onsale = $yyyy * 10000 + $mm * 100 + $dd; my $cutoff = $eyear * 10000 + $emon * 100 + $edotm; my $today = $tyear * 10000 + $tmon * 100 + $tdotm; return ($onsale > $cutoff && $onsale <= $today); } # We use certain magic tags in the calendar.txt file, to save typing and # enable certain other semantic hacks. # sub rewrite_dj_tags($$) { my ($lineno, $html) = @_; $html =~ s@\"]+\")>(.*?) @
$3@xigs; $html =~ s@<(DJ|OTHER)(\s+DUP)?\s+(HREF=\"[^<>\"]+\")>(.*?) @$4@xigs; $html =~ s@<(/?)BAND(\s+DUP)?>@<$1B>@igs; $html =~ s@@@igs; $html =~ s@@@igs; $html =~ s@@@igs; return $html; } sub rewrite_tickets_tags($$$$$) { my ($event, $html, $show_tickets_p, $lineno, $tickets_form_p) = @_; my $ticket_count = 0; my $did_ticket_p = 0; while ($html =~ m@]*)>@i) { my $args = $1; my $url_p = 0; my $vip_p = 0; my $ticket = undef; my $text = undef; my $onsale = undef; my $offsale = undef; if ($args =~ m@\b((HREF)|ID)\s*=\s*(\"([^\"]*)\"|([^<>\"\s]+))@si) { $url_p = $2; $ticket = $4 || $5; } if ($args =~ m@\bTEXT\s*=\s*\"([^\"]*)\"@si) { $text = $1; } if ($args =~ m@\bONSALE\s*=\s*\"([^\"]*)\"@si) { $onsale = $1; } if ($args =~ m@\bOFFSALE\s*=\s*\"([^\"]*)\"@si) { $offsale = $1; } $args =~ s/\s*"[^"]*"\s*/ /gsi; # lose quoted strings if ($args =~ m@\bVIP\b@si) { $vip_p = 1; } my $blurb = "(What's this?)"; if ($tickets_form_p) { $blurb = "\n $blurb\n"; } else { $blurb =~ s/[()]//gs; $blurb = "\n
($blurb)\n"; } my $target; my $off_p = ticket_off_sale_p ($event, $onsale, $offsale); if (! $show_tickets_p) { $target = ''; } elsif ($off_p) { my ($dd, $mm, $yyyy) = ($onsale =~ m/(\d\d?)-([a-z]{3})-(\d\d\d\d)/si); $mm =~ s/^(.)/{uc($1)}/xe; if ($vip_p) { $target = ''; } elsif ($did_ticket_p) { $target = ''; } elsif ($off_p > 0) { # no longer on sale $target = ''; } else { # not on sale yet $target = "Tickets on sale $mm $dd. "; } } elsif ($tickets_form_p && !$url_p) { if (! $vip_p) { $text = "Buy Tickets" unless ($text); $blurb = ''; } else { $text = "VIP Service: $vip_price" unless $text; } $target = ("

\n" . " \n" . " \n" . "  \n" . " \n" . $blurb . "\n"); } else { $ticket = $ticket_url_base . $ticket unless ($url_p); if (! $vip_p) { $text = "Buy tickets now!" unless ($text); $blurb = ''; } else { $text = "VIP Service: $vip_price" unless $text; } $target = ("" . "$text" . $blurb . "
"); $target = "

$target" if ($vip_p); } $did_ticket_p = 1 if $target; $html =~ s@]*>\s*@$target@is; # delete first copy of tag! $ticket_count++; } error ("$lineno: TICKET wasn't converted") if ($html =~ m@, , and tags in the HTML, and populate the # %performers table with the result. (Date args are for error messages.) # sub parse_dj_tags($$$$) { my ($html, $dotm, $month, $year) = @_; # Parse out each DJ/BAND/OTHER tag and create a reversed list, so # that we get the "Nth" numbers right (the first band listed on the # calendar goes on last, so the ordinals count backwards within a # given day.) # Swap Main Room and Lounge, so that the main room DJs are considered # to be "earlier" than the lounge DJs (first DJ on in the main room # has a lower ordinal than the first or last DJ in the lounge.) $html =~ s@((Main Room|Performing Live).*)(Lounge.*)@$3$1@gsi; my @pp = (); foreach (split ('<', $html)) { next unless m@^((DJ)|(BAND)|OTHER) (\s+DUP\b)? \s* (HREF=\"([^<>\"]+)\")?\s*>(.*)$@isx; my $tag = $1; my $djp = defined($2); my $bandp = defined($3); my $dupp = defined($4); my $url = $6 || ''; my $name = $7; my $type = ($djp ? 'dj' : $bandp ? 'band' : 'other'); $name =~ s/^\s+//gs; $name =~ s/\s+$//gs; error ("$dotm $months[$month-1] $year: unparsable <$tag>") if ($name eq ''); my @p = ($tag, $djp, $bandp, $dupp, $url, $name, $type); unshift @pp, \@p; } # Now iterate over the pre-parsed and reversed tags. # foreach my $p (@pp) { my ($tag, $djp, $bandp, $dupp, $url, $name, $type) = @$p; my $key = $name; $key = asciify (de_entify ($key)); # simplify accented characters $key = lc($key); # downcase $key =~ s/\b(the|and|dj|mc|mr)\b//g; # noise words $key =~ s/\s//g; # lose whitespace $key =~ s/^!$/shift1/s; # kludge for stupid "DJ !" $key =~ s/[^a-z\d]//g; # lose non-alphanumeric $key =~ s/([^aeiouy])\1+/$1/g; # merge consecutive consonants error ("can't canonicalize $name") if ($key eq ''); if ($dupp) { # kludge for when there's a DJ and a BAND with the same name: # one of them looks like to suppress the complaint. $key .= " _DUP_"; } my $listref = $performers{$key}; my @list = (defined($listref) ? @$listref : ()); my @dates = (); my $ord = undef; if (defined($listref)) { # there was an entry already my ($oname, $otype, $ourl, $oord, $odatesref) = @list; if ($otype ne $type) { # warn about type mismatch my $m = $months[$month-1]; $m =~ s/^(...).*$/$1/; error ("$dotm $m $year: is $name a DJ, BAND, or OTHER?"); } if ($oname ne $name && # warn about name spelling change length($name) > 2 && $verbose) { my $m = $months[$month-1]; $m =~ s/^(...).*$/$1/; print STDERR "$progname: $dotm $m $year: \"$oname\" " . "changed to \"$name\"\n"; } if ($ourl ne $url && $verbose) { # warn about URL change my $m = $months[$month-1]; $m =~ s/^(...).*$/$1/; print STDERR "$progname: $dotm $m $year: $name: " . "\"$ourl\" changed to \"$url\"\n"; } $ord = $oord; @dates = @$odatesref; error ("$key: no dates?") if ($#dates < 0); } my $d = $year * 10000 + $month * 100 + $dotm; if ($#dates == -1 || $dates[$#dates] != $d) { # avoid dups push @dates, $d; } my $datesref = \@dates; if (! defined($ord)) { $ord = ($bandp ? $performers_band_tick++ : $djp ? $performers_dj_tick++ : $performers_other_tick++); } @list = ($name, $type, $url, $ord, $datesref); $listref = \@list; $performers{$key} = $listref; } } # Write the calendar/ledger.html file. # sub generate_ledger_html($$) { my ($dir, $topten_p) = @_; my $bands = ''; # html for the bands/djs themselves my $djs = ''; my $others = ''; my $dj_idx = ''; # html for the A-Z indexes my $band_idx = ''; my $other_idx = ''; my $dj_count = 0; # totals my $band_count = 0; my $other_count = 0; my $dj_letter = ''; # previous first-letter-of-name encountered my $band_letter = ''; my $other_letter = ''; my $ties_p = 1; foreach my $key (sort (keys (%performers))) { my $listref = $performers{$key}; my @list = @$listref; my ($name, $type, $url, $ord, $datesref) = @list; my @dates = @$datesref; $name =~ s/^(the|dj|mc|mr\.?)\s+//i; # lose leading noise words my $letter = $key; $letter =~ s/^(.).*$/$1/; $letter = uc($letter); $letter = 'A' unless $letter =~ m/[A-Z]/i; my $ccount; if ($type eq 'dj') { $ccount = ++$dj_count; my $letter2 = $letter; $letter = '' if ($letter eq $dj_letter); $dj_letter = $letter2; } elsif ($type eq 'band') { $ccount = ++$band_count; my $letter2 = $letter; $letter = '' if ($letter eq $band_letter); $band_letter = $letter2; } elsif ($type eq 'other') { $ccount = ++$other_count; my $letter2 = $letter; $letter = '' if ($letter eq $other_letter); $other_letter = $letter2; } else { error ("unknown ledger type: $type"); } my ($colorL, $colorR) = (((($ccount-1) / 3) & 1) ? (" CLASS=\"LL\"", " CLASS=\"LO\"") : ("", " CLASS=\"LE\"")); my $desc = ""; my $n = "$name"; $n = "$n" if ($url ne ''); if ($letter eq '') { $desc .= ""; } else { my $anchor = "${type}_$letter"; $desc .= ("" . "" . "$letter" . "" . ""); my $idx = "$letter "; if ($type eq 'dj') { $dj_idx .= $idx; } elsif ($type eq 'band') { $band_idx .= $idx; } elsif ($type eq 'other') { $other_idx .= $idx; } else { error ("unknown ledger type: $type"); } } $desc .= "$n"; $desc .= ""; my $count = 0; foreach (@dates) { my $year = int($_/10000); my $month = int($_/100) % 100; my $dotm = $_ % 100; my $u = sprintf("%04d/%02d.html\#%02d", $year, $month, $dotm); $desc .= ",\n" unless ($count == 0); my $m = $months[$month-1]; $m =~ s/^(...).*$/$1/; $desc .= "$dotm-$m-$year"; $count++; } $desc .= " (" . ($#dates+1) . ")" if ($#dates > 9); $desc .= "\n"; if ($type eq 'dj') { $djs .= $desc; } elsif ($type eq 'band') { $bands .= $desc; } elsif ($type eq 'other') { $others .= $desc; } else { error ("unknown ledger type: $type"); } } # Compute the top ten. # if ($topten_p) { my $output = ''; my $n = 25; my $i; my @sorted = sort { my $aa = $performers{$a}->[4]; my $bb = $performers{$b}->[4]; return ($#$bb == $#$aa ? ($performers{$a}->[0] cmp $performers{$b}->[0]) : $#$bb <=> $#$aa); } (keys (%performers)); { my $n1 = $total_event_count; my $n2 = $total_live_count; my ($first, $last) = find_scheduled_range (); $_ = $first; my ($first_year, $first_month) = m/^(\d{4})-(\d{2})$/; $_ = $last; my ($last_year, $last_month) = m/^(\d{4})-(\d{2})$/; my $start = mktime (0,0,0, 1, $first_month-1, $first_year-1900, 0,0,-1); my $end = mktime (0,0,0, 30, $last_month-1, $last_year-1900, 0,0,-1); my $days = ($end - $start) / (60 * 60 * 24); my $weeks = $days / 7; my $months = $days / 30; my $years = $days / 365.25; $output .= sprintf ("\nScheduled range: %s %d - %s %d:\n", $months[$first_month-1], $first_year, $months[$last_month-1], $last_year); $output .= sprintf (" " . "%d days; %d weeks; %.1f months; %.2f years.\n\n", $days, $weeks, $months, $years); $output .= sprintf (" Total events: %4d\n", $n1); $output .= sprintf (" Total live shows: %4d\n", $n2); $output .= sprintf (" Events / month: %4.1f ", ($n1/$months)); $output .= sprintf (" Live shows / month: %4.1f\n", ($n2/$months)); $output .= sprintf (" Events / week: %4.1f ", ($n1/$weeks)); $output .= sprintf (" Live shows / week: %4.1f\n", ($n2/$weeks)); $start = $one_year_ago; $end = $stats_cutoff; $days = 365; $weeks = $days / 7; $months = $days / 30; $n1 = $event_count_12mo; $n2 = $live_count_12mo; $output .= sprintf ("\nLast %d months:\n\n", $months); $output .= sprintf (" Total events: %4d\n", $n1); $output .= sprintf (" Total live shows: %4d\n", $n2); $output .= sprintf (" Events / month: %4.1f ", ($n1/$months)); $output .= sprintf (" Live shows / month: %4.1f\n", ($n2/$months)); $output .= sprintf (" Events / week: %4.1f ", ($n1/$weeks)); $output .= sprintf (" Live shows / week: %4.1f\n", ($n2/$weeks)); $start = $six_months_ago; $end = $stats_cutoff; $days = 365 / 2; $weeks = $days / 7; $months = $days / 30; $n1 = $event_count_6mo; $n2 = $live_count_6mo; $output .= sprintf ("\nLast %d months:\n\n", $months); $output .= sprintf (" Total events: %4d\n", $n1); $output .= sprintf (" Total live shows: %4d\n", $n2); $output .= sprintf (" Events / month: %4.1f ", ($n1/$months)); $output .= sprintf (" Live shows / month: %4.1f\n", ($n2/$months)); $output .= sprintf (" Events / week: %4.1f ", ($n1/$weeks)); $output .= sprintf (" Live shows / week: %4.1f\n", ($n2/$weeks)); } { $output .= "\n"; my $url = 'http://cerebrum.dnalounge.com/usage/slist-stats.txt'; my $lists = `wget -qO- $url`; $output .= $lists; } $i = 1; $output .= "\nMost frequent bands:\n\n"; my $last_n = -1; my $last_i = -1; foreach my $key (@sorted) { my $listref = $performers{$key}; my ($name, $type, $url, $ord, $datesref) = @$listref; next unless ($type eq 'band'); my $nd = $#$datesref+1; next if ($nd == 1); my $ii = ($ties_p && $nd == $last_n ? $last_i : $i); $last_n = $nd; $last_i = $ii; $output .= sprintf("%5d: %s (%d)\n", $ii, de_entify($name), $nd); last if (++$i > $n && $n != 0); } $last_n = -1; $last_i = -1; $i = 1; $output .= "\nMost frequent DJs:\n\n"; foreach my $key (@sorted) { my $listref = $performers{$key}; my ($name, $type, $url, $ord, $datesref) = @$listref; next unless ($type eq 'dj'); my $nd = $#$datesref+1; next if ($nd == 1); my $ii = ($ties_p && $nd == $last_n ? $last_i : $i); $last_n = $nd; $last_i = $ii; $output .= sprintf("%5d: %s (%d)\n", $ii, de_entify($name), $nd); last if (++$i > $n && $n != 0); } if (0) { $last_n = -1; $last_i = -1; $i = 1; $output .= "\nMost frequent \"others\":\n\n"; foreach my $key (@sorted) { my $listref = $performers{$key}; my ($name, $type, $url, $ord, $datesref) = @$listref; next unless ($type eq 'other'); my $nd = $#$datesref+1; next if ($nd == 1); my $ii = ($ties_p && $nd == $last_n ? $last_i : $i); $last_n = $nd; $last_i = $ii; $output .= sprintf("%5d: %s (%d)\n", $ii, de_entify($name), $nd); last if (++$i > $n && $n != 0); } } { my $all_genres_p = 0; $last_n = -1; $last_i = -1; $i = 1; $output .= "\n" . ($all_genres_p ? "All genres, by frequency: " . "All genres, alphabetical:" : "Most frequent genres:") . "\n\n"; my @out = (); foreach my $key (sort { my $aa = $genres{$a}; my $bb = $genres{$b}; if ($aa == $bb) { return ($a cmp $b); } else { return ($bb <=> $aa); } } keys (%genres)) { my $val = $genres{$key}; my $ii = ($ties_p && $val == $last_n ? $last_i : $i); $last_n = $val; $last_i = $ii; push @out, sprintf("%5d: %s (%d)\n", $ii, $key, $val); $i++; last if (!$all_genres_p && $i > $n && $n != 0); } if ($all_genres_p) { my @keys = sort (keys (%genres)); foreach my $line (@out) { my $key = shift @keys; my $val = $genres{$key}; $line = sprintf("%-38s %s (%d)\n", $line, $key, $val); } } $output .= join ("", @out); } $output .= "\nInteresting Nths:\n"; my @cool_numbers = (1, 10, 50, 69, 100, 500, 666, 999, 1000, 1024, 1500, 2000, 2048, 2500, 3000, 3500, 4000, 4096, 4500, 5000, 5500, 6000, 6500, 7000, 7500, 8000, 8500, 9000, 9500, 10000); my @cool_numbers_2 = (150, 200, 250, 300, 400, 600, 700, 800, 900); foreach my $type ('dj', 'band') { $output .= "\n"; my @out = (); foreach my $key (@sorted) { my $listref = $performers{$key}; my ($name, $ttype, $url, $ord, $datesref) = @$listref; next unless ($ttype eq $type); my @dates = @$datesref; my $date = $dates[0]; my $cool = 0; my @nums = @cool_numbers; push @nums, @cool_numbers_2 if ($type eq 'band'); foreach my $c (sort { $a <=> $b } (@nums)) { if ($c == $ord+1) { $cool = $ord+1; last; } } next unless ($cool); $date =~ s@^(\d{4})(\d\d)(\d\d)$@ my ($y, $m, $d) = ($1, $2, $3); $m = $months[$2-1]; $m =~ s/^(...).*$/$1/; "$d-$m-$y";@gsex; $cool = "$cool" . ($cool == 1 ? "st" : "th"); $cool .= " " . ($type eq 'dj' ? 'DJ' : $type); $name = de_entify($name); $name =~ s/^(.{20}).*/$1/; push @out, sprintf("%13s: %-20s %s\n", $cool, $name, $date); } @out = sort { my ($aa, $bb) = ($a, $b); $aa =~ s/^\s*(\d+).*$/$1/; $bb =~ s/^\s*(\d+).*$/$1/; return $aa <=> $bb; } @out; $output .= join ("", @out); } $output .= "\n"; foreach my $c (sort { $a <=> $b } (@cool_numbers, @cool_numbers_2)) { next if ($c == 1); my $event = $event_ords[$c-1]; next unless defined ($event); my $title = $$event->{title}; my $date = $$event->{date}; $date =~ s/^.*, //; $date =~ s/ /-/g; $date = "0$date" unless ($date =~ m/^\d\d/); my $cool = "$c" . ($c == 1 ? "st" : "th") . " event"; $title =~ s/^(.{20}).*/$1/; $output .= sprintf("%13s: %-20s %s\n", $cool, $title, $date); } $output .= "\n"; my $outfile = "$dir/$calendar_topten_file"; write_file_if_changed ($outfile, $output, 0); return; # don't bother writing ledger.html in topten mode } { my $a = "

"; my $b = "
"; $band_idx = $a . $band_idx . $b; $dj_idx = $a . $dj_idx . $b; } my $output = ''; $output .= "$page_title: Bands and DJs\n"; my $nevents = $total_event_count; $nevents -= ($#future_events + 1); # $band_count =~ s/(\d)(\d\d\d)$/$1,$2/; # $dj_count =~ s/(\d)(\d\d\d)$/$1,$2/; # $nevents =~ s/(\d)(\d\d\d)$/$1,$2/; $output .= ("\n" . "\n" . "\n"); $output .= ("\n" . "
\n" . " $band_count bands
\n" . " $dj_count djs
\n" . " $nevents events
\n" . "
\n" . "\n" . "\n" . "\n" . "Bands and DJs\n" . "\n" . "


\n" . "This is a list of all the bands and DJs who have performed\n" . "at DNA Lounge since we re-opened in 2001. Clicking on a\n" . "name will take you to that performer's home page (if we\n" . "know it) and clicking on a date will take you to the\n" . "appropriate entry in the\n" . "calendar.\n" . "From there, you can find the corresponding\n" . "flyers,\n" . "photo galleries,\n" . "etc.\n" . "

\n" . "If you have any corrections to the URLs or names below,\n" . " please \n" . "let us know!\n" . "

\n" . "There is also a very incomplete\n" . "list of bands who performed here before 2001.\n" . "

\n" . "\n" . " \n" . " \n" . " \n" . $bands . " \n" . " \n" . " \n" . $djs); # $output .= (" \n" . # " \n" . # " \n"); # $output .= $others; # This kludge turns this: Into this: # # ----------------- ----------------- # | X | ABC | | X | ABC | # ----------------- | |---------| # | | DEF | | | DEF | # ----------------- | |---------| # | | GHI | | | GHI | # ----------------- ----------------- { $_ = $output; s@(@i, $chunk); my $count = $#junk; if ($count > 0) { $count++; $chunk =~ s@(ROWSPAN=)(\d+)@$1$count@gsi; $chunk =~ s@@@gsi; } } $output = join ('', @chunks); } $output .= "
\n" . "
\n" . "\n" . "Bands\n" . "\n" . $band_idx . "


\n" . "

\n" . "

\n" . "
\n" . "\n" . "DJs, MCs, etc.\n\n" . $dj_idx . "


\n" . "

\n" . # "

\n" . # "
\n" . # "\n" . # "\"Others\"\n\n" . # $other_idx . # "


\n" . # "

]* ROWSPAN=)@\001$1@gs; my @chunks = split (/\001/, $_); foreach my $chunk (@chunks) { my @junk = split (m@
\n"; $output .= "\n"; my $outfile = "$dir/$calendar_ledger_file"; 1 while $outfile =~ s@[^/]+/\.\./@@g; write_file_if_changed ($outfile, $output, 1); } # Write the calendar/names.txt file. # This contains a line for each event, "YYYY-MM-DD \t NAME" # The name begins with an asterisk if it's a "live" event. # If there are two events, the second date ends in "b". # sub generate_names_file($) { my ($dir) = @_; my %output; foreach my $key (sort (keys %calendar)) { my ($year, $month, $dotm) = ($key =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)$/); next unless defined ($dotm); my $listref = $calendar{$key}; if (defined ($listref)) { my $count = 0; foreach my $event (@$listref) { my $title = $$event->{title}; my $live_p = ($$event->{html_src} =~ m/<(BAND|LIVE)\b/i); $title = "*$title" if ($live_p); my $key2 = ($count ? $key . "b" : $key); $output{$key2} = "$key2\t$title\n"; $count++; } } } my $output = ''; foreach my $key (sort (keys %output)) { $output .= $output{$key}; } my $outfile = "$dir/$calendar_names_file"; write_file_if_changed ($outfile, $output, 0); } # Write the calendar/1985-1999.html file, based on calendar/1985-1999.txt. # sub generate_eighties_file($) { my ($dir) = @_; local *IN; my $file = "$dir/$calendar_eighties_file"; open (IN, "<$file") || error ("$file unreadable"); print STDERR "$progname: reading $file\n" if ($verbose > 3); my $prev_date = ''; my $prev_year = ''; my $table_top = ("\n" . "" . "" . "" . "\n"); my $output = "
\n"; while () { my ($year, $mm, $dd, $bands) = m@^(\d{4})-([\d\?][\d\?])-([\d\?][\d\?])\s+(.*)$@s; error ("$file: unparsable: $_") unless $bands; my $mmm = ($mm eq '??' ? '???' : $months[$mm-1]); $mmm =~ s/^(...).*$/$1/s; my $date = "$dd $mmm"; # if ($dd ne '??') { # my $dotw = $days[dotw($dd, $mm, $year)]; # $dotw =~ s/^(...).*$/$1/s; # $date .= " ($dotw)"; # } error ("$file: dup date: $year-$mm-$dd") if ($date eq $prev_date && $dd !~ m/\?/); $prev_date = $date; my $year_col = ''; if ($year ne $prev_year) { $output .= ("\n" . "

") if ($prev_year); $output .= ("\n" . "" . "

$year
" . "\n" . "\n" . "\n" . $table_top); $prev_year = $year; } $bands =~ s/&/&/gs; $bands =~ s//>/gs; $bands =~ s/\s+/ /gs; my $flyer = "flyers/1985-1999/$year-$mm-$dd.html"; $bands = "$bands" if (-f $flyer); $output .= (" " . "$date" . "" . "$bands" . "\n"); } $output .= "\n"; my $title = "DNA Lounge Calendar: 1985-1999"; $output = ("$title\n" . "\n" . " \n" . "\n" . "\n" . "

\n" . "<< 1984\n" . "2001 >>\n" . "\n" . "\n" . "\n" . "$title\n\n" . "

\n" . $eighties_top_blurb . "


\n" . $output . "\n" ); my $outfile = $file; $outfile =~ s@\.txt$@.html@s; write_file_if_changed ($outfile, $output, 1); } ############################################################################## # # Flyers, Photos # ############################################################################## # Search for a tag in the given HTML, which means # "this entry shares a flyer with the entry on the given other date." # sub parse_shared_flyer($$$$) { my ($date, $lineno, $html, $second_event_same_day) = @_; return unless ($html =~ m@]*)>@i); $_ = $1; my ($val) = m/\bDATE=\"([^<>\"]*)\"/i; if ($val =~ m/^NONE$/i) { print STDERR "$progname: $lineno: skipped flyer $date = NONE\n" if ($verbose > 3); } elsif ($val =~ m/^(\d\d?)(b?)[-\s]+($month_re)[-\s]+(\d{4})\s*(\#2)?\s*$/io) { my $ndotm = 0 + $1; my $ord = $2; my $nmonth = $3; my $nyear = 0 + $4; $ord = 'b' if $5; $nmonth = $monthvals{lc($nmonth)}; my $nnumeric = ($nyear * 10000) + ($nmonth * 100) + $ndotm; my $ndate = sprintf ("%04d-%02d-%02d%s", $nyear, $nmonth, $ndotm, $ord); $date =~ m/^(\d{4})-(\d{2})-(\d{2})$/; my $odotm = 0 + $3; my $omonth = $2; my $oyear = 0 + $1; my $onumeric = ($oyear * 10000) + ($omonth * 100) + $odotm; if ($nnumeric >= $onumeric) { $omonth = $months[$omonth-1]; $omonth =~ s/^(...).*$/$1/; $nmonth = $months[$nmonth-1]; $nmonth =~ s/^(...).*$/$1/; $date = "$odotm-$omonth-$oyear"; error ("$lineno: flyer target ($ndate) >= date ($date)"); } $date .= "b" if ($second_event_same_day); $shared_flyers{$date} = $ndate; print STDERR "$progname: $lineno: stored flyer $date = $ndate\n" if ($verbose > 3); } else { error ("$lineno: unparsable FLYER tag"); } } # Returns the relative HREF to the photo gallery for this date, if any. # sub find_photo($$$) { my ($event) = @_; my $times = $event->{times}; my ($year, $month, $dotm) = @$times; my $ordP = $event->{day_ord}; my @ord = $ordP ? @$ordP : (); my $dir = sprintf ("gallery/%04d/%02d-%02d", $year, $month, $dotm); $dir .= "b" if ($ord[0]); return "$dir/" if (-d $dir); return undef; } # Returns the relative HREF to the flyer for this date, if any. # sub find_flyer($) { my ($event) = @_; my $html = $event->{html_src}; if ($html =~ m@@i) { return undef; } my $times = $event->{times}; my ($year, $month, $dotm) = @$times; my $ordP = $event->{day_ord}; my @ord = $ordP ? @$ordP : (); my $date = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); $date .= "b" if ($ord[0]); my $shared = $shared_flyers{$date}; my $result = $inline_flyers{$date}; if (defined ($shared)) { error ("both shared and real flyers for $date") if $result; $result = $inline_flyers{$shared}; error ("expected shared flyer $shared for $date") unless $result; } return undef unless defined($result); my ($url) = ($result =~ m@]+)"@i); error ("no href in flyer html? $date - $result") unless ($url); 1 while $url =~ s@[^/]+/\.\./@@g; return $url; } # Reads the flyer thumbnail index for the given month, and caches the HTML # describing the first thumbnail of each date (the and tags, # including width and height). # sub cache_inline_flyers($$) { my ($year, $month) = @_; my $donep = sprintf ("%04d-%02d-DONE", $year, $month); return if ($inline_flyers{$donep}); $inline_flyers{$donep} = 1; my $file = sprintf ("flyers/%04d/%02d/index.html", $year, $month); local *IN; if (open (IN, "<$file")) { print STDERR "$progname: reading $file\n" if ($verbose > 3); my $body = ''; while () { $body .= $_; } close IN; $body =~ s/\s+/ /gs; $body =~ s/(]*>) \s* ()@xi) { my $html = "$1$2"; my $dotm = $3; my $ord = $4; my $key = sprintf ("%04d/%02d/%02d%s", $year, $month, $dotm, $ord); $key =~ s@/@-@g; # YYYY-MM-DDb my $prefix = sprintf ("../../flyers/%04d/%02d/", $year, $month); $html =~ s@((SRC|HREF)\s*=\s*\")@$1$prefix@gi; error ("two flyers for $key: $inline_flyers{$key}") if ($inline_flyers{$key}); $inline_flyers{$key} = $html; print STDERR "$progname: cached flyer image $key\n" if ($verbose > 4); } } } } # Given the file name of a flyer URL, returns the HTML describing the # first thumbnail of that flyer. (Assuming cache_inline_flyers() has # already been called for this month). # sub find_inline_flyer($) { my ($flyer_file) = @_; my ($key) = ($flyer_file =~ m@(\d{4}/\d\d/\d\db?)\.html$@); error ("unparsable flyer filename: \"$flyer_file\"") unless ($key); $key =~ s@/@-@g; # YYYY-MM-DDb my $html = $inline_flyers{$key}; error ("missing flyer: $flyer_file") unless ($html); return $html; } # Returns a list of the the URLs present in the given HTML. # if $all_p is true, returns all of them; # else, returns only the "important" looking ones. # sub find_event_urls($$) { my ($head_html, $all_p) = @_; my @result = (); $head_html =~ s@]*>@ @gsi; $head_html =~ s/\s+/ /gs; # squash whitespace $head_html =~ s@(<[^/])@\n$1@gs; # break at open tags foreach (split ('\n', $head_html)) { next unless (m@^\s*<(A|ETITLE)\s+HREF="([^\"]+)">\s*([^<>]+?)\s*@i); my ($tag, $url, $text) = ($1, $2, $3); if ($all_p || $tag =~ m/ETITLE/i) { push @result, $url; } } return @result; } ############################################################################## # # Looking up entries in the calendar # ############################################################################## # Returns a string describing the holiday on this date, if any. # sub lookup_holiday($$$) { my ($year, $month, $dotm) = @_; my $key = sprintf ("%04d-%02d-%02d-H", $year, $month, $dotm); my $key2 = sprintf ("%04d-%02d-%02d-H", 0, $month, $dotm); my $val = $calendar{$key}; $val = undef if ($val && $val =~ m/^\s*$/s); if (!defined($val)) { $key = $key2; $val = $calendar{$key}; } $val = undef if ($val && $val =~ m/^\s*$/s); if ($val) { $_ = $val; s/\\/\\\\/g; s/\"/\\\"/g; s/\n/\\n/g; print STDERR "$progname: holiday: $key: \"$_\"\n" if ($verbose > 2); return $val; } else { return undef; } } # Returns a string describing the holiday on the *following* day, if any. # sub lookup_holiday_tomorrow($$$) { my ($year, $month, $dotm) = @_; my $dpm = days_per_month ($month, $year); if (++$dotm > $dpm) { $dotm = 1; if (++$month == 12) { $year++; } } return lookup_holiday ($year, $month, $dotm); } # Returns an entry-hash describing a repeating entry for this day, if any. # # $dotw is the day of the week (0-6) # $dotw_number is the number of that day in this month # (e.g., 2 for "2nd wednesday") # $total_dotws is how many of that dotw this month has # (e.g., 5 for "there are 5 wednesdays"); # sub lookup_repeating_entry($$$$$$) { my ($year, $month, $dotm, $dotw, $dotw_number, $total_dotws) = @_; my $value = undef; my $vlineno = undef; $_ = $days[$dotw]; s/^(...).*/$1/; foreach my $ptr (@repeaters) { my @entry = @$ptr; my ($edotw, $rep, $rdesc, $from_year, $from_month, $from_dotm, $to_year, $to_month, $to_dotm, $lineno, $entry_hash) = @entry; next unless ($edotw == $dotw); # skip if out of range. my $from_i = ($from_year * 10000 + $from_month * 100 + $from_dotm); my $to_i = ($to_year * 10000 + $to_month * 100 + $to_dotm); my $i = ($year * 10000 + $month * 100 + $dotm); next if ($i < $from_i || $i > $to_i); # today is within the range of this repeater. my @nths = ("1st", "2nd", "3rd", "4th", "5th"); if ($rep == 0) { # an "every week" entry # always matches. } elsif ($rep >= 1 && $rep <= 4) { # an "every [1-4] week" entry next unless ($dotw_number == $rep); } elsif ($rep == 5) { # an "every 5th week" entry # the 4th day in a month with 5 of them. # (note that "4th friday" and "5th friday" indicate the same day # in a month that has 5 fridays.) next unless ($dotw_number == 4 && $total_dotws == 5); } elsif ($rep == -1) { # an "every last week" entry next unless ($dotw_number == $total_dotws); } elsif ($rep == -666) { # an "every other week" entry error ("\"every other week\" not supported yet"); } else { error ("INTERNAL ERROR: unknown repeater $rep\n"); } # If we made it here, this repeater matches this day. if (defined($value)) { print STDERR "$progname: WARNING: duplicate repeating matches: " . "line $lineno and line $vlineno\n"; } elsif ($verbose > 2) { my $key = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); $_ = $entry_hash->{title}; print STDERR "$progname: repeater: $key: \"$_\"\n"; } my %new_hash = %$entry_hash; # copy it $value = \%new_hash; $vlineno = $lineno; parse_calendar_html_2 ($value, $dotm, $month, $year); parse_calendar_event_final_1 ($value, 1, 0, $rdesc, 0); } return $value; } ############################################################################## # # Start the parser going # ############################################################################## sub load_calendar($$) { my ($file, $fast_p) = @_; local *IN; open(IN, "$file") || error ("$file: $!"); my $slineno = 0; my $lineno = 0; my @lines = (); $total_event_count = 0; $total_live_count = 0; $event_count_12mo = 0; $live_count_12mo = 0; $event_count_6mo = 0; $live_count_6mo = 0; { my ($sec, $min, $hour, $dotm, $mon, $year) = localtime (time); $mon++; $year += 1900; $stats_cutoff = $year * 10000 + $mon * 100 + $dotm; $mon -= 6; if ($mon < 1) { $mon += 12; $year--; } $six_months_ago = $year * 10000 + $mon * 100 + $dotm; $mon -= 6; if ($mon < 1) { $mon += 12; $year--; } $one_year_ago = $year * 10000 + $mon * 100 + $dotm; } while () { my $line = $_; my $contp = 0; $lineno++; $_ = undef; next if ($line =~ m/^\s*\#/); # skip fully commented lines $line =~ s/\s*\#+\s.*//g; # lose comments at end of line $contp = 1 if ($line =~ m/^\s/ || $line =~ m/^$/); $contp = 0 if ($line =~ m/^HOLIDAY/i); # kludge if (!$contp) { if ($#lines >= 0) { parse_calendar_entry ($slineno, $fast_p, @lines); @lines = (); } $slineno = $lineno; } $line =~ s/\n$//s; if ($#lines >= 0 || !$contp) { push @lines, $line; } } if ($#lines >= 0) { # last entry parse_calendar_entry ($slineno, $fast_p, @lines); } # Second pass, to finish parsing things. # foreach (sort (keys %calendar)) { my ($year, $month, $dotm) = m/^(\d\d\d\d)-(\d\d)-(\d\d)$/; parse_calendar_event_final ($dotm, $month, $year) if defined($year); } close IN; } # returns ("YYYY-MM" "YYYY-MM") of the range that the calendar covers. # sub find_scheduled_range() { my $min = 9999999; my $max = 0; foreach (keys (%calendar)) { if (m/^(\d+)-(\d+)-(\d+)$/) { my ($year, $month, $dotm) = ($1, $2, $3); my $i = $year * 1000 + $month; $min = $i if ($i < $min); $max = $i if ($i > $max); } } error ("no calendar events scheduled at all!") if ($max <= 0); my $first = sprintf ("%04d-%02d", $min/1000, $min%100); my $last = sprintf ("%04d-%02d", $max/1000, $max%100); print STDERR "$progname: scheduled range: $first - $last\n" if ($verbose > 3 && ($first || $last)); return ($first, $last); } ############################################################################## # # Generating calendar overview grids # ############################################################################## # returns a list of five items: # the html for the cal grid header; # the URLs that are in the "prev" and "next" links (or undef). # the pretty names of the "prev" and "next" links (or undef). # sub build_cal_header($$$$) { my ($year, $month, $prev_p, $next_p) = @_; my $output = ""; my ($prev_name, $next_name); my $last_month = $months[($month+10) % 12]; my $this_month = $months[$month-1]; my $next_month = $months[$month % 12]; my $last_month_year = ($month == 1 ? $year-1 : $year); my $next_month_year = ($month == 12 ? $year+1 : $year); my $last_month_href = sprintf("%02d", ((($month+10) % 12) + 1)) . ".html"; my $next_month_href = sprintf("%02d", ((($month) % 12) + 1)) . ".html"; $last_month_href = "../$last_month_year/$last_month_href" if ($year != $last_month_year); $next_month_href = "../$next_month_year/$next_month_href" if ($year != $next_month_year); $last_month_href = undef unless ($prev_p); $next_month_href = undef unless ($next_p); $prev_name = "$last_month $last_month_year" if $prev_p; $next_name = "$next_month $next_month_year" if $next_p; $last_month =~ s/^(...).*$/$1/; $this_month =~ s/^(...).*$/$1/; $next_month =~ s/^(...).*$/$1/; $last_month = "<< $last_month"; $next_month = "$next_month >>"; $output .= (($last_month_href ? "$last_month" : "$last_month") . "\n" . ($next_month_href ? "$next_month" : "$next_month") . "\n" . "

$this_month
\n"); $output =~ s/^/ /gm; $output =~ s/^\s+//s; return ($output, $last_month_href, $next_month_href, $prev_name, $next_name); } sub event_cell_title($) { my ($e) = @_; my $t = $e->{title}; my $a = $e->{age}; if ($a) { if ($a eq 'AA') { $a = "all ages"; } else { $a .= "+"; } $t .= "\n$a"; } return $t; } # Returns the HTML for a calendar grid, with embedded hrefs. # sub make_calendar_grid_html($$$$$) { my ($dir, $year, $month, $prev, $next) = @_; my $today; { my ($sec, $min, $hh, $dd, $mm, $yyyy) = localtime; $mm++; $yyyy += 1900; $today = ($month == $mm && $year == $yyyy) ? $dd : -1; } my $output = ("
\n" . ($prev ? " <<\n" : '') . ($next ? " >>\n" : '') . "
Sun
\n" . "
Mon
\n" . "
Tue
\n" . "
Wed
\n" . "
Thu
\n" . "
Fri
\n" . "
Sat
\n" . "
\n" . "\n"); my $days = days_per_month ($month, $year); my $dotm = 1; my $dotw = dotw ($dotm, $month, $year); my $did_suspension_p = 0; my $cdotw = $dotw; for (my $week = 0; $week < 6; $week++) { last if ($dotm > $days); $output .= "
\n"; for (; $cdotw <= 6; $cdotw++, $dotm++) { next if ($dotm > $days); my $key = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); my $event = $calendar{$key}; my $repeat_p = 0; my $suspended_p = 0; # If there wasn't a static event, see if there was a repeating event. if (!defined($event)) { my ($dotw_number, $total_dotws) = dotw_count ($year, $month, $dotm); $event = lookup_repeating_entry ($year, $month, $dotm, $cdotw, $dotw_number, $total_dotws); $repeat_p = defined($event); } # Kludge: If this event just says "closed", then this is just there to # shadow a repeating event that would otherwise show up here. E.g., # when there's an "every thu" event but one specific day is dark. # # Likewise, omit it from the calendar grid if it is "CANCELLED". # if (!$repeat_p && defined($event)) { my @L = @$event; my $e0 = $L[0]; my $title = $$e0->{title}; if (defined ($title) && $title =~ m/\b(CANCELL?ED|POSTPONED|CLOSED|SUSPENDED)\b/i) { $event = undef; $suspended_p = ($title =~ m/\bSUSPENDED\b/i); } } my $class = "ccell ccell$cdotw"; my $id = ($today == $dotm ? " ID=\"today\"" : ""); if (defined ($event)) { my $nn = sprintf("%02d", $dotm); my @titles = (); my $t; if ($repeat_p) { push @titles, event_cell_title($event); } else { foreach my $e0 (@$event) { push @titles, event_cell_title($$e0); } } my $count = 0; foreach my $title (@titles) { my $b = ($#titles > 0 ? chr(ord('a') + $count) : ''); $class .= " ccell$b" if $b; $title = entitify($title); $title =~ s@\n([^\n]*)$@$1@si; # italicize age line $output .= (" " . "
" . ($count > 0 ? '' : "$dotm") . $title . "
" . "
\n"); $count++; } } elsif ($suspended_p) { my $body = ''; if (! $did_suspension_p) { local *IN; open (IN, "<$dir/$suspension_file") || error ("$dir/$suspension_file: $!"); print STDERR "$progname: reading $dir/$suspension_file\n" if ($verbose > 2); my $b2 = ''; while () { $b2 .= $_; } close IN; $b2 =~ s@^.*]*>(.*).*$@$1@si; $b2 =~ s@\s*@@gs; $b2 =~ s@((HREF|SRC)="../)@$1../@gs; $b2 =~ s/\bccell\d\b/ccell$cdotw/s; $did_suspension_p = 1; $body = $b2; } my $id2 = "$id STYLE=\"background:#600\""; $output .= "
$dotm
\n"; $output .= $body; } else { $output .= "
$dotm
\n"; } } $output .= "
\n\n"; $cdotw = 0; } return $output; } ############################################################################## # # Generating full calendar body text and html # ############################################################################## # Constructs an EMBED tag from a YouTube URL. # sub make_embed_tag($$$$) { my ($url, $title, $w, $h) = @_; my ($id, $url2, $url3); if ($url =~ m@^http://www\.youtube\.com/@) { error ("extra junk in youtube URL: $url") if ($url =~ m/\&|\?.*\?/); $url =~ s@&.*$@@; # lose args $url =~ s@/(watch)?\?v=@/v/@; # "/watch?v=XXX" => "/v/XXX" ($id) = ($url =~ m@/([^/]+)$@si); $url .= '&color1=0&color2=0x004400'; # green ui $url .= '&fs=1'; # enable full screen button $url .= '&rel=0'; # turn off "related" mouseovers? $url .= '&showsearch=0'; # turn off search field $url .= '&showinfo=0'; # turn off title overlay $url .= '&iv_load_policy=3'; # turn off annotations $url .= '&ap=%2526fmt=18'; # higher quality, if available # $url .= '&ap=%2526fmt=22'; # HD, but only works if HD exists # $url .= "&start=$start" if ($start); $url =~ s/\&/&/gsi; # URL-entity-quotify $url2 = "http://www.youtube.com/watch?v=$id"; $url3 = "http://img.youtube.com/vi/$id/0.jpg"; } else { error ("VIDEO URL is not YouTube: $url"); } $w -= 2; # "video" class adds 1px border $h -= 2; $w = sprintf ("%0.3fem", $w * 0.075); $h = sprintf ("%0.3fem", $h * 0.075); $w =~ s/0+(em)/$1/s; $w =~ s/\.(em)/$1/s; $h =~ s/0+(em)/$1/s; $h =~ s/\.(em)/$1/s; return ("
" . # Note: rewritten by utils/menuify.pl "" . # MSIE uses only "MOVIE"; Firefox uses only "DATA"; # Safari and Opera use either. So we need both. "\n " . "\n \n" . " \n" . "" . "
\n"); } sub url_quote($) { my ($u) = @_; $u =~ s|([^-a-zA-Z0-9.\@/_\r\n])|sprintf("%%%02X", ord($1))|ge; return $u; } # Constructs HTML for linking to this event on social networking sites. # sub make_event_id_link($$$) { my ($site, $id, $event_title) = @_; my ($site_name, $url); $site = lc($site); if ($site eq 'going') { $site_name = "Going"; my $tt = url_quote ($event_title); $tt =~ s/%20/_/g; # dumb. $url = "http://sanfrancisco.going.com/$id;$tt"; } elsif ($site eq 'upcoming') { $site_name = "Upcoming"; $url = "http://upcoming.yahoo.com/event/$id/"; } elsif ($site eq 'lastfm') { $site_name = "Last.FM"; $url = "http://www.last.fm/event/$id"; } elsif ($site eq 'sonicliving') { $site_name = "SonicLiving"; $url = "http://sonicliving.com/event/$id/"; } elsif ($site eq 'facebook') { $site_name = "Facebook"; $url = "http://www.facebook.com/event.php?eid=$id"; } elsif ($site eq 'myspace') { $site_name = "MySpace"; if ($id =~ m/^\d+$/s) { $url = "http://events.myspace.com/Event/View/$id/"; # new style } else { $url = ("http://events.myspace.com/index.cfm" . # old style, "?fuseaction=events.detail" . # NNNNNN.NNNNN "&eventID=$id"); } } elsif ($site eq 'yelp') { $site_name = "Yelp"; $url = "http://www.yelp.com/events/$id"; } elsif ($site eq 'sfstation') { # Don't include this one in the calendar. #$site_name = "SFStation"; #$url = "http://www.sfstation.com/$id"; } elsif ($site eq 'sfweekly' || $site eq 'sfgate') { # These aren't real IDs. } else { error ("unknown event site: $site ($id)"); } return ($url ? "$site_name" : undef); } sub make_event_id_links($$) { my ($event, $future_p) = @_; my @elinks; my $title = $$event->{title}; foreach my $key (@{$$event->{event_ids}}) { my ($site, $id) = ($key =~ m/^(.*?)=(.*)$/si); error ("unparsable event_id: $key") unless ($id); my $e = make_event_id_link ($site, $id, $title); push @elinks, $e if ($e); } return '' if ($#elinks < 0 && !$future_p); my $times = $$event->{times}; my ($year, $month, $dotm) = @$times; my $id = sprintf ("%02d%02d%02d", $year, $month, $dotm); if ($$event->{day_ord} && $$event->{day_ord}[1] > 1) { $id .= chr (ord('a') + $$event->{day_ord}[0]); } # Only include the links to export.cgi for future events, because # it works by parsing the .ics file, and that does not contain # historic events. # my $u1 = ""; my $u3 = ""; if ($future_p) { unshift @elinks, ($u1 . "ics" . $u2 . "Apple iCal" . $u3, $u1 . "outlook" . $u2 . "Outlook" . $u3, $u1 . "google" . $u2 . "Google" . $u3, $u1 . "yahoo" . $u2 . "Yahoo" . $u3); } my $html = "Add this event to
your calendar:
\n
    "; foreach (@elinks) { $html .= "
  • $_
  • \n"; } $html .= "
"; if ($future_p) { $html .= ("Or post about it on
" . $u1 . "twitter" . $u2 . "Twitter" . $u3 . " or " . $u1 . "facebook" . $u2 . "Facebook" . $u3 . "
"); } return $html; } sub make_fblike_link($) { my ($event) = @_; my $url = $$event->{flyer}; return '' unless $url; $url = $url_base . $url; $url =~ s@:@%3A@gs; $url =~ s@/@%2F@gs; return (""); } sub simplify_event($) { my ($event) = @_; my $date = $$event->{date}; my $name = $$event->{title}; my $html = $$event->{html_src}; my ($col0, $pres, $title, $stats, $genre, $time, $age, $price, $tickets, $lineup, $blurb, $video) = ('', '', '', '', '', '', '', '', '', '', '', ''); # Parse out all the junk in the first column. # if ($html =~ m/^(.*?)\n\n(.*)$/s) { ($col0, $html) = ($1, $2); } else { $col0 = $html; $html = ''; } $pres = $3 if ($col0 =~ s@(<(PRES ) [^<>]*> (.*?) )@@six); $title = $1 if ($col0 =~ s@(<(ETITLE ) [^<>]*> (.*?) )@@six); $stats = $3 if ($col0 =~ s@(<(STATS ) [^<>]*> (.*?) )@@six); $genre = $3 if ($stats =~ s@(<(GENRE ) [^<>]*> (.*?) )@@six); $time = $3 if ($stats =~ s@(<(TIME ) [^<>]*> (.*?) )@@six); $age = $3 if ($stats =~ s@(<(AGE ) [^<>]*> (.*?) )@@six); $price = $3 if ($stats =~ s@(<(PRICE ) [^<>]*> (.*?) )@@six); $tickets = $1 if ($stats =~ s@((]*> \s* (<(P|BR)>\s*)*)+)@@six); $blurb = $3 if ($html =~ s@(<(BLURB ) [^<>]*> (.*?) )@@six); # col1. $video = $1 if ($html =~ s@(<(VIDEO ) [^<>]*> )@@six); # col1. $html =~ s@]*>\s*@@gsi; # lose extra videos error ("$date: $name: leftover junk in col0: $col0") if ($col0 =~ m/[^\s]/s); # $html =~ s/\s*

\s*/

/gsi; # $html =~ s/\s*(
)\s*(
)\s*(
\s*)*/$1$2/gsi; my ($prefix, $para1title, $para1, $para2title, $para2, $suffix, $extra) = ('', '', '', '', '', '', ''); $html =~ s@(

\s*)+@

@gsi; $html =~ s@\s*

\s*$@@gsi; $html =~ s@()\s+&\s+($2@gsi; #### hmm $html =~ s@()\s+&\s+($2@gsi; #### hmm $html =~ s@-vs-@
@gsi; #### hmmm if ($html =~ m@^\s*$@si) { #print STDERR "BLANK $date: $name\n"; } elsif ($html =~ m@^ \s* (?: ]*> \s* )? ( .*? )? # 1 prefix (?: <(?:BR|P)> \s* )? ( Main \s Room: ) \s* # 2 para1title <(?:BR|P)> \s* ( (?: <(?:DJ|BAND|OTHER)[^<>]*> [^<>]+ # 3 para1 \s* (?: (?:
\s* )? [^<>]+ \s* )?
\s* )+ ) (?:

\s* ( Lounge: ) \s* # 4 para2title <(?:BR|P)> \s* ( (?: <(?:DJ|BAND|OTHER)[^<>]*> [^<>]+ # 5 para2 \s* (?: (?:
\s* )? [^<>]+ \s* )?
\s* )+ ) )? (?: <(?:BR|P)> \s* ( .+ ) )* \s* # 6 suffix $ @gsix) { ($prefix, $para1title, $para1, $para2title, $para2, $suffix) = ($1||'', $2||'', $3||'', $4||'', $5||'', $6||''); my $o = "[$prefix]\n$para1title\n[$para1]\n$para2title\n[$para2]\n[$suffix]\n"; $o =~ s/^/\t/gm; #print STDERR "#1 $date: $name:\n$o\n"; } elsif ($html =~ m@^ \s* (?: ]*> \s* )? ( .*? )? # 1 prefix (?: <(?:BR|P)> \s* )? ( Performing \s live: ) \s* # 2 para1title <(?:BR|P)> \s* ( (?: <(?:DJ|BAND|OTHER)[^<>]*> [^<>]+ # 3 para1 \s* (?: (?: <(?:BR|P)> \s* )? [^<>]+ \s* )? <(?:BR|P)> \s* )+ ) (?:

\s* ( <[BI]> (?:With|Plus) \s DJs?:? ) \s* # 4 para2title (?: <(?:BR|P)> \s* )? ( (?: <(?:DJ|BAND|OTHER)[^<>]*> [^<>]+ # 5 para2 \s* (?: (?:
\s* )? [^<>]+ \s* )?
\s* )+ ) )? (?: <(?:BR|P)> \s* ( .+ ) )* \s* # 6 suffix $ @gsix) { ($prefix, $para1title, $para1, $para2title, $para2, $suffix) = ($1||'', $2||'', $3||'', $4||'', $5||'', $6||''); my $o = "[$prefix]\n$para1title\n[$para1]\n$para2title\n[$para2]\n[$suffix]\n"; $o =~ s/^/\t/gm; #print STDERR "#2 $date: $name:\n$o\n" } elsif ($html =~ m@^ \s* (?: ]*> \s* )? ( .*? )? # 1 prefix (?: <(?:BR|P)> \s* )? ( <[BI]> (?: Featuring \s )? Performances \s by: ) \s* # 2 para1title <(?:BR|P)> \s* ( (?: <(?:DJ|BAND|OTHER)[^<>]*> [^<>]+ # 3 para1 \s* (?: (?: <(?:BR|P)> \s* )? [^<>]+ \s* )? <(?:BR|P)> \s* )+ ) (?:

\s* ( <[BI]> (?:With|Plus) \s DJs?:? ) \s* # 4 para2title (?: <(?:BR|P)> \s* )? ( (?: <(?:DJ|BAND|OTHER)[^<>]*> [^<>]+ # 5 para2 \s* (?: (?:
\s* )? [^<>]+ \s* )?
\s* )+ ) )? (?: <(?:BR|P)> \s* )? ( .+ ) \s* # 6 suffix $ @gsix) { ($prefix, $para1title, $para1, $para2title, $para2, $suffix) = ($1||'', $2||'', $3||'', $4||'', $5||'', $6||''); my $o = "[$prefix]\n$para1title\n[$para1]\n$para2title\n[$para2]\n[$suffix]\n"; $o =~ s/^/\t/gm; #print STDERR "#3 $date: $name:\n$o\n" } else { print STDERR "NO $date: $name\n"; # print STDERR "$html\n\n"; } # $html =~ s/\s*((
)+)\s*/$1\001/gsi; # foreach my $line (split(/\001/, $html)) { # if ($line =~ m@^<[BI]>Performing live:@si && !$para1title) { # $para1title = "Performing live:\n

\n"; # } elsif ($line =~ m@^<[BI]>Main room:@si && !$para1title) { # $para1title .= "Main room:
\n"; # } elsif ($line =~ m@^<[BI]>Lounge:@si && !$para2title) { # $para2title = "Lounge:
\n"; # } elsif ($line =~ m@^<[BI]>With DJs?:@si && !$para2title) { # $para2title = "With DJs:\n

\n"; # } elsif ($line =~ m@^\s*<(BAND|DJ|OTHER)@si) { # if ($para2title || $para2) { # $para2 .= "{$line}\n"; # } else { # $para1 .= "{$line}\n"; # } # } elsif (! ($para1title || $para1 || $para2title || $para2)) { # $prefix .= "[$line]\n"; # } else { # $suffix .= "[$line]\n"; # } # } # $para1title =~ s/\n//gsi; # $para2title =~ s/\n//gsi; # $para1 =~ s/\s*$//si; # $para2 =~ s/\s*$//si; # $prefix =~ s/\s*$//si; # $suffix =~ s/\s*$//si; # $extra =~ s/\s*$//si; # $para1 =~ s/^/\t/gmi if $para1; # $para2 =~ s/^/\t/gmi if $para2; # $prefix =~ s/\n/\n\t/gsi; # $suffix =~ s/\n/\n\t/gsi; # $extra =~ s/\n/\n\t/gsi; # $para1 .= "\n" if $para1; # $para2 .= "\n" if $para2; # print STDERR ("################### $date: $name\n" . # "PRE:\t[$prefix]\n" . # "P1:\t[$para1title]\n$para1" . # "P2:\t[$para2title]\n$para2" . # "SUF:\t[$suffix]\n" . # ($extra ? "EXTRA:\t$extra\n" : "")); } # Returns the HTML for a single calendar event. # sub build_event_html($$$) { my ($event, $future_p, $tickets_form_p) = @_; # simplify_event($event); my $html = $$event->{html_src}; my $date = $$event->{date}; my $times = $$event->{times}; my $holiday = $$event->{holiday}; my $repeat = $$event->{repeat} || ''; my $flyer = $$event->{flyer} || ''; my $photo = $$event->{photos} || ''; my $video = $$event->{video} || ''; # { # my $tickets = $$event->{tickets}; # foreach my $t (@$tickets) { # my $f = $flyer; # $f =~ s@\.html$@-1-thumb.jpg@s; # $f = "${url_base}$f" if $f; # my ($ticket, $desc, $onsale, $offsale, $vip_p) = @$t; # if ($ticket !~ m@^https://cart\.dnalounge\.com/@si) { # print STDERR "# $ticket - $date\n"; # } else { # $ticket =~ s@^.*=@@s; # print STDERR "update items set thumbnail_image='$f' where item_id=$ticket;\n"; # } # } # } if ($tickets_form_p == 2) { # this means "allow advance ticket sales" $tickets_form_p = 0; } my ($year, $month, $dotm) = @$times; my $dotw = dotw ($dotm, $month, $year); error ("$date: <$1> tag in body") if ($html =~ m@^\s*.*\n\n.*{day_ord} && $$event->{day_ord}[1] > 1) { $anchor .= chr (ord('a') + $$event->{day_ord}[0]); } $date_html = "" . "$date_html"; if (defined ($holiday)) { $holiday =~ s/^(.*?):\s+(.*?)$/$1\n($2)/si; $date_html .= "\n\n$holiday"; } $date_html =~ s/\n/
/gs; my ($col0, $pres, $title, $stats, $genre, $time, $age, $price, $tickets, $lineup, $blurb) = ('', '', '', '', '', '', '', '', '', '', ''); # Parse out all the junk in the first column. # if ($html =~ m/^(.*?)\n\n(.*)$/s) { ($col0, $html) = ($1, $2); } else { $col0 = $html; $html = ''; } $pres = $3 if ($col0 =~ s@(<(PRES ) [^<>]*> (.*?) )@@six); $title = $1 if ($col0 =~ s@(<(ETITLE ) [^<>]*> (.*?) )@@six); $stats = $3 if ($col0 =~ s@(<(STATS ) [^<>]*> (.*?) )@@six); $genre = $3 if ($stats =~ s@(<(GENRE ) [^<>]*> (.*?) )@@six); $time = $3 if ($stats =~ s@(<(TIME ) [^<>]*> (.*?) )@@six); $age = $3 if ($stats =~ s@(<(AGE ) [^<>]*> (.*?) )@@six); $price = $3 if ($stats =~ s@(<(PRICE ) [^<>]*> (.*?) )@@six); $tickets = $1 if ($stats =~ s@((]*> \s* (<(P|BR)>\s*)*)+)@@six); $blurb = $3 if ($html =~ s@(<(BLURB ) [^<>]*> (.*?) )@@six); # col1. if ($title =~ m@HREF=@s) { # ... $title =~ s@\bETITLE\b@A@gs; } else { # ... $title =~ s@]*>@@gs; } $col0 =~ s/<(P|BR)>\s*/\n/g; error ("$date: extra junk in column 0: $col0") unless ($col0 =~ m/^\s*$/s); $stats =~ s@^<(STATS)>(.*)$@$2@si; $stats =~ s@<(FLYER|EVENT)[^<>]*>@@gsi; # discard $stats =~ s@<(P|BR)>\s*@\n@g; error ("$date: extra junk in STATS: $stats") unless ($stats =~ m/^\s*$/s); $html =~ s@<(VIDEO|LIVE|WEBCAST)[^<>]*>@@gsi; # discard $lineup = rewrite_dj_tags ($date, $html); $html = undef; $tickets = rewrite_tickets_tags ($event, $tickets, $future_p, $date, $tickets_form_p) if ($tickets); if ($video) { # # At 150+ px wide, the scrollbar is shown. # At 240+ px wide, the fullscreen button is shown. # At 320+ px wide, the options button is shown. # Control area is 25 pixels high. # my $w = 240; my $h = $w / (16 / 9) + 25; # 160 # my $h = $w / (4 / 3) + 25; # 205 my $videoname = $$event->{videoname}; $video = make_embed_tag ($video, $videoname, $w, $h); } $photo = "[ photos ]" if $photo; $flyer = find_inline_flyer ($flyer) if ($flyer); my $share = make_event_id_links ($event, $future_p) || ''; my $fblike = make_fblike_link ($event); $time =~ s/(&[a-z\d]+;)/$1\001/gsi; # kludge to protect entities. $price =~ s/(&[a-z\d]+;)/$1\001/gsi; $time =~ s/; /;
/gs; # break after semicolons. $price =~ s/; /;
/gs; $time =~ s/\001//gs; $price =~ s/\001//gs; $title =~ s/: /:
/gs; # break after colons. $title =~ s@(:)(
.*?)()@$1$3$2@gsi; # Leave subtitle out of link. if (!$age) { } elsif ($age eq 'AA') { $age = "all ages."; } elsif ($age eq '18') { $age = "$age+."; } else { $age = "$age+."; } if ($time) { # hcalendar microformat nonsense. Using is required for the # date fields, but we don't have 3 easily-parsable pieces of text to # hang the fields on. Also, that causes there to be an annoying # tooltip over whatever text we do wrap in . # # So, use null text inside the and hope that works, even # though the hcalendar validator warns about it. # # Also, I'm not sure this is a legal usage of "tzid" here. # my ($ical_start, $ical_duration, $tz) = ical_dtstart ($event); $time = ("" . "" . "" . $time); } my $awards = find_awards ($pres, $title, 0) || ''; my $groups = find_awards ($pres, $title, 1) || ''; $date_html = "

$date_html
"; $photo = "
$photo
" if ($photo); $repeat = "
$repeat
" if ($repeat); $pres = "
$pres
" if ($pres); $title = "
$title
"; $genre = "
$genre
" if ($genre); $time = "
$time
" if ($time); $age = "
$age
" if ($age); $price = "
$price
" if ($price); $tickets = "
$tickets
" if ($tickets); $awards = "
$awards
" if ($awards); $share = "
$share
" if ($share); $fblike = "
$fblike
" if ($fblike); # Make the first N characters of the blurb unwrappable, so that we don't # end up with ugly wrapping around the floated video (e.g, "The" being # to the left of the video and "Bandname" being pushed down below it # because it is too wide.) This causes it to push the whole thing down # if the space is narrow. # $blurb =~ s@^\s*([^<>]{12}[^<>\s]*)\s*@$1\n@s; # Append the "Join the Facebook group" stuff to the blurb, if there is one. # $blurb .= "\n

$groups" if ($blurb && $groups); my $event_output = ("

\n" . # hcalendar "
\n" . $date_html . $photo . $flyer . $repeat . $fblike . $awards . "
\n" . "
\n" . "
\n" . # hcalendar "
\n" . "
" . # hcalendar $pres . $title . "
" . $genre . $time . $age . $price . $tickets . $share . "
\n" . "
\n" . $lineup . "
\n" . "
\n" . $video . $blurb . "
\n" . "
\n" . "
\n\n"); $event_output = clean_html ($date, $event_output); # one last time return $event_output; } # Returns the plain text for a single calendar event. # sub build_event_text($$$$$$$) { my ($event, $date_p, $future_p, $infoline_p, $long_lines_p, $day_prefix, $day_suffix) = @_; my $name = $$event->{title}; my $html = $$event->{html_src}; my $date = $$event->{date}; my $holiday = $$event->{holiday}; my $flyer = $$event->{flyer}; my $photo = $$event->{photos}; my $times = $$event->{times}; my $tickets = $$event->{tickets}; my $embed = $$event->{video}; my ($year, $month, $dotm) = @$times; my $output = ""; my $do_urls_p = !$infoline_p; my $m = $months[$month-1]; $m =~ s/^(...).*/$1/ unless ($infoline_p); my $blurb = undef; if ($html =~ s@\s*\s*(.*?)\s*\s*(

\s*)?@@si) { $blurb = $1; my $pres = $3 if ($html =~ m@(<(PRES ) [^<>]*> (.*?) )@six); my $title = $3 if ($html =~ m@(<(ETITLE ) [^<>]*> (.*?) )@six); my $groups = find_awards ($pres, $title, 1) || ''; $blurb .= "\n

$groups" if ($groups); } $blurb = undef if ($infoline_p); $html =~ s@(]*>)(.*?)() @{ $1 . ($infoline_p ? $2 : upcase_html($2)) . $3 }@xeigs; $html =~ s@<(STRIKE)>.*?@@gsi; # omit this! $html = reformat_infoline ($html) if ($infoline_p); my ($head, $tail) = ($html =~ m/^(.*?)\n\n+(.*?)\s*$/s); $head = $html unless ($head); $tail = '' unless ($tail); my $reformat_p = ($tail =~ m/^\s*(<[^>]*>\s*)*Performing live:/si); $_ = $date; # the dotw desc (e.g., "Sunday Night") is my ($d) = m/^([^,]+),/; # the first set of words before the comma. $d = "$day_prefix $d" if (defined ($day_prefix)); $d = "$d -- $day_suffix" if (defined ($day_suffix)); if (defined($holiday) && !$infoline_p) { $holiday =~ s@
@ @gi; $holiday =~ s@<[^<>]+>@@g; # strip any tags in holiday text $d .= " -- $holiday"; } # slightly rewrite if there's a day suffix and a holiday. $d =~ s/^(.*) -- (.*) -- (.*)$/$1 -- $2, $3/; if ($infoline_p) { $output .= sprintf("\n$d, $m $dotm.\n\n"); } elsif ($date_p) { $output .= "\n" . ('-' x 72) . "\n\n"; # $fill_column ? $output .= sprintf("%02d $m $year ($d)\n\n", $dotm); } my @event_urls = ($infoline_p ? () : find_event_urls ($head, 0)); # throw away everything but the STATS section if reformatting. $head =~ s@^.*(.*?).*$@$1@gsi if ($reformat_p); # Kludge for the Facebook URLs in the blurb: # $blurb =~ s@"]+)"> (.*?) [-.!\s]* [-.!\s]* @$2: $1\n@gsix if ($blurb); $head = de_htmlify ($head, 0); $tail = de_htmlify ($tail, $do_urls_p); $blurb = de_htmlify ($blurb, 0) if ($blurb); if (! $infoline_p) { $tail = indentify ($tail, $long_lines_p); # Blah. $tail =~ s/([ \t]--)[ \t]*\n[ \t]+/$1 /gs; $head =~ s/\n\n+/\n/sg; # delete blank lines # but put one back after capwords (including cap acronyms and posessives.) $head =~ s/([[:upper:]][.\']?[[:upper:]] [ \t]*[[:upper:]\d]* [ \t]*[.,+&]?:?)$/$1\n/xmg; # Total kludge for a night with a single character name... $head =~ s/^(Q)$/$1\n/mg; } # put some URLs between the head and the tail. # the flyer URL always come first. if (! $infoline_p) { my $flyer = $$event->{flyer}; if ($flyer) { $flyer = "$url_base$flyer"; unshift @event_urls, $flyer; } push @event_urls, $embed if ($embed); } if ($reformat_p) { #$head =~ s/^/ /gm; $tail .= "\n\n$head"; $head = ''; } if ($#event_urls >= 0) { my $u = ''; # only include each URL once # (in case the event URL is the same as one of the band URLs.) foreach (@event_urls) { my $re = qr/$_/; $u .= "$_\n" unless ($tail =~ m/$re/i); } if ($reformat_p) { $tail = "$tail\n\n$u"; } else { $tail = "$u\n\n$tail"; } } if ($future_p && !$infoline_p) { if ($tickets) { my $ticket_txt = ""; my $vip_txt = ""; my $ticket_count = 0; my $first_onsale_date = undef; foreach my $t (@$tickets) { my ($ticket, $desc, $onsale, $offsale, $vip_p) = @$t; my $off_p = ticket_off_sale_p ($event, $onsale, $offsale); if ($off_p < 0) { # not on sale yet my ($dd, $mm, $yyyy) = ($onsale =~ m/(\d\d?)-([a-z]{3})-(\d\d\d\d)/si); $mm =~ s/^(.)/{uc($1)}/xe; $first_onsale_date = "$mm $dd" unless defined($first_onsale_date || $ticket_txt); } elsif ($off_p > 0) { # no longer on sale } elsif ($vip_p) { $vip_txt = ("VIP Service:" . ((length ($ticket) > 59) ? "\n" : " ") . "$ticket\n"); } elsif ($desc) { $desc = de_entify($desc); $ticket_txt .= "\n $desc: $ticket"; } else { $ticket_txt .= ((length ($ticket) > 59) ? "\n" : " "); $ticket_txt .= "$ticket\n"; } } if ($ticket_txt) { $ticket_txt = "Tickets:$ticket_txt"; } elsif ($first_onsale_date) { $ticket_txt = "Tickets on sale $first_onsale_date."; } $ticket_txt .= ($ticket_txt ? "\n\n" : "") . $vip_txt; $tail .= "\n\n$ticket_txt" if $ticket_txt; } } $html = "$head\n\n$tail"; # put linebreaks after URLs not at end of line # $html =~ s@(https?:[^\s]+)([ \t]+)(.+)$@$1\n$3@gm; $html =~ s/^(https?)/ $1/mg; # Indented URLs right before non-indented "Tickets:" lines look funny. # 1 while ($html =~ s/^ +(http[^\s]+\n)\n*(http:|Tickets:)/$1$2/m); # Re-wrap the blurb text, and tack it on the end. # if ($blurb) { $Text::Wrap::columns = ($long_lines_p ? 1024 : 65); $Text::Wrap::huge = 'wrap'; my @paras = split(/\n *\n+/, $blurb); foreach my $p (@paras) { $p = wrap ('', '', $p); } $blurb = join ("\n\n", @paras); $html .= "\n\n$blurb"; } $html =~ s/^/ /mg; $html =~ s/\s+$//s; $html =~ s/[ \t]+$//gm; $html =~ s/(\n\n)\n+/$1/gs; # compress multiple blank lines $html =~ s/^\n+//s; $html =~ s/\n+$//s; $output .= $html; $output .= "\n"; my @lines = split (/\n/, $output); if (! $long_lines_p) { foreach my $line (@lines) { # if line is longer than 72 characters, strip spaces after/before "--" 1 while ($line =~ m/^.{$fill_column}/s && $line =~ s/(-- ) /$1/s); 1 while ($line =~ m/^.{$fill_column}/s && $line =~ s/ ( --)/$1/s); # if line is still longer than 72 characters, wrap it. if ($line =~ m/^ (.{$fill_column})/s) { my ($pre) = ($line =~ m/^(\s+)/s); $Text::Wrap::columns = $fill_column; $Text::Wrap::huge = 'overflow'; $line = wrap ('', $pre, $line); } } } return join ("\n", @lines) . "\n"; } # Returns the crontab text for a single calendar event. # sub build_event_crontab($) { my ($event) = @_; my $title = $$event->{title}; my $webcast = $$event->{webcast}; return '' if ($webcast eq 'off'); my $times = $$event->{times}; my ($year, $month, $dotm, $dotw, $start_minute, $end_minute, $webcast_end_minute) = @$times; my $days_in_month = days_per_month ($month, $year); my $flyer = $$event->{flyer} || ''; $flyer =~ s@^flyers/@@s; $title = asciify ($title); my $length = $webcast_end_minute - $start_minute; my $shour = int($start_minute / 60); my $smin = int($start_minute % 60); $length = sprintf("%d:%02d", int($length / 60), int($length % 60)); my $ct = sprintf ("%2d %2d %2d %2d * ", $smin, $shour, $dotm, $month); $ct .= "LENGTH=$length FLYER=$flyer TITLE=\"$title\"\n"; # mktime: dotm: 1-31; month: 0-11; year: -1900; dotw: 0-6. # cron: dotm: 1-31; month: 1-12; year: n/a. dotw: 0-6; my $stime = mktime (0, $smin, $shour, $dotm, $month-1, $year-1900, 0, 0, -1); my $etime = $stime + (($webcast_end_minute - $start_minute) * 60); my ($esec, $emin, $ehour, $edotm, $emon, $eyear) = localtime ($etime); my $ct2 = sprintf ("%2d %2d %2d %2d * ", $emin, $ehour, $edotm, $emon+1); $ct2 .= "END\n"; $ct .= $ct2; if ($verbose > 3) { my $d = "$dotm-$months[$month-1]-$year"; my $s = sprintf("%d:%02d %s", ($shour == 0 ? 12 : $shour > 12 ? $shour - 12 : $shour), $smin, ($shour < 12 ? "AM" : "PM")); $_ = $length; my ($lh, $lm) = m/^(\d\d?):(\d\d)$/; my $ee = ($shour * 60) + $smin + ($lh * 60) + $lm; my $ehour = int ($ee / 60) % 24; my $emin = int ($ee % 60); my $e = sprintf("%d:%02d %s", ($ehour == 0 ? 12 : $ehour > 12 ? $ehour - 12 : $ehour), $emin, ($ehour < 12 ? "AM" : "PM")); $_ = $ct; s/^/$progname: crontab: /gm; s/\n$/ ($d: $s - $e)\n/s; print STDERR "$_"; } return $ct; } # Constructs files describing the given month. Returns: # - the body of an HTML document; # - the body of a text document. # If $summarize_only_p, then only generates the @future_events list. # sub build_month_bodies($$$$$$) { my ($dir, $year, $month, $prev_p, $next_p, $summarize_only_p) = @_; my $html = ""; my $text = ""; my ($csec, $cmin, $chour, $cdotm, $cmon, $cyear) = localtime; $cmon++; $cyear += 1900; my $now_i = ($cyear * 100000000 + $cmon * 1000000 + $cdotm * 10000 + $chour * 100 + $cmin); my $mon_i = ($year * 100000000 + $month * 1000000 + 32 * 10000); ###################### # Generate html header ###################### $html .= "$page_title: $months[$month-1] $year\n"; my ($header, $prev, $next, $prev_name, $next_name) = build_cal_header ($year, $month, $prev_p, $next_p); my $grid = make_calendar_grid_html ($dir, $year, $month, $prev, $next); my $links = ''; $links .= (" \n" . " \n" . " \n" . " \n" . " \n" . $xml_link_tag); $links .= " \n" if ($prev); $links .= " \n" if ($next); $html .= $links; $html .= ("\n" . "$header\n" . "\n" . "\n" . "

\n" . $grid . "
\n" . "\n" . "\n" ); # update the "d30" IDs in the CSS, in the same manner as # calendar/update-calboxes.pl... # { my ($csec, $cmin, $chour, $cdotm, $cmon, $cyear) = localtime; $cmon++; $cyear += 1900; my $dd = (($cmon == $month && $cyear == $year) ? $cdotm : 0); $html =~ s/^( *(\.[a-z\d]+)?#d)\d\d?\b/$1$dd/gm; } ############################ # Generate plain text header ############################ my $hr = center_line('-' x 24) . "\n"; $text .= "$hr\n"; $text .= center_line($page_title) . "\n"; $text .= center_line($months[$month-1] . " $year") . "\n"; $text .= "\n$hr\n"; $text .= center_line(sprintf ("${url_base}calendar/$year/%02d.html", $month)) . "\n"; ################################ # Loop over body events for both ################################ my $days = days_per_month ($month, $year); my @future_events_1 = (); # the event just before the "today" cutoff time my $month_yesterday_event = undef; # Parse the "--only" option, if any. # my @target_date; my $found_target_p = 0; if ($summarize_only_p ne 0 && $summarize_only_p ne 1) { my ($dd, $mmm, $yyyy, $two) = ($summarize_only_p =~ m/^(\d\d?)[-\s]+([a-z]{3}[a-z]*)[-\s]+(\d{4})\s*(#2)?$/si); error ("unparsable --only \"$summarize_only_p\"") unless $yyyy; @target_date = ($dd+0, $monthvals{lc($mmm)}, $yyyy+0, ($two ? 2 : undef)); } for (my $dotm = 1; $dotm <= $days; $dotm++) { my $key = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); my $listref = $calendar{$key}; my @list = (); if ($target_date[0]) { # --only was specified if ($dotm == $target_date[0] && $month == $target_date[1] && $year == $target_date[2]) { $found_target_p = 1; } else { next; } } my $dotw = dotw ($dotm, $month, $year); if (defined ($listref)) { @list = @$listref; # Kludge: If this event just says "closed", then this is just there to # shadow a repeating event that would otherwise show up here. E.g., # when there's an "every thu" event but one specific day is dark. # my $e0 = $list[0]; my $title = $$e0->{title}; if (defined ($title) && $title =~ m/^\(?CLOSED|SUSPENDED\)?$/si) { next; } } else { my ($dotw_number, $total_dotws) = dotw_count ($year, $month, $dotm); my $repeater = lookup_repeating_entry ($year, $month, $dotm, $dotw, $dotw_number, $total_dotws); @list = (\$repeater) if (defined ($repeater)); } next if ($#list < 0); my $event_number = 1; foreach my $event (@list) { my $times = $$event->{times}; my ($igy, $igm, $igdm, $igdw, $start_minute) = @$times; my $hour = int ($start_minute / 60); my $min = int ($start_minute % 60); # Events go on @future_events if the event starts today or later. # That controls what goes on the front page. # my $event_i = $year * 100000000 + $month * 1000000 + ($dotm+1) * 10000; my $future_p = ($event_i >= $now_i); push @future_events_1, $event if ($future_p); $month_yesterday_event = $event # save the last "past" event unless ($future_p); # Found our --only date. if ($found_target_p && ($target_date[3] ? $event_number == 2 : 1)) { print STDOUT build_event_html ($event, $future_p, 1); return (); } if (!$summarize_only_p) { $html .= build_event_html ($event, $future_p, 1); $text .= build_event_text ($event, 1, $future_p, 0, 0, undef, undef); } $event_number++; } } # put this together backwards, since the order in which we generate # the calendar pages is later months to earlier months. # (the order is Dec01 ... Dec31, Nov01 .. Nov31, etc.) # unshift @future_events, @future_events_1; $yesterday_event = $month_yesterday_event unless ($yesterday_event); # Only needed to compute future_events in --summarize-only mode, # don't need to finish constructing the page. return () if ($summarize_only_p); ###################### # Generate html footer ###################### $html .= "\n
\n" . "$header\n
\n"; $html .= "

\n"; $html .= "\n"; ###################### # Generate text footer ###################### $text .= "\n" . ('-' x 72) . "\n"; # $fill_column ? $text =~ s/ +$//gm; if ($text =~ m/(&[a-z]+;)/i) { error ("$year-$month: stray entity: \"$1\""); } return ($html, $text); } # Writes files describing the given month: # # - calendar/YYYY/MM.html # - calendar/YYYY/MM.txt # # If $summarize_only_p, doesn't actually write any files (but does some # other necessary work for the tickets pages, etc.) # sub write_month_bodies($$$$$$$) { my ($dir, $year, $month, $outfile, $prev_p, $next_p, $summarize_only_p) = @_; my ($html, $text) = build_month_bodies ($dir, $year, $month, $prev_p, $next_p, $summarize_only_p); return if ($summarize_only_p); write_file_if_changed ($outfile, $html, 1); $outfile =~ s/\.html$/.txt/ || error ("no text version of $outfile?"); write_file_if_changed ($outfile, $text, 0); } # Returns a one liner describing this event, if there are or # tags present. Plaintext. # sub event_live_show_summary($) { my ($event) = @_; my $bands = compute_band_based_title ($event, 0, 0); return "" if (!defined ($bands)); $bands = de_entify ($bands); my $times = $$event->{times}; my ($year, $month, $dotm, $dotw) = @$times; $month = $months[$month-1]; $month =~ s/^(...).*/$1/; $dotw = $days[$dotw]; $dotw =~ s/^(...).*$/$1/; my $date = sprintf ("%s, %s %02d", $dotw, $month, $dotm); return "$date -- $bands\n"; } # If this event has or tag, return a comma-separeted list # off all those bands. Else undef. # sub compute_band_based_title($$$) { my ($event, $only_one_p, $event_first_p) = @_; my $result = undef; my $html = $$event->{html_src}; if ($html =~ m/]+)\")?/) { my $t = $2; if (defined ($t)) { $t =~ s@\\n(.*)$@
$1@si; # second line of title in italics $result = $t; } else { $result = $$event->{title}; } } if (! $result) { $_ = $html; s/\s+/ /gs; s/(]*>(.*?)@si) { my $b = $2; $b =~ s/\s+/ /gs; $b =~ s/^\s//gs; $b =~ s/\s$//gs; $b = de_entify ($b); push @bands, $b; last if ($only_one_p); } } if ($#bands >= 0) { $result = join (", ", @bands); # If the event description doesn't begin with "Performing live:" then # this is probably a band playing at a club night, so mention the event # name too. # my ($body) = ($$event->{html_src} =~ m/^.*?\n\n(.*)$/s); my $traditional_live_show = ($body =~ m/^\s*(<[^>]*>\s*)*Performing live:/si); my $mention_event_name = !$traditional_live_show; # Dept. of Redundancies Dept. my $re = qr/$$event->{title}/; $mention_event_name = 0 if ($result =~ m/$re/si); if ($mention_event_name) { my $t = $$event->{title}; $t =~ s/: .*$//s; # lose subtitle after colon if ($event_first_p) { $result = "$t (with $result)"; } else { $result = "$result (at $t)"; } } } } if (! $event_first_p) { # whether this is "Next event:" #### Kludge: no need to front-page-hype a band who plays here twice a month. return undef if ($result && $result =~ m/Smash-Up Derby \(at Bootie/si); } #### Kludge: Smash-Up is at Bootie all the time. Omit. $result =~ s/^(Bootie\b.*) \(with Smash-Up Derby\)$/$1/si if $result; return $result; } # Returns the subset of @future_events that have tickets for sale, # or that are live bands, or both. # sub ticket_events($$) { my ($bands_p, $tickets_p) = @_; my @result = (); foreach my $event (@future_events) { if ($bands_p && $$event->{html_src} =~ m/<(BAND|LIVE)/i) { push @result, $event; } else { my $tix_p = 0; my $tickets = $$event->{tickets}; if ($tickets) { foreach my $t (@$tickets) { my ($ticket, $desc, $onsale, $offsale, $vip_p) = @$t; $tix_p = 1 unless $vip_p; # VIP tickets only don't count. } } push @result, $event if ($tickets_p && $tix_p); } } return @result; } # Find the %%LEFT%% text from the store pages (the table that has links # to the shopping cart and order status pages) so that we can have those # links on the tickets/index.html page without having to duplicate that # HTML here too. # sub load_store_blurb($) { my ($dir) = @_; my $file = "$dir/$store_file"; 1 while $file =~ s@[^/]+/\.\./@@g; local *IN; my $body = ''; open (IN, "<$file") || error ("$file: $!"); print STDERR "$progname: reading $file\n" if ($verbose > 2); while () { $body .= $_; } close IN; my ($blurb) = ($body =~ m@(.*?)@s); $blurb = '' unless defined ($blurb); $blurb =~ s/^\s+//s; $blurb =~ s/\s+$//s; error ("no %%LEFT%% in $file") unless ($blurb =~ m/./); $blurb .= "\n"; return $blurb; } # Loads the template file and constructs our etc from it. # This is used for the overview.html and upcoming.html files. # sub load_template() { local *IN; $body_template = ''; open (IN, "<$template_file") || error ("$template_file: $!"); print STDERR "$progname: reading $template_file\n" if ($verbose > 3); while () { $body_template .= $_; } close IN; # lose everything inside $body_template =~ s@(]*>).*(.*)$@$1\n $2@si; $body_template =~ s@^\s*\s*\n@@gmi; # bleh. $body_template =~ s@%%ROOT%%@../@gs; } # Write the tickets/index.html file. # sub generate_tickets_html($) { my ($dir) = @_; my @tickets = ticket_events (0, 1); # tickets only (not bands too) my @rcnt_html; my @live_html; my @other_html; foreach my $event (@tickets) { my $title = $$event->{title}; my $times = $$event->{times}; my $eband_p = ($$event->{html_src} =~ m/<(BAND|LIVE)\b/i); my $date = $$event->{date}; my $tickets = $$event->{tickets}; my ($year, $month, $dotm) = @$times; my $url = sprintf ("../calendar/%04d/%02d.html#%02d", $year, $month, $dotm); $date =~ s/^([a-z]{3})[a-z]* (\s+[a-z]+)? ,? \s (\d\d?) \s ([a-z]{3}) [a-z]* \s \d{4} $ /$1, $4 $3/six; if ($eband_p) { my $t2 = compute_band_based_title ($event, 0, 0); if ($t2) { $title = $t2; } else { $eband_p = 0; # might have decided this one isn't worth listing. } } $title =~ s/&/&/gs; $title =~ s//>/gs; my $d = '

'; if ($eband_p && $title =~ s@^([^,()]+),\s*(.*)$@$d$1
with $2@si) { } elsif ($eband_p && $title =~ s@^([^,()]+)\s*\((.*)\)$@$d$1
$2@si) { } else { $title = "$d$title"; } my $html = ("
\n" . "
" . "$date:" . "
\n" . "
$title
\n" . "
\n\n"); my $any_this_week_p = 0; foreach my $t (@$tickets) { my ($ticket, $desc, $onsale, $offsale, $vip_p) = @$t; if (!$onsale || ticket_on_sale_this_week_p ($onsale, 14)) { $any_this_week_p = 1; } } if ($any_this_week_p) { push @rcnt_html, $html; } if ($eband_p) { push @live_html, $html; } else { push @other_html, $html; } } my $rcnt_html1 = join('', @rcnt_html[0 .. $#rcnt_html/2]); my $rcnt_html2 = join('', @rcnt_html[$#rcnt_html/2+1 .. $#rcnt_html]); my $live_html1 = join('', @live_html[0 .. $#live_html/2]); my $live_html2 = join('', @live_html[$#live_html/2+1 .. $#live_html]); my $other_html1 = join('', @other_html[0 .. $#other_html/2]); my $other_html2 = join('', @other_html[$#other_html/2+1 .. $#other_html]); if ($#rcnt_html < 4) { $rcnt_html1 .= $rcnt_html2; $rcnt_html2 = ''; } if ($#live_html < 4) { $live_html1 .= $live_html2; $live_html2 = ''; } if ($#other_html < 4) { $other_html1 .= $other_html2; $other_html2 = ''; } my $output = ("$page_title: Tickets\n" . "\n" . "\n" . "\n" . "\n" . "\n" . "\n" . load_store_blurb ($dir) . "\n" . "\n" . "
\n" . ($rcnt_html1 ? ("

\n" . "\n" . "Recently Announced\n" . "\n" . "

\n\n" . $rcnt_html1 . "
\n" . "
\n\n" . $rcnt_html2 . "
\n\n") : "") . ($live_html1 ? ("

\n" . "\n" . "Upcoming Live Shows\n" . "\n" . "

\n\n" . $live_html1 . "
\n" . "
\n\n" . $live_html2 . "
\n\n") : "") . ($other_html1 ? ("

\n" . "\n" . "Tickets Also Available for these DJ Events\n" . "\n" . $tickets_vip_blurb . "

\n\n" . $other_html1 . "
\n" . "
\n\n" . $other_html2 . "
\n\n") : "") . "
\n" . "\n"); # Since this HTML was generated assuming it would be sitting in # "calendar/YYYY/MM.html", and we're putting it in "tickets/index.html", # we have one too many ".." links in the relative URLs. Take one off. # # $output =~ s@(\"\.\./)(\.\./)@$1@sg; my $outfile = "$dir/$calendar_tickets_file"; 1 while $outfile =~ s@[^/]+/\.\./@@g; write_file_if_changed ($outfile, $output, 1); } sub generate_calendar_upcoming_1($$$$$$$$) { my ($dir, $max_events, $bands_p, $tickets_p, $list_all_bands_p, $list_next_p, $thisweek_p, $ticket_form_p) = @_; my $output = ""; my @events = ticket_events ($bands_p, $tickets_p); # Get the next upcoming event, but skip any private/cancelled events. # my $next_event = undef; my $next_event_2 = undef; for (my $i = 0; $i <= $#future_events; $i++) { $next_event = $future_events[$i]; $next_event_2 = $future_events[$i+1]; # $next_event_2 is only set if there are two events on the same day. if ($next_event_2 && ($$next_event_2->{times}[0] ne $$next_event->{times}[0] || $$next_event_2->{times}[1] ne $$next_event->{times}[1] || $$next_event_2->{times}[2] ne $$next_event->{times}[2])) { $next_event_2 = undef; } # If our next event is a pair of events on the same day, but event #1 # is already over, throw it away and use only event #2. # if ($next_event_2) { my $times = $$next_event->{times}; my ($year, $month, $dotm, $dotw, $start_minute, $end_minute, $webcast_end_minute) = @$times; my $shour = int($start_minute / 60); my $smin = int($start_minute % 60); my $stime = mktime (0, $smin, $shour, $dotm, $month-1, $year-1900, 0, 0, -1); my $etime = $stime + (($end_minute - $start_minute) * 60); my $now = time(); if ($now >= $etime) { shift @events; $next_event = $next_event_2; $next_event_2 = undef; } } my $title = $$next_event->{title}; if ($title =~ m/\b(PRIVATE\s+(PARTY|EVENT)|CANCELL?ED|POSTPONED|CLOSED|SUSPENDED)\b/i) { $next_event = undef; print STDERR "$progname: skipping private: $title\n" if ($verbose > 2); } if ($next_event_2) { $title = $$next_event->{title}; if ($title =~ m/\b(PRIVATE\s+(PARTY|EVENT)|CANCELL?ED|POSTPONED|CLOSED|SUSPENDED)\b/i) { $next_event_2 = undef; print STDERR "$progname: skipping private: $title\n" if ($verbose > 2); } } if (defined($next_event_2) && !defined($next_event)) { $next_event = $next_event_2; $next_event_2 = undef; } last if (defined ($next_event)); } if (!$list_next_p) { $next_event = undef; $next_event_2 = undef; } if ($list_next_p) { unshift @events, $next_event_2 unless (!defined ($next_event_2) || $events[0] eq $next_event_2 || $events[1] eq $next_event_2); unshift @events, $next_event unless ($events[0] eq $next_event || $events[1] eq $next_event); } my $thisweek_twit = ''; if ($#events >= 0) { my $body = ""; my $heading_inserted_p = 0; my $next_inserted_p = 0; my $thisweek_html = ''; my $output_count = 0; foreach my $event (@events) { my $next_p = ($next_event && $event eq $next_event); my $next2_p = ($next_event_2 && $event eq $next_event_2); my $title = $$event->{title}; my $times = $$event->{times}; my $flyer = $$event->{flyer}; my $tickets = $$event->{tickets}; my $eband_p = ($$event->{html_src} =~ m/<(BAND|LIVE)\b/i); my $hype_p = ($$event->{html_src} =~ m/<(BAND|LIVE)\b[^<>]*\bHYPE\b/i); my $free_p = ($$event->{html_src} =~ m/\bFREE ADMISSION\b/i && !($$event->{html_src} =~ m/\$\d+\b/i)); my $out_p = ($$event->{html_src} =~ m/\bSOLD OUT\b/i); my $do_thisweek_p = 0; my $first_ticket = undef; foreach my $t (@$tickets) { my ($ticket, $desc, $onsale, $offsale, $vip_p) = @$t; if (!$first_ticket && !$vip_p && !ticket_off_sale_p ($event, $onsale, $offsale)) { $first_ticket = $ticket; if ($thisweek_p && (!$onsale || ticket_on_sale_this_week_p ($onsale, 7))) { $do_thisweek_p = 1; } } } if ($eband_p) { $title = compute_band_based_title ($event, !$list_all_bands_p, ($next_p || $next2_p)); next unless $title; # might have decided this one isn't worth listing. } # If we've emitted enough events, stop emitting. # But, if *any* upcoming event has the HYPE property, # emit that event even if it means we exceed the max. # Also emit any event that went on sale this week, if we're checking. # next if (!$hype_p && !$do_thisweek_p && $output_count >= $max_events); $output_count++; my ($year, $month, $dotm, $dotw) = @$times; $dotw = $days[$dotw]; $dotw =~ s/^(...).*$/$1/; $_ = $months[$month-1]; s/^(...).*$/$1/; my $date = sprintf("%s %s %02d", $dotw, $_, $dotm); $month = sprintf("%02d", $month); $dotm = sprintf("%02d", $dotm); my $cal_url = "$year/$month.html\#$dotm"; my $flyer_open = ($flyer ? "" : ""); my $flyer_close = ($flyer ? "" : ""); # # Build up the line for this event. # my $tr = " \n"; my $col1 = ''; if ($first_ticket) { $col1 = "(buy)"; if ($ticket_form_p && $first_ticket =~ m@/order/\?item=(\d+)$@s) { my $id = $1; $col1 = ("
\n" . " \n" . " \n" . "
"); } } elsif ($free_p) { $col1 = "-free-"; } elsif ($out_p) { $col1 = "sold out!"; } $tr .= " $col1"; my $ages = ''; if (!$list_all_bands_p) { # include ages $ages = $$event->{age} || ''; if ($ages) { $ages = ($ages eq 'AA' ? 'all ages' : $ages . '+'); $ages = "$ages"; $ages = "($ages)" unless ($title =~ m/\)\s*$/s); } } $title =~ s/&/&/gs; $title =~ s//>/gs; $title = "$flyer_open$title$flyer_close"; # Convert "Band (at Event)" to # Convert "Band (at Event)". $title =~ s@( \(.*?\))(()?
)@$2$1@si; $title .= " $ages"; $tr .= (" " . "  $date    " . "" . " " . "$title" . "\n" . " \n"); # If this is a ticket that went on sale this week, save it for # later so that we can move it to the top of the list. # if ($do_thisweek_p) { $thisweek_html .= $tr; my $turl = ($first_ticket || $cal_url); my $tt = compute_band_based_title ($event, 1, 0); my $td = $date; $td =~ s@^[a-z]+, @@si; $td =~ s@ 0@ @s; $tt =~ s@\s*\(.*\)@@s; $thisweek_twit .= "$td: $tt $turl "; next; } # # Insert the "Upcoming" headings into $body if it's now time. # if (!$heading_inserted_p && !($next_p || $next2_p)) { # Heading goes on its own line $body .= (" \n" . " " . "" . "Upcoming live shows:" . "" . "\n" . " \n"); $heading_inserted_p = 1; } if (!$next_inserted_p && ($next_p || $next2_p)) { $body .= (" " . "" . "Next event:" . "" . "\n"); $next_inserted_p = 1; } # # Finally, tack this line onto $body. # $body .= $tr; } if ($thisweek_html) { $body =~ s@(Upcoming live shows:)@
Other \l$1@si; $body = (" \n" . " " . "" . "Just announced:" . "" . "\n" . " \n" . $thisweek_html . $body); } $output .= $body; } if ($thisweek_twit) { $thisweek_twit =~ s/; *$//s; $thisweek_twit = "Just announced: $thisweek_twit\n"; } return ($output, $thisweek_twit); } # Write the calendar/upcoming.html file. # This file is later included in the top level /index.html file, # as the "Upcoming Live Shows" header box. # sub generate_calendar_upcoming($) { my ($dir) = @_; my $output = ''; $output .= ("\n" . "

\n" . "\n" . "

\n" . "\n"); my ($upcoming_html, $output_twit) = generate_calendar_upcoming_1 ($dir, 5, # $max_events 1, # $bands_p 0, # $tickets_p 0, # $list_all_bands_p 1, # $next_p 0, # $thisweek_p 0 # ticket_form_p - ugly. ); $output .= ($upcoming_html . " \n" . "
\n" . "Advance tickets for other events are " . "available here." . "
\n" . "\n" . "\n"); my $body = $body_template; my $title = "$page_title: Tickets"; $body =~ s@(]*>).*()@$1$title$2@s; $body =~ s@(]*>).*()@$1\n\n$output $2@s; my $outfile = "$dir/$calendar_upcoming_file"; 1 while $outfile =~ s@[^/]+/\.\./@@g; write_file_if_changed ($outfile, $body, 1); } # Write the calendar/overview.html file. # This file is later included in the top level /index.html file, # as the "Calendar Overview" sidebar. # sub generate_calendar_overview($) { my ($dir) = @_; my $output = ""; my $ov_width = 180; my $max_embeds = 2; $output .= ("\n" . "
\n" . "
\n"); $output .= ("\n" . "
\n" . "
Calendar Overview
\n"); my $count = 0; my $embed_html = ''; my $embed_count = 0; my %already_embedded; foreach my $event (@future_events) { my $title = $$event->{title}; my $times = $$event->{times}; my $flyer = $$event->{flyer}; my $embed = $$event->{video}; my $embedname = $$event->{videoname}; my ($year, $month, $dotm, $dotw) = @$times; $dotw = $days[$dotw]; $dotw =~ s/^(...).*$/$1/; $_ = $months[$month-1]; s/^(...).*$/$1/; my $date = "$dotw, $_ $dotm"; $month = sprintf("%02d", $month); $dotm = sprintf("%02d", $dotm); my $cal_url = "../calendar/$year/$month.html\#$dotm"; if ($$event->{day_ord} && $$event->{day_ord}[1] > 1) { $cal_url .= chr (ord('a') + $$event->{day_ord}[0]); } # Don't include private or cancelled events on the front page. if ($title =~ m/\b(PRIVATE\s+(PARTY|EVENT)|CANCELL?ED|POSTPONED|CLOSED|SUSPENDED)\b/i) { print STDERR "$progname: skipping private: $date: $title\n" if ($verbose > 2); next; } $title = entitify ($title); $title = "$title" if defined ($flyer); $output .= ("
\n" . " $date\n" . " $title\n" . "
\n"); last if (++$count >= $overview_max_links); # If this event has embedded video, save that to emit after the table. # if ($embed && $embed_count < $max_embeds) { my $w = $ov_width; my $h = $w / (16 / 9) + 25; # 126 # my $h = $w / (4 / 3) + 25; # 160 $embedname =~ s/\s*:.*$//s; # lose everything after colon $embedname =~ s/\s*,\s+\d+[-\s][a-z]+[-\s]\d+\s*$//si; # trailing date $embedname = "$date: $embedname"; if (! $already_embedded{$embed}) { $embed_html .= ("
" . # for Firefox... make_embed_tag ($embed, $embedname, $w, $h) . "" . "$embedname" . "
" . "

\n"); $embed_count++; } $already_embedded{$embed} = 1; } } $output .= ("

\n" . " iCal\n" . " " . "more >>\n" . "
\n" . "
\n"); if ($embed_count > 0) { $output .= "\n$embed_html"; } $output .= ("\n" . "\n" . "
\n" . "
\n" . "\n"); my $body = $body_template; my $title = "$page_title: Overview"; $body =~ s@(]*>).*()@$1$title$2@s; $body =~ s@(]*>).*()@$1\n\n$output $2@s; my $outfile = "$dir/$calendar_overview_file"; write_file_if_changed ($outfile, $body, 1); } # Write the calendar/dnalounge.rss file, for XML/RSS syndication. # sub generate_calendar_rss($) { my ($dir) = @_; my $output = ""; $output .= ("\n" . "\n" . " \n" . " $rss_channel_title\n" . " $rss_channel_url\n" . " \n" . " $rss_channel_desc\n" . " \n" . " $rss_channel_lang\n" . " webmaster\@dnalounge.com (DNA Lounge)" . "\n" . " webmaster\@dnalounge.com (DNA Lounge)" . "\n" . " \n" . #### these are optional # Tue, 09 Dec 2008 03:40:19 GMT # Tue, 09 Dec 2008 03:40:19 GMT " \n" . " $rss_channel_title\n" . " $rss_logo_url\n" . " $rss_channel_url\n" . " $rss_logo_width\n" . " $rss_logo_height\n" . " $rss_channel_title\n" . " \n"); my @events = @future_events; unshift @events, $yesterday_event if ($yesterday_event); my %guids_used; my $count = 0; foreach my $event (@events) { my $title = $$event->{title}; my $times = $$event->{times}; my $flyer = $$event->{flyer}; my ($year, $month, $dotm, $dotw, $start_minute) = @$times; # Guids can't be duplicated, so when we have two events using the same # flyer, we can only use the flyer as the guid for the first one; we # need to use the calendar as the guid for subsequent ones. # if ($flyer) { if (defined($guids_used{$flyer})) { $flyer = undef; } else { $guids_used{$flyer} = 1; } } $dotw = $days[$dotw]; $dotw =~ s/^(...).*$/$1/; $_ = $months[$month-1]; s/^(...).*$/$1/; my $date = "$_ $dotm ($dotw)"; # Don't include private or cancelled events in the RSS file. if ($title =~ m/\b(PRIVATE\s+(PARTY|EVENT)|CANCELL?ED|POSTPONED|CLOSED|SUSPENDED)\b/i && ($event ne $yesterday_event)) { print STDERR "$progname: skipping private: $date: $title\n" if ($verbose > 2); next; } $month = sprintf("%02d", $month); $dotm = sprintf("%02d", $dotm); my $cal_url = "calendar/$year/$month.html\#$dotm"; my $ititle = "$date: $title"; my $url = $url_base . ($flyer ? $flyer : $cal_url); # Guids can't be duplicated, so when we have two events using the same # date, tack a "b" onto the second one (even though that URL isn't # quite accurate). This won't happen if the event has a flyer. # $url .= "b" if (defined($guids_used{$url})); $guids_used{$url} = 1; my ($text, $html); if (defined ($$event->{html_src})) { $html = build_event_html ($event, 1, 0); $text = build_event_text ($event, 0, 1, 0, 1, undef, undef); $html = decolorize_html ($html); $html = hardcode_stylesheet ($dir, $html); # Also, "../vip.html" => "..../calendar/vip.html" $html =~ s@\"\.\./([^/\"]+\")@\"${url_base}calendar/$1@sg; $text = asciify ($text); $text =~ s/^ //gm; # de-indent by 4 spaces. $text =~ s/&/&/gsi; $text =~ s//>/g; } $ititle = asciify (de_entify ($ititle)); $ititle =~ s/&/&/gsi; $ititle =~ s//>/g; $html = '' unless $html; $html =~ s/\s+/ /gs; $html =~ s/^\s+|\s+$//gs; $html = "

$html"; # Safari screws up if CDATA doesn't begin with

my $start_hour = int($start_minute / 60); $start_minute = int($start_minute % 60); my $stime = mktime (0, $start_minute, $start_hour, $dotm, $month-1, $year-1900, 0, 0, -1); my $sdate = strftime ("%a, %d %b %Y %H:%M:%S %Z", localtime ($stime)); $output .= (" \n" . " $ititle\n" . " $url\n" . " $sdate\n" . " $text\n" . " \n" . make_custom_rss ($event) . " \n"); last if (++$count >= $rss_max_links); } $output .= " \n"; $output .= "\n"; my $outfile = "$dir/$calendar_rss_file"; write_file_if_changed ($outfile, $output, 0); } sub make_custom_tag($$;$) { my ($name, $val, $attrs) = @_; return '' unless defined ($val); my $ns = 'dnalounge'; $name = lc($name); $attrs = '' unless defined ($attrs); $val =~ s@<[^<>]*>@ @gsi; $val = asciify ($val); $val =~ s/&/&/gsi; $val =~ s//>/g; $val =~ s/\s+/ /gsi; $val =~ s/^\s+//gsi; $val =~ s/\s+$//gsi; if ($val =~ m/\.(html|jpg)$/ && $val !~ m/^http:/) { $val = $url_base . $val; } return "<$ns:$name$attrs>$val\n"; } sub make_custom_rss($) { my ($event) = @_; my $ns = 'dnalounge'; my $rss = ''; $_ = $$event->{html_src}; my ($time) = m@ @xsi; my ($genre) = m@ \s* (.+?) \s* @xsi; my ($price) = m@ \s* (.+?) \s* @xsi; my ($age) = m@ \s* (.+?) \s* @xsi; if ($age) { error ("unparsable AGE tag: \"$age\"") unless ($age =~ m/^(AA|18|21)$/si); $age = 'all ages' if ($age eq 'AA'); } if ($price) { $price =~ s@ ?< ?@ before @gsi; $price =~ s@ ?> ?@ after @gsi; } my $date = $$event->{date}; $date =~ s/ (Morning|Afternoon|Night)\b//si; $date =~ s/^([a-z][a-z][a-z])[a-z]*, (.*)/$2 ($1)/gsi; my $tickets = $$event->{tickets}; my $live_title = compute_band_based_title ($event, 1, 0); if ($live_title && $live_title eq $$event->{title} && $$event->{html_src} !~ m/{title}); $rss .= make_custom_tag ('live_title', $live_title) if ($live_title); $rss .= make_custom_tag ('flyer', $$event->{flyer}); $rss .= make_custom_tag ('date', $date); $rss .= make_custom_tag ('time', $time); $rss .= make_custom_tag ('genre', $genre); $rss .= make_custom_tag ('price', $price); $rss .= make_custom_tag ('age', $age) if ($age); if ($tickets) { foreach my $t (@$tickets) { my ($ticket, $desc, $onsale, $offsale, $vip_p) = @$t; if (!ticket_off_sale_p ($event, $onsale, $offsale)) { my $tag = ($vip_p ? "vipticket" : "ticket"); $desc = ($desc ? " text=\"$desc\"" : undef); $rss .= make_custom_tag ($tag, $ticket, $desc); } } } s/\s+/ /gsi; s@(Lounge:|

)@\n$1@gsi; my $loungep = 0; foreach (split (/\n/)) { $loungep = 1 if (m/^Lounge:/); $loungep = 0 if (m/^

/); next unless m@^<(DJ|BAND|OTHER)([^<>]*)>(.*?)@si; my ($tag, $attrs, $body) = ($1, $2, $3); $tag =~ s/^OTHER$/PERFORMER/si; $attrs =~ s/ +DUP\b//gsi; $attrs = " LOUNGE=\"true\"" . $attrs if ($loungep); $rss .= make_custom_tag ($tag, $body, lc($attrs)); } $rss =~ s/^/ /gm; return $rss; } # Quotifies the text to make it safe for iCal/vCalendar # sub ical_quote($) { my ($text) = @_; $text =~ s/\s+$//gs; # lose trailing newline. $text =~ s/([\"\\,;])/\\$1/gs; # quote backslash, comma, semicolon. $text =~ s/\n/\\n\n /gs; # quote newlines, and break at newlines. # Combining blank lines confuses the Sonic Living parser. Fuck it. # $text =~ s/(\\n)\n (\\n)/$1$2/gs; # combine blank lines. # combine multiple blank lines into one. $text =~ s/(\n *\n)( *\n)+/$1/gs; # iCal seems to insist on Unicode, not Latin1. Fuck that, they get ASCII. $text = asciify($text); # Wrap long lines, just in case something somewhere chokes on them. { $Text::Wrap::columns = 77; my @paras = split(/\n/, $text); foreach my $p (@paras) { $p = wrap ('', ' ', $p) if (length($p) > $Text::Wrap::columns); } $text = join ("\n", @paras); } return $text; } # Returns the start and duration of the event in ical format. # sub ical_dtstart($) { my ($event) = @_; my $times = $$event->{times}; my ($year, $month, $dotm, $dotw, $start_minute, $end_minute) = @$times; my $shour = int($start_minute / 60); my $smin = int($start_minute % 60); # Set the end time to midnight, since Mozilla Calendar loses its mind # when events span days. MacOS iCal doesn't lose its mind, but in # the month view, it does show the event on both days, and that looks # funny. # my $midnight = (24 * 60); $end_minute = $midnight if ($end_minute > $midnight); my $length = $end_minute - $start_minute; my $start = sprintf ("%04d%02d%02dT%02d%02d%02d", $year, $month, $dotm, $shour, $smin, 0); my $duration = (($length % 60) ? sprintf ("PT%dH%dM", ($length / 60), ($length % 60)) : sprintf ("PT%dH", ($length / 60))); my $tz = $rss_channel_tz; #$tz =~ s@/@-@gs; # RFC example says "US-Pacific", # but Apple iCal requires "US/Pacific". return ($start, $duration, $tz); } # Write the calendar/dnalounge.ics file, for iCal/vCalendar syndication. # See http://www.faqs.org/rfcs/rfc2445.html # sub generate_calendar_ical($) { my ($dir) = @_; my $outfile = "$dir/$calendar_ical_file"; # First, read the existing iCal file, to see what the DTSTAMPs are. # This is so we can leave the DTSTAMP ("entry creation time") unchanged # if the body of the entry has not changed. # my %dtstamps; { local *IN; if (open (IN, "<$outfile")) { my $body = ''; while () { $body .= $_; } close IN; $body =~ s/\r\n/\n/gs; $body =~ s/(^BEGIN:VEVENT)/\000$1/gm; my @entries = split (/\000/, $body); shift @entries; foreach (@entries) { s/END:VCALENDAR.*$//gs; my ($url) = m/UID:([^\s]+)/; my ($dt) = m/DTSTAMP:([^\s]+)/; my ($seq) = m/SEQUENCE:([^\s]+)/; $seq = 0 unless defined($seq); s/(DTSTAMP|SEQUENCE):([^\s]+)/$1:%%$1%%/gs; my @entry = ($dt, $seq, $_); $dtstamps{$url} = \@entry; } } } my $output = ("BEGIN:VCALENDAR\n" . "X-WR-CALNAME;VALUE=TEXT:" . ical_quote ($rss_channel_title) . "\n" . "X-WR-CALDESC;VALUE=TEXT:" . ical_quote ($rss_channel_desc2) . "\n" . "X-WR-TIMEZONE;VALUE=TEXT:$rss_channel_tz\n" . "PRODID:-//dnalounge.com//calendar//EN\n" . "VERSION:2.0\n" . "CALSCALE:GREGORIAN\n" . "METHOD:PUBLISH\n"); my @events = @future_events; unshift @events, $yesterday_event if ($yesterday_event); my $count = 0; foreach my $event (@events) { my $title = $$event->{title}; my $times = $$event->{times}; my $flyer = $$event->{flyer}; my ($year, $month, $dotm, $dotw, $start_minute, $end_minute) = @$times; $month = sprintf("%02d", $month); $dotm = sprintf("%02d", $dotm); my $url = "${url_base}calendar/$year/$month.html\#$dotm"; $flyer = "${url_base}$flyer" if defined($flyer); my $locurl = "${url_base}directions/"; # Don't include cancelled events in the iCal file. # But do include private events. if ($title =~ m/\b(CANCELL?ED|POSTPONED)\b/i) { my $m = $months[$month-1]; $m =~ s/^(...).*$/$1/; print STDERR "$progname: skipping cancelled: $dotm $m: $title\n" if ($verbose > 2); next; } my ($start, $duration, $tz) = ical_dtstart ($event); # If this is a new or changed entry, set DTSTAMP to "now" # and increment SEQUENCE. my ($csec, $cmin, $chour, $cdotm, $cmon, $cyear) = localtime; $cmon++; $cyear += 1900; my $dtstamp = sprintf ("%04d%02d%02dT%02d%02d%02d", $cyear, $cmon, $cdotm, $chour, $cmin, $csec); my $sequence = 0; my $desc = build_event_text ($event, 0, 1, 0, 1, undef, undef); # delete the calendar and flyer url lines from the text. # (this would be a good idea if any of the calendar programs # respected the URL or ALTREP fields, but they don't!) # # $desc =~ s/^ *(?-xism:$url) *\n//gm; # $desc =~ s/^ *(?-xism:$flyer) *\n//gm if defined ($flyer); $desc =~ s/^ //gm; # de-indent by 4 spaces. # The UID of entries must be different for two events on the same day. # my $url_b = $url; if ($$event->{day_ord} && $$event->{day_ord}[1] > 1) { $url_b .= chr (ord('a') + $$event->{day_ord}[0]); } $url = ical_quote ($url); $url_b = ical_quote ($url_b); $flyer = ical_quote ($flyer) if $flyer; my $entry = ("BEGIN:VEVENT\n" . "UID:" . $url_b . "\n" . "DTSTAMP:%%DTSTAMP%%\n" . "SEQUENCE:%%SEQUENCE%%\n" . "ORGANIZER:dnalounge\n" . "LOCATION;" . "ALTREP=\"$locurl\":\n " . ical_quote ($rss_channel_loc) . "\n" . "GEO:" . $rss_latlong . "\n" . # don't quote ";" "SUMMARY:" . ical_quote ($title) . "\n" . "DTSTART;" . "TZID=$tz:$start\n" . "DURATION:$duration\n" . "URL:" . ($flyer ? $flyer : $url) . "\n" . "DESCRIPTION;" . "ALTREP=\"$url\":\n " . ical_quote ($desc) . "\n" . "CLASS:PUBLIC\n" . "CATEGORIES:Performance\n" . "STATUS:CONFIRMED\n" . "END:VEVENT\n"); my $oentry = $dtstamps{$url_b}; if (defined ($oentry)) { my ($odt, $oseq, $oentry) = @$oentry; $sequence = $oseq+1; if ($entry eq $oentry) { $dtstamp = $odt; $sequence = $oseq; print STDERR "$progname: $outfile: keeping " . "$dtstamp / $sequence \"$title\"\n" if ($verbose > 4); } else { print STDERR "$progname: $outfile: CHANGED " . "$dtstamp / $sequence \"$title\"\n" . "$progname: $outfile: from $odt / $oseq\n" if ($verbose > 3); } } else { print STDERR "$progname: $outfile: new $dtstamp / $sequence \"$title\"\n" if ($verbose > 3); } $entry =~ s/%%DTSTAMP%%/$dtstamp/gs; $entry =~ s/%%SEQUENCE%%/$sequence/gs; $output .= $entry; } $output .= "END:VCALENDAR\n"; $output =~ s/\n/\r\n/gs; # convert to CRLF write_file_if_changed ($outfile, $output, 0); } # Write the calendar/crontab.txt file, containing the webcast crontab # for the next few events. # sub generate_crontab($) { my ($dir) = @_; my @events = @future_events; unshift @events, $yesterday_event if ($yesterday_event); my $nevents = 10; my $output = ""; foreach my $event (@events) { my $title = $$event->{title}; my $date = $$event->{date}; # Don't include cancelled events in the crontab file. if ($title =~ m/\b(CANCELL?ED|POSTPONED|CLOSED|SUSPENDED)\b/i) { print STDERR "$progname: crontab: cancelled: $date: $title\n" if ($verbose > 2); } elsif ($nevents <= 0) { print STDERR "$progname: crontab: future: $date: $title\n" if ($verbose > 3); } else { $output .= build_event_crontab ($event); $nevents--; } } my $outfile = "$dir/$calendar_crontab_file"; write_file_if_changed ($outfile, $output, 0); } # expands the first URL relative to the second. # sub expand_url($$) { my ($url, $base) = @_; $url =~ s/^\s+//gs; # lose whitespace at front and back $url =~ s/\s+$//gs; $url =~ s@^//@http://@; # slashdot does this stupidity if (! ($url =~ m/^[a-z]+:/)) { $base =~ s@(\#.*)$@@; # strip anchors $base =~ s@(\?.*)$@@; # strip arguments $base =~ s@/[^/]*$@/@; # take off trailing file component my $tail = ''; if ($url =~ s@(\#.*)$@@) { $tail = $1; } # save anchors if ($url =~ s@(\?.*)$@@) { $tail = "$1$tail"; } # save arguments my $base2 = $base; $base2 =~ s@^([a-z]+:/+[^/]+)/.*@$1@ # if url is an absolute path if ($url =~ m@^/@); my $ourl = $url; $url = $base2 . $url; $url =~ s@/\./@/@g; # expand "." 1 while ($url =~ s@/[^/]+/\.\./@/@g); # expand ".." $url .= $tail; # put anchors/args back print STDERR "$progname: relative URL: $ourl --> $url\n" if ($verbose > 6); } else { print STDERR "$progname: absolute URL: $url\n" if ($verbose > 7); } return $url; } # converts all relative URLs in SRC= or HREF= to absolute URLs, # relative to the given base. # sub expand_urls($$) { my ($html, $base) = @_; $html =~ s/]*>)@\n$1@gsi; $html =~ s@\s*()\s*@$1\n@gs; $html =~ s/^\s+|\s+$//gs; # leading/trailing whitespace in document $html =~ s/^[ \t]+|[ \t]+$//gm; # leading/trailing whitespace on line $html =~ s/[ \t]+/ /gm; # horizontal to single space $html =~ s/\n\n+/\n/gs; # replace SPC with \n for lines still longer than 60 chars 1 while ($html =~ s@^([^\n]{60,}?)[ \t]@$1\n@gmi); return $html; } # Remove the style sheet from the HTML, and convert all CLASS= to STYLE=. # This is so we don't have to deal with namespace conflicts in HTML that # will end up getting displayed embedded in someone's webmail page. # my $hardcoded_css = undef; sub hardcode_stylesheet($$) { my ($dir, $html) = @_; my $sheet = ''; # Note: load the sheets in the reverse order (doc, cal, toplevel.) while ($html =~ s@(\s*)@@si) { $sheet .= "\n$2"; } if (! $hardcoded_css) { $hardcoded_css = ''; local *IN; foreach my $f ("$dir/calendar.css", "$dir/../dnalounge.css") { open (IN, "<$f") || error ("$f: $!"); my $sheet1 = ''; while () { $sheet1 .= $_; } close IN; $hardcoded_css .= $sheet1; } } $sheet .= $hardcoded_css; # Comments. $sheet =~ s@()\s*@@gs; $sheet =~ s@/\*.*?\*/@@gs; # This isn't actually valid parsing, but it works for our sheets. # Just toss everything between "@media" and "\n}\n". $sheet =~ s/\n\@media\b.*?\n}\n/\n/gs; $sheet .= "\n .noprint {}\n"; # Kludge. # Remove all colors. $sheet =~ s@(color|background):\s*[^;{}]+;?@@gsi; $sheet =~ s@(border[-a-z]*:[^;{}]+?)#[\dA-F]{3,6}\b\s*@$1@gsi; # Kludge for the black line at the top of the "event" DIV. $sheet =~ s/border-top: \s* [\d.]+ em \s* solid \s* ; /border: 1px solid;/gsix; # The div that provides the clipped background color is toxic. # Gmail and Hotmail strip out "position:absolute" so we have to # omit this entirely, or else it is a zillion pixels tall. # $sheet =~ s@( \.dboxbg \s* { ) [^{}]* ( } ) @$1$2@six; # These are also stripped by Hotmail. Strip them here since # they *shouldn't* matter, and if they do, I want to see it. # $sheet =~ s@ \b ( position | margin(-[a-z]+)? | z-index | overflow ) \s* : \s* [^;"{}]+ ;? @@gsix; while ($sheet =~ s@^ \s* ( [-.a-z\d_:#]+ ( [,\s]+ >? \s* [-.a-z\d_:#]+ )* ) \s* { \s* ( [^{}]* ) \s* } \s* @@six) { my ($key, $val) = ($1, $3); $val =~ s/\s+/ /gs; $val =~ s/(^\s+|[;\s]+$)//gs; $val =~ s/\s*([:;])\s*/$1/gs; foreach my $sel (split (m/,\s*/, $key)) { next unless ($sel =~ s/^\.//s); next if ($sel =~ m/[^-_a-z\d]/si); # complicated selector $sel = qr/$sel/; my $s = ($val ? " STYLE=\"$val\"" : ""); $html =~ s@\s+CLASS=\"$sel\"@$s@gsi; } } error ("unparsable style sheet: $sheet") if $sheet; error ("failed to nuke class \"$1\": $html") if ($html =~ m@\bCLASS=\"([^\"]+)@si); error ("stripped CSS still contains $1") if ($html =~ m@\b (( position | margin(-[a-z]+)? | z-index ) \s*:\s* [^;"]+ )@six); # merge adjascent styles within a tag 1 while ($html =~ s@(STYLE="[^"]+)"\s+STYLE="@$1;@gsi); # The tags are only in here for the hcalendar microformat. $html =~ s@]*>@@gsi; # Since we had to strip "margin" properties, add some line breaks. $html =~ s@($1@gsi; # Before "fblike" $html =~ s@(]*>)@

$1

@gsi; # Around images $html =~ s@(

$1@gsi; # Before DIVs with borders # Let's make the "fblike" iframe be light instead of dark. $html =~ s@(colorscheme)=dark@$1=light@gs; return $html; } # Decolorizing the style sheet isn't quite enough... # This also converts relative URLs to absolute. # sub decolorize_html($) { my ($html) = @_; $html =~ s@((HREF|SRC)=\")../../@$1$url_base@gsi; # fix relative URLs $html =~ s@]*>(.*?)@$1@gsi; # Can't fix this. $html =~ s/\bBGCOLOR=[^\s<>]+//gsi; $html =~ s/\s+[HV]SPACE=\"?\d+\"?//gsi; $html =~ s/\b(background|color):[^\";<>]+\s*;?\s*//gsi; $html =~ s/\b(border\b.*)\s+#[\dA-F]+\b/$1/gsi; $html =~ s/\s+STYLE=""//gsi; $html =~ s/[ \t]+/ /gsi; $html =~ s@()\s+@$1@gsi; $html =~ s@\s+(]*>@@gsi; return $html; } # Write the calendar/weekly.txt and calendar/weekly.html files, # containing the text and HTML versions of the next two weeks' # worth of events. This inserts "calendar/prolog.txt" in each, # if it exists. # # The output files are used by calendar/mail-weekly.pl # sub generate_weekly($) { my ($dir) = @_; # my $ndays = 16; my $output_text = ""; my $output_html = ""; my $live_shows = ""; my $prologue = ''; { local *IN; if (open (IN, "<$dir/$calendar_weekly_prolog")) { while () { $prologue .= $_; } close IN; } $prologue =~ s/[ \t]+$//mi; $prologue =~ s/\s+$//si; } my $prologue_html = $prologue; $prologue_html =~ s/&/&/g; $prologue_html =~ s//>/g; $prologue_html =~ s/\n\n+/\n

/gsi; $prologue_html =~ s@\b(http://([^<>\s\"\',/]+)[^<>\s\"\',]+)\b@$2@gsi; $prologue_html =~ s@\b([a-z\d]+\@[a-z\d]+\.[a-z\d.]+)\b@$1@gsi; my ($csec, $cmin, $chour, $cdotm, $cmon, $cyear) = localtime; my $today = mktime (0, 0, 0, $cdotm, $cmon, $cyear, 0, 0, -1); my $from = $today; foreach my $event (@future_events) { my $title = $$event->{title}; if ($title =~ m/\b(PRIVATE\s+(PARTY|EVENT)|CLOSED|SUSPENDED)\b/i) { print STDERR "$progname: infoline: skipping private: $title\n" if ($verbose > 2); next; } my $times = $$event->{times}; my ($year, $month, $dotm) = @$times; my $etime = mktime (0, 0, 0, $dotm, $month-1, $year-1900, 0, 0, -1); $live_shows .= event_live_show_summary ($event); my $days_away = int (($etime - $from) / (24 * 60 * 60)); my $hype_p = ($$event->{html_src} =~ m/<(BAND|LIVE)\b[^<>]*\bHYPE\b/si); if ($days_away < 14 || $hype_p) { my $day_prefix = ($days_away < 7 ? "This" : $days_away < 14 ? "Next" : undef); my $day_suffix = ($days_away == 0 ? "Tonight" : $days_away == 1 ? "Tomorrow" : undef); my $txt = build_event_text ($event, 1, 1, 0, 0, $day_prefix, $day_suffix); $output_text .= $txt; my $html = build_event_html ($event, 1, 0); $html = decolorize_html ($html); # hack the day prefixes/suffixes if ($day_prefix || $day_suffix) { my $days_re = "\\b((" . join ('|', @days) . ")\\b[^<>]*)"; my $pre = ($day_prefix ? "$day_prefix " : ""); my $suf = ($day_suffix ? "
-- $day_suffix" : ""); $html =~ s@$days_re@$pre$1$suf@; } $output_html .= $html; $output_html .= '

'; # since Hotmail strips margin-top. if ($verbose > 3) { my $title = $$event->{title}; my $date = $$event->{date}; $date = "$day_prefix $date" if (defined ($day_prefix)); $date = "$day_suffix, $date" if (defined ($day_suffix)); print STDERR "$progname: weekly: $date: $title\n"; } } elsif ($verbose > 3) { my $title = $$event->{title}; my $date = $$event->{date}; print STDERR "$progname: weekly: future: $date: $title\n"; } } $output_text = ("DNA Lounge, 375 Eleventh Street.\n" . "\n" . " ${url_base}directions/\n" . " ${url_base}calendar/\n" . " ${url_base}flyers/\n" . "\n" . " webcal://www.dnalounge.com/calendar/dnalounge.ics (iCal feed)\n" . $output_text); my ($upcoming_html, $output_twit) = generate_calendar_upcoming_1 ($dir, 15, # $max_events 1, # $bands_p 0, # $tickets_p 1, # $list_all_bands_p 0, # $next_p 1, # $thisweek_p 0 # ticket_form_p ); $upcoming_html = decolorize_html ($upcoming_html); $upcoming_html = "\n" . "$upcoming_html
"; $upcoming_html =~ s@(\(buy\))@$1  @gsi; $upcoming_html = ("

\n" . "
\n" . $upcoming_html . "
\n" . "
\n"); $output_html = ("
\n" . "" . "" . "" . "" . "" . "" . "" . "" . "" . "" . "
" . "DNA Lounge. 375 Eleventh Street.
\n" . "
" . "
" . $prologue_html . "

Unsubscribe info is at the bottom." . "

" . "
" . # The "?M" is a webbug for the logs. "
" . "
" . "\n" . "Directions
\n" . "Calendar
\n" . "Flyers
\n" . "" . "iCal Feed\n" . "
\n" . "

\n" . # "
" . # "It's that time of year again! Reserve a date now to host your " . # "company's holiday party here at DNA Lounge. Wouldn't you rather have " . # "it here than in the company cafeteria? Of course you would! We can " . # "provide any level of support for your party, from a simple room " . # "rental, to catering and start-to-finish entertainment. Please check " . # "out our " . # "Private Parties page, and contact us at " . # "booking\@dnalounge.com " . # "for more information or to schedule a tour. " . # "

" . "

Won't you be our friend? We're on " . "Facebook, " . "MySpace, " . "SonicLiving, " . "Last.FM, " . "Going, " . "Upcoming, " . "Yelp " . "and " . "Twitter." . "

" . "
" . "

" . "\n" . $upcoming_html . "

" . $output_html); if ($live_shows ne '') { $live_shows =~ s/^/ /gm; $live_shows = "Upcoming live shows:\n\n" . $live_shows . "\n"; $live_shows .= "For advance tickets to all these shows,"; $live_shows .= " go to $url_base\n\n"; $output_text = $live_shows . $output_text; } $output_text .= ("\n" . ('-' x 72) . "\n" . # $fill_column ? "To unsubscribe, send mail to \"announce-request\@dnalounge.com\" with\n" . "\"unsubscribe\" in the body of the message. To subscribe a different\n". "address, send mail from the new address with \"subscribe\" in the body.\n" ); # $output_text = "upcoming DNA Lounge events\n\n" . $output_text; $output_text = "$prologue\n\n$output_text" if ($prologue); $output_html .= ("\n

" . "

" . "To unsubscribe, send mail to " . "" . "announce-request\@dnalounge.com with\n" . "\"unsubscribe\" in the body of the message." . " To subscribe a different\n". "address, send mail from the new address with " . "\"subscribe\" in the body." . "

\n" ); # Fix URLs. $output_html = expand_urls ($output_html, "$url_base$dir/"); # Auugh. $output_html =~ s@(\.com)(/(vip\.html|export\.cgi))@$1/calendar$2@g; # Lose any empty rows $output_html =~ s@ \s* ]*> \s* \s* \s*@@gsix; $output_html = ("\n" . "\n" . "\n" . "\n" . "\n" . "$output_html\n" . "\n" . "\n"); $output_html = hardcode_stylesheet ($dir, $output_html); $output_html = compress_and_wrap_html ($output_html); $output_html .= "\n"; my $outfile = "$dir/$calendar_weekly_file"; write_file_if_changed ($outfile, $output_text, 0); $outfile =~ s@\.txt$@.html@; write_file_if_changed ($outfile, $output_html, 1); $outfile =~ s@\.html$@-twit.txt@; write_file_if_changed ($outfile, $output_twit, 0); } # Write the calendar/infoline.txt file, containing the text version of # the next two weeks' worth of events. # sub generate_infoline($) { my ($dir) = @_; my $ndays = 16; my $output = (" Hello! And thank you for calling DNA Lounge.\n" . " We are located at 375 Eleventh Street," . " between Folsom and Harrison.\n" . " Advance tickets are available at" . " DNA Lounge dot com.\n\n" . " Here are our upcoming events!\n" . " Press star to skip ahead.\n" . " Press any other key to back up.\n\n"); my ($csec, $cmin, $chour, $cdotm, $cmon, $cyear) = localtime; my $today = mktime (0, 0, 0, $cdotm, $cmon, $cyear, 0, 0, -1); my $from = $today; my $to = $from + ($ndays * 24 * 60 * 60); foreach my $event (@future_events) { #foreach my $event (@event_ords) { my $title = $$event->{title}; my $times = $$event->{times}; my ($year, $month, $dotm) = @$times; my $etime = mktime (0, 0, 0, $dotm, $month-1, $year-1900, 0, 0, -1); if ($title =~ m/\b(PRIVATE\s+(PARTY|EVENT)|CLOSED|SUSPENDED)\b/i) { print STDERR "$progname: infoline: skipping private: $title\n" if ($verbose > 2); } elsif ($etime <= $to) { my $txt = build_event_text ($event, 1, 1, 1, 1, undef, undef); $output .= $txt; if ($verbose > 3) { my $title = $$event->{title}; my $date = $$event->{date}; print STDERR "$progname: infoline: $date: $title\n"; } } elsif ($verbose > 3) { my $title = $$event->{title}; my $date = $$event->{date}; print STDERR "$progname: infoline: future: $date: $title\n"; } } $output .= ("\n\n" . " Thank you for calling!\n" . " Please check our site on the inter-webs for more" . " details. DNA Lounge dot com.\n" . " You can also be our friend on the face-books and" . " the twitters.\n" . " Transmission ends.\n"); # compress multiple blank lines $output =~ s/(\n\n)\n+/$1/gs; # terminate un-punctuated sentences (lines). $output =~ s/([a-z\d])[ \t]*(\n)/$1.$2/gsi; my $outfile = "$dir/$calendar_infoline_file"; write_file_if_changed ($outfile, $output, 0); } sub reformat_infoline($) { ($_) = @_; # # strip some crap out of the HTML before reformatting it. # Punctuate lists of DJs and bands. # Lose end-of-line commentary (parentheticals, etc.) # s@<(AFF)>.*?@@gsi; # lose affiliations s@<(/?)BAND\b@<$1DJ@gsi; # treat BAND as DJ (don't upcase). s@\s+--.*?(<(P|BR))@$1@gsi; # lose everything after " --" on line. s@\s+\(.*?\)@@gsi; # lose everything in parens. s@\s*()\s*(
)@$1.$3@gsi; # punctuate lists. s@(Main Room|Lounge):@In the $1:

@gsi; s@()@$1

@gsi; # para break after genres. # s@()@$1

@gsi; # para break after times. s/\s<\s/ before /gi; s/\s>\s/ after /gi; s/\s&([\s<])/ and$1/gi; s/[+]/ and /gi; s@\bw/@with @gs; s@(.*?)@{ my $a = $1; $a =~ s/\.\s*/.
/g; $a; }@gsexi; # Unwrap all lines, and re-wrap only at sentence ends. s/\n/ /gsi; # s/
/ /gsi; # s/([:;.!?])(\s)/$1
\n/gsi; s/(
)/$1\n/gsi; s@(]*>)@$1\n@gsi; $_ .= "\n"; s@\b(presents?)(\s*<(P|BR|/PRES))@$1:$2@gsi; # punctuate "presents" line # delete any sentences about who's doing visuals or massage. # s/^.*\bvisual install.*\n//gim; s/^.*\bvisual support .*\n//gim; s/^.*\bvisuals by .*\n//gim; s/^.*\bbody work\b.*\n//gim; s/^.*\bmassage by\b.*\n//gim; s/^.*\bstring art by\b.*\n//gim; s/^.*\bstring artistry\b.*\n//gim; s/^.*\bcostumed freestyle go-gos by .*\n//gim; s/^.*\bBest of .* winner.*\n//gim; s/^.*\bIf your band would like to participate.*\n//gim; s/\n\n+/\n/gs; s/[ \t]+/ /gs; return $_; } # Check the upcoming events for any that still have no info or "TBA" # in them, and warn... # sub check_impending_tba() { return unless $verbose; my $ndays1 = 8; my $ndays2 = 16; my ($csec, $cmin, $chour, $cdotm, $cmon, $cyear) = localtime; my $today = mktime (0, 0, 0, $cdotm, $cmon, $cyear, 0, 0, -1); my $from = $today; my $to1 = $from + ($ndays1 * 24 * 60 * 60); my $to2 = $from + ($ndays2 * 24 * 60 * 60); my $warnings = ''; my $errors = ''; foreach my $event (@future_events) { my $times = $$event->{times}; my ($year, $month, $dotm) = @$times; my $etime = mktime (0, 0, 0, $dotm, $month-1, $year-1900, 0, 0, -1); my $flyer = $$event->{flyer}; my $time_to_warn_p = ($etime <= $to2); my $title = $$event->{title}; my $date = $$event->{date}; $date =~ s/^([a-z]{3})[a-z]*( [A-Za-z]+)?,\s*(\d+) +([a-z]+).*$/$1, $4 $3/si; $date =~ s/ (\d)$/ 0$1/s; my $ignore_p = ($title =~ m/\b(PRIVATE\s+(PARTY|EVENT)|CANCELL?ED|POSTPONED)\b/i); my $html = $$event->{html_src}; my $band_p = ($html =~ m@]*VIP\b@si); my $price_p = ($html =~ m@(.*?)@si); my $blurb_p = ($blurb && $blurb =~ m@[^\s]@s); # Don't warn about videos for these events, since they don't have them. $video_p = 1 if ($title =~ m/^( Battle\s+of\s+the\s+Bands )$/six); if ($html =~ m/\n\n/s) { $html =~ s/^.*?\n\n//s; # lose "title" column } else { $html = ''; # it was all title-column. } $html =~ s/<[^<>]*>//g; # lose tags my $blank_p = ($html =~ m@^\s*$@i); # consider BotB with no bands to be blank, too... $blank_p = 1 if ($title =~ m/Battle of the Bands/i && !$band_p); # If an event is blank, don't warn about certain things. # if ($blank_p) { $blurb_p = 1; $video_p = 1; $age_p = 1; $tba_p = 0; # If a blank event is still far in the future, don't warn about these: # if (!$time_to_warn_p) { $blank_p = 0; $flyer = 1; $ticket_p = 1; $vip_p = 1; $price_p = 1; $blurb_p = 1; } } if (!$ignore_p) { my @miss = (); push @miss, "time" if (!$start_p && !$blank_p); push @miss, "line-up" if ($blank_p && !$tba_p); push @miss, "ticket" if (!($ticket_p || $vip_p)); push @miss, "flyer" unless $flyer; push @miss, "price" unless $price_p; push @miss, "age" unless $age_p; push @miss, "blurb" unless $blurb_p; push @miss, "video" unless $video_p; my $miss = ''; if ($#miss >= 0) { foreach (@miss[0..$#miss-1]) { $_ .= ','; } @miss[$#miss..$#miss+1] = ("or", @miss[$#miss..$#miss+1]) if ($#miss > 0); $miss = join (' ', @miss); $miss =~ s/, or / or /; $miss = "no $miss"; } if ($tba_p) { if ($miss) { $miss = "still TBA; $miss"; } else { $miss = "still TBA" } } if ($miss) { $miss = " $date: $miss: $title\n"; if ($etime > $to1) { $warnings .= $miss; } else { $errors .= $miss; } } } } print STDERR "\n" if ($errors || $warnings); print STDERR "Missing info -- IMMINENT:\n\n$errors\n" if ($errors); print STDERR "Missing info -- less-imminent:\n\n$warnings\n" if ($warnings); } # Check whether any of the embedded videos have been deleted. # sub ping_videos() { my $all_p = 0; foreach my $event ($all_p ? @event_ords : @future_events) { my $date = $$event->{date}; my $video = $$event->{video}; next unless $video; $date =~ s/^([A-Z][a-z][a-z])[a-z]*( [A-Za-z]+)?/$1/; my $body = `wget -qO- '$video'`; my $err; $err = "blank page" if (!$err && $body =~ m/^\s*$/si); $err = "censored" if (!$err && $body =~ m/that is inappropriate/i); $err = "deleted" if (!$err && $body =~ m/removed by the user/i); $err = "deleted audio" if (!$err && $body =~ m/audio has been disabled/i); $err = "unembeddable" if (!$err && $body =~ m/embedding disabled/i); if (!$err && $body =~ m@\s*(.*?)\s*

@si) { $err = $2; $err = 'UNKNOWN ERROR' if ($err =~ m/^[^\s]*$/s); } if ($err) { my $title = $$event->{title}; print STDERR "$date: $err: $title ($video)\n"; } elsif ($verbose > 1) { print STDERR "$date: video OK\n"; } } } # Compare the tickets actually on sale in the store against the tickets # listed here. # sub check_tickets() { my $url = "${ticket_form_url}onsale.php"; print "$progname: loading $url\n" if ($verbose > 2); my $data = `wget --no-check -qO- '$url'`; error ("no data: $url") unless (length ($data) > 500); my %found; my %names; foreach my $line (split (/\n/, $data)) { my ($id, $date, $calid, $price, $svc, $vip, $flyer, $name) = split(/\t/, $line); error ("unparsable: $line") unless ($name && $name =~ m/[^\s]/s); $found{$id} = 0; $names{$id} = $name; foreach my $event (@event_ords) { my $tickets = $$event->{tickets}; next unless $tickets; foreach my $t (@$tickets) { my ($ticket, $desc, $onsale, $offsale, $vip_p) = @$t; $ticket =~ s/^.*item=(\d+)$/$1/s; if ($ticket eq $id) { $found{$id}++; my $eflyer = $$event->{flyer} || ''; my $times = $$event->{times}; my ($year, $month, $dotm, $dotw, $start_minute) = @$times; my $start_hour = int($start_minute / 60); $start_minute = int($start_minute % 60); my $edate = mktime (0, $start_minute, $start_hour, $dotm, $month-1, $year-1900, 0, 0, -1); my $ordP = $$event->{day_ord}; my @ord = $ordP ? @$ordP : (); my $eid = sprintf ("%04d-%02d-%02d", $year, $month, $dotm); $eid .= "b" if ($ord[0]); my $ok = 1; # Compare dates. # if ($date != $edate) { print STDERR "$progname: date mismatch: $id \"$name\"\n" . "$progname:\t" . strftime ("%a, %d %b %Y %H:%M", localtime($date)) . " vs\n". "$progname:\t" . strftime ("%a, %d %b %Y %H:%M", localtime($edate)) . "\n"; $ok = 0; } # Compare IDs. # if ($calid ne $eid) { print STDERR "$progname: ID mismatch: $id \"$name\"\n" . "$progname: \"$calid\" vs \"$eid\"\n"; $ok = 0; } # Compare names. # my $t2 = $$event->{title}; $t2 =~ s/SF (Drag King) Contest/$1/gsi; #### kludge $t2 =~ s/: .*$//s; # cut after colon $t2 =~ s/^The //si; $t2 =~ s/^(.{15}).*$/$1/s; # only check first N chars $t2 = qr/$t2/i; if ($name !~ m/$t2/si) { print STDERR "$progname: name mismatch: $id \"$name\"\n" . "$progname:\t vs \"$$event->{title}\"\n"; $ok = 0; } # Compare prices. # my $eprice; $desc = $vip_price if ($vip_p && !$desc); ($eprice) = ($desc =~ m/\$([\d.]+)/si) if ($desc); if (! $eprice) { my $html = $$event->{html_src}; ($html) = ($html =~ m@(.*?)@si); if ($html =~ m/\$(\d+)\s*advance/si) { $eprice = $1; } else { # Delete "$8 < .." and "$8 before .." to get the later price. $html =~ s/\$[\d.]+ (<|b4|before|until) //sg; # always take the last price listed, # so that "$7 before 10, $10 after" # results in a presale price of $10, not $7. # $html =~ s/\$.*\$/\$/s; if ($html =~ m/\$(\d+)\b/si) { $eprice = $1; } } } $eprice = 0 unless defined($eprice); if ($price != $eprice) { print STDERR "$progname: price mismatch: $id \"$name\"\n" . "$progname:\t\$$price vs \$$eprice\n"; $ok = 0; } # Compare thumbnails. # if ($eflyer) { $eflyer = $url_base . $eflyer; $eflyer =~ s/\.html$/-1-thumb.jpg/si; } if ($eflyer && !$flyer) { print STDERR "$progname: thumbnail missing: $id \"$name\"\n" . "$progname: $eflyer\n"; $ok = 0; } elsif ($flyer ne $eflyer) { print STDERR "$progname: thumbnail mismatch: $id \"$name\"\n" . "$progname: " . ($flyer || "''") . " vs\n" . "$progname: " . ($eflyer || "''") . "\n"; $ok = 0; } if ($ok && $verbose > 1) { print STDERR "$progname: OK: $id \"$name\", \$$price\n"; } } } } } foreach my $id (sort keys (%found)) { if ($found{$id} == 1) { } elsif ($found{$id} == 0) { print STDERR "$progname: unlisted: $id \"$names{$id}\"\n"; } else { print STDERR "$progname: multi-listed: $id \"$names{$id}\"\n"; } } } # Write all of the files: # - calendar/YYYY/MM.html # - calendar/YYYY/MM.txt # - calendar/upcoming.html # - calendar/overview.html # - calendar/dnalounge.rss # - calendar/crontab.txt # - tickets/index.html # sub generate_calendars($$$$$) { my ($dir, $first, $last, $summarize_only_p, $topten_p) = @_; load_template (); error ("from range must be YYYY-MM: $first\n") unless $first =~ m/^\d{4}-\d{2}$/; error ("to range must be YYYY-MM: $last\n") unless $last =~ m/^\d{4}-\d{2}$/; $_ = $first; my ($first_year, $first_month) = m/^(\d{4})-(\d{2})$/; $_ = $last; my ($last_year, $last_month) = m/^(\d{4})-(\d{2})$/; @future_events = (); $yesterday_event = undef; for (my $year = $last_year; $year >= $first_year; $year--) { my $start_month = ($year == $first_year ? $first_month : 1); my $end_month = ($year == $last_year ? $last_month : 12); for (my $month = $end_month; $month >= $start_month; $month--) { my $outfile = sprintf ("$dir/$year/%02d.html", $month); my $prev = !($year == $first_year && $month == $first_month); my $next = !($year == $last_year && $month == $last_month); write_month_bodies ($dir, $year, $month, $outfile, $prev, $next, ($summarize_only_p || $topten_p)); } } return if ($summarize_only_p =~ m/^../s); # it's a date ("--only") if (! $topten_p) { check_impending_tba () unless ($summarize_only_p); generate_tickets_html ($dir); generate_calendar_overview ($dir); generate_calendar_upcoming ($dir); generate_calendar_rss ($dir); generate_calendar_ical ($dir); generate_crontab ($dir); generate_weekly ($dir); generate_infoline ($dir); generate_names_file ($dir) unless ($summarize_only_p); generate_eighties_file ($dir) unless ($summarize_only_p); } generate_ledger_html ($dir, $topten_p) unless ($summarize_only_p); } # Generates all of the "YYYY/index.html" pages listing all the months # in each year. # sub generate_months($$$) { my ($dir, $first, $last) = @_; error ("from range must be YYYY-MM: $first\n") unless $first =~ m/^\d{4}-\d{2}$/; error ("to range must be YYYY-MM: $last\n") unless $last =~ m/^\d{4}-\d{2}$/; $_ = $first; my ($first_year, $first_month) = m/^(\d{4})-(\d{2})$/; $_ = $last; my ($last_year, $last_month) = m/^(\d{4})-(\d{2})$/; for (my $year = $first_year; $year <= $last_year; $year++) { my $start_month = ($year == $first_year ? $first_month : 1); my $end_month = ($year == $last_year ? $last_month : 12); my $output = ""; $output .= "$page_title: $year\n"; my @order = (1, 5, 9, 2, 6, 10, 3, 7, 11, 4, 8, 12); my $row = 0; my $col = 0; { my $links = ''; my $prev = sprintf ("%04d", $year-1); my $next = sprintf ("%04d", $year+1); $prev = undef if ($year == $first_year); $next = undef if ($year == $last_year); my $fh = ($year == $first_year ? undef : "../$first_year/"); my $lh = ($year == $last_year ? undef : "../$last_year/"); $links .= " \n"; $links .= " \n"; $links .= " \n" if ($fh); $links .= " \n" if ($prev); $links .= " \n" if ($next); $links .= " \n" if ($lh); $links .= $xml_link_tag; $output .= $links; } { my $prev = sprintf ("%04d", $year-1); my $next = sprintf ("%04d", $year+1); my $prev_href = ($year == $first_year) ? undef : "../$prev/"; my $next_href = ($year == $last_year) ? undef : "../$next/"; # Kludge for the ancient history... # if ($year == $first_year) { my $f = $calendar_eighties_file; $f =~ s/\.txt$/.html/s; $prev_href = "../$f"; $f =~ s/\..*$//; $prev = $f; } $prev = "<< $prev"; $next = "$next >>"; $output .= ("\n" . ($prev_href ? "$prev\n" : "$prev\n") . ($next_href ? "$next\n" : "$next\n") . "\n"); } $output .= "\n"; $output .= "\n"; $output .= "$year Calendar\n\n"; $output .= "
" . "\n"; $output .= " \n"; for (my $i = 0; $i < 12; $i++) { my $month = $order[$i]; my $text = $months[$month-1]; if ($month >= $start_month && $month <= $end_month) { $text = sprintf ("$text", $month); } else { $text = "$text"; } my $align = ($col == 0 ? "RIGHT" : $col == 1 ? "CENTER" : "LEFT"); $output .= " \n"; if (++$col == 3) { $col = 0; $row++; $output .= " \n"; $output .= " \n" if ($row < 4) } } $output .= "
$text
\n"; $output .= "\n"; my $outfile = "$dir/$year/index.html"; write_file_if_changed ($outfile, $output, 1); } } ############################################################################## # # Writing files # ############################################################################## # Returns true if the two files differ (by running "cmp") # sub cmp_files($$) { my ($file1, $file2) = @_; my @cmd = ("cmp", "-s", "$file1", "$file2"); print "$progname: executing \"" . join(" ", @cmd) . "\"\n" if ($verbose > 2); system (@cmd); my $exit_value = $? >> 8; my $signal_num = $? & 127; my $dumped_core = $? & 128; error ("$cmd[0]: core dumped!") if ($dumped_core); error ("$cmd[0]: signal $signal_num!") if ($signal_num); return $exit_value; } sub diff_files($$) { my ($file1, $file2) = @_; my @cmd = ("diff", "-U2", "--unidirectional-new-file", "$file1", "$file2"); print "$progname: executing \"" . join(" ", @cmd) . "\"\n" if ($verbose > 2); system (@cmd); my $exit_value = $? >> 8; my $signal_num = $? & 127; my $dumped_core = $? & 128; error ("$cmd[0]: core dumped!") if ($dumped_core); error ("$cmd[0]: signal $signal_num!") if ($signal_num); return $exit_value; } # If the two files differ: # mv file2 file1 # else # rm file2 # sub rename_or_delete($$) { my ($file, $file_tmp) = @_; my $changed_p = cmp_files ($file, $file_tmp); if ($changed_p && $debug_p) { print STDOUT "\n" . ('#' x 79) . "\n"; diff_files ("$file", "$file_tmp"); $changed_p = 0; } if ($changed_p) { if (!rename ("$file_tmp", "$file")) { unlink "$file_tmp"; error ("mv $file_tmp $file: $!"); } print STDERR "$progname: wrote $file\n" if ($verbose); } else { unlink "$file_tmp" || error ("rm $file_tmp: $!\n"); print STDERR "$progname: $file unchanged\n" if ($verbose > 1); print STDERR "$progname: rm $file_tmp\n" if ($verbose > 2); } } # Write the given body to the file, but don't alter the file's # date if the new content is the same as the existing content. # If $menuify_p is true, runs @menuify_cmd on the file as well. # sub write_file_if_changed($$$) { my ($outfile, $body, $menuify_p) = @_; local *OUT; my $file_tmp = "$outfile.tmp"; open(OUT, ">$file_tmp") || error ("$file_tmp: $!"); if ($body) { print OUT $body || error ("$file_tmp: $!"); } close OUT || error ("$file_tmp: $!"); if ($menuify_p) { my @cmd = @menuify_cmd; if ($verbose > 2) { push @cmd, ("-" .("v" x ($verbose - 2))); } push @cmd, $file_tmp; print "$progname: executing \"" . join(" ", @cmd) . "\"\n" if ($verbose > 2); system @cmd; my $exit_value = $? >> 8; my $signal_num = $? & 127; my $dumped_core = $? & 128; error ("$cmd[0]: core dumped!") if ($dumped_core); error ("$cmd[0]: signal $signal_num!") if ($signal_num); error ("$cmd[0]: exited with $exit_value!") if ($exit_value); } rename_or_delete ("$outfile", "$file_tmp"); } sub error($) { my ($err) = @_; print STDERR "$progname: $err\n"; exit 1; } sub usage() { print STDERR "usage: $progname [--verbose] [--quiet] [--debug]\n" . "\t[--summarize-only] [--topten] [--ping-videos] [--only DD-MMM-YYYY]\n" . "\tdirectory [configfile]\n"; exit 1; } sub main() { my ($dir, $infile); my $summarize_only_p = 0; my $topten_p = 0; my $ping_videos_p = 0; my $check_tickets_p = 0; error ("LANG is $ENV{LANG} -- UTF is no good, man!") if ($ENV{LANG} && $ENV{LANG} =~ m/utf/i); while ($_ = $ARGV[0]) { shift @ARGV; if (m/^--?verbose$/s) { $verbose++; } elsif (m/^-v+$/) { $verbose += length($_)-1; } elsif (m/^--?q(uiet)?$/s) { $verbose = 0; } elsif (m/^--?summarize(-only)?$/s) { $summarize_only_p++; } elsif (m/^--?topten$/s) { $topten_p++; } elsif (m/^--?ping-videos$/s) { $ping_videos_p++; } elsif (m/^--?check-tickets$/s) { $check_tickets_p++; } elsif (m/^--?debug$/s) { $debug_p++; } elsif (m/^--?only$/s) { $summarize_only_p = shift @ARGV; } elsif (m/^-./) { usage; } elsif (!defined($dir)) { $dir = $_; } elsif (!defined($infile)) { $infile = $_; } else { usage; } } usage unless $dir; $dir =~ s@/+$@@; # lose trailing slash $infile = $calendar_data_file unless ($infile); $summarize_only_p = 0 if ($topten_p); load_calendar ($infile, $summarize_only_p); my ($from, $to) = find_scheduled_range (); generate_calendars ($dir, $from, $to, $summarize_only_p, $topten_p); generate_months ($dir, $from, $to) unless ($summarize_only_p || $topten_p); ping_videos() if ($ping_videos_p); check_tickets() if ($check_tickets_p); } main(); exit 0;