# Brighton Council — Advertised Development Applications (site page, not PlanBuild) require "date" require "nokogiri" require "cgi" require "fileutils" require_relative "../lib/enrich" require_relative "../lib/log" require_relative "../lib/util" TABLE = ENV.fetch("TABLE_NAME") # run_all.sh sets this from filename: da_brighton URL = "https://www.brighton.tas.gov.au/planning/advertised-development-applications/" DOWNLOAD_ATTACHMENTS = ENV["DOWNLOAD_ATTACHMENTS"] == "1" DOWNLOAD_DIR = ENV["DOWNLOAD_DIR"] || "/app/downloads" DB.ensure_table!(TABLE) # --- helpers --------------------------------------------------------------- # DA/APP refs like "DA2025-130", "DA 2024/174", etc → "DA YYYY / NNN…" REF_RX = %r{ \b(?:DA|APP|APPLICATION)\s* (20\d{2})\s*[/\-]?\s* ([A-Za-z0-9\-_.]{2,})\b }ix def extract_ref_from(str) s = CGI.unescape(str.to_s) if (m = s.match(REF_RX)) return "DA #{m[1]} / #{m[2]}" end # Compact like DA20250123 if (m = s.match(/\bDA(20\d{2})(\d{3,})\b/i)) return "DA #{m[1]} / #{m[2]}" end nil end def abs_url(base, href) return "" if href.to_s.strip.empty? URI.join(base, href).to_s rescue URI::InvalidURIError href.to_s end def safe_name(s) = s.to_s.gsub(/[^\w\-.]+/, "_") def strip_ordinals(s) s.to_s.gsub(/\b(\d{1,2})(st|nd|rd|th)\b/i, '\1') end def parse_close_date(s) Util.parse_aus_date(strip_ordinals(s.to_s)) end def download_pdf(url, council_reference) return nil unless DOWNLOAD_ATTACHMENTS && !url.to_s.strip.empty? folder = File.join(DOWNLOAD_DIR, "brighton", safe_name(council_reference)) FileUtils.mkdir_p(folder) begin res = Http.get_response(url) rescue Http.get(url) body = res.respond_to?(:body) ? res.body : res.to_s fname = safe_name(File.basename(URI.parse(url).path)) fname += ".pdf" unless fname.downcase.end_with?(".pdf") path = File.join(folder, fname) File.binwrite(path, body) puts "Saved PDF #{path}" "/files/brighton/#{safe_name(council_reference)}/#{fname}" rescue StandardError => e Log.warn "scraper", "PDF download failed for #{url}: #{e.class} #{e.message}" nil end end # --- scrape --------------------------------------------------------------- html = Http.get(URL) doc = Nokogiri::HTML(html) # Find the advertised table by headers table = doc.css("table").find do |t| heads = t.css("thead th").map { |th| th.text.strip.downcase } heads.any? && heads.any? { |h| h.include?("description") } && heads.any? { |h| h.include?("address") } && heads.any? { |h| h.include?("closing") || h.include?("closing date") } && heads.any? { |h| h.include?("pdf") } end unless table puts "No advertised table found on #{URL}" exit 0 end # Column indexes by header text (fallback to 0..3 if needed) heads = table.css("thead th").map { |th| th.text.strip.downcase } i_desc = heads.index { |h| h.include?("description") } || 0 i_addr = heads.index { |h| h.include?("address") } || 1 i_close= heads.index { |h| h.include?("closing") } || 2 i_pdf = heads.index { |h| h.include?("pdf") } || 3 rows = table.css("tbody tr") puts "Found #{rows.length} row(s) for #{TABLE}" saved = 0 today = Date.today rows.each do |tr| tds = tr.css("td") next if tds.empty? description = tds[i_desc]&.text&.strip.to_s address = tds[i_addr]&.text&.strip.to_s close_raw = tds[i_close]&.text&.strip.to_s on_notice_to = parse_close_date(close_raw) date_received = on_notice_to && (on_notice_to - 14) # one or more PDF links pdf_links = tds[i_pdf]&.css('a[href]')&.map { |a| abs_url(URL, a["href"].to_s) } || [] document_url = pdf_links.first.to_s # reference from link text or file name ref_texts = tds[i_pdf]&.css('a')&.map { |a| a.text.to_s } || [] council_reference = ref_texts.map { |tx| extract_ref_from(tx) }.compact.first || pdf_links.map { |u| extract_ref_from(File.basename(u)) }.compact.first # minimal keys next if address.empty? || council_reference.nil? DB.upsert(TABLE, { description: description.empty? ? "Development Application" : description, date_received: date_received, date_received_raw: today.strftime("%Y-%m-%d"), on_notice_to: on_notice_to, # store close date in DATE column on_notice_to_raw: strip_ordinals(close_raw), address: address, council_reference: council_reference, applicant: "", owner: "" }) enrich_after_upsert!( table: TABLE, council_reference: council_reference, address: address ) # remote URL begin upd = DB.client.prepare("UPDATE `#{DB.client.escape(TABLE)}` SET document_url = ? WHERE council_reference = ? AND address = ?") upd.execute(document_url, council_reference, address) rescue StandardError => e Log.warn "scraper", "document_url update skipped for #{council_reference}: #{e.class} #{e.message}" end # local copy local_doc_url = download_pdf(document_url, council_reference) if local_doc_url begin upd2 = DB.client.prepare("UPDATE `#{DB.client.escape(TABLE)}` SET local_document_url = ? WHERE council_reference = ? AND address = ?") upd2.execute(local_doc_url, council_reference, address) rescue StandardError => e Log.warn "scraper", "local_document_url update skipped for #{council_reference}: #{e.class} #{e.message}" end end puts "Upserted #{council_reference} -> #{address} (doc: #{document_url.empty? ? 0 : 1}, saved: #{local_doc_url ? 1 : 0})" saved += 1 end puts "Done #{TABLE}. Saved #{saved} item(s)."