centralcoast.rb 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. # Central Coast Council — Current Planning Applications (site page)
  2. # Source: https://www.centralcoast.tas.gov.au/current-planning-applications/
  3. require "date"
  4. require "nokogiri"
  5. require "cgi"
  6. require "fileutils"
  7. require_relative "../lib/http"
  8. require_relative "../lib/db"
  9. require_relative "../lib/util"
  10. require_relative "../lib/geocode"
  11. require_relative "../lib/enrich"
  12. TABLE = ENV.fetch("TABLE_NAME") # run_all.sh sets from filename: da_centralcoast
  13. URL = "https://www.centralcoast.tas.gov.au/current-planning-applications/"
  14. DOWNLOAD_ATTACHMENTS = ENV["DOWNLOAD_ATTACHMENTS"] == "1"
  15. DOWNLOAD_DIR = ENV["DOWNLOAD_DIR"] || "/app/downloads"
  16. DB.ensure_table!(TABLE)
  17. ensure_extra_columns!(TABLE)
  18. def abs_url(base, href)
  19. return "" if href.to_s.strip.empty?
  20. URI.join(base, href).to_s
  21. rescue URI::InvalidURIError
  22. href.to_s
  23. end
  24. def safe_name(s) = s.to_s.gsub(/[^\w\-.]+/, "_")
  25. def filename_from_response(res, fallback)
  26. cd = res.respond_to?(:[]) ? res["content-disposition"].to_s : ""
  27. if cd =~ /filename\*?=(?:UTF-8''|")?([^\";]+)/
  28. return safe_name($1)
  29. end
  30. base = safe_name(fallback || "document")
  31. ct = res.respond_to?(:[]) ? res["content-type"].to_s.downcase : ""
  32. ext = if base =~ /\.[A-Za-z0-9]{2,4}\z/
  33. ""
  34. elsif ct.include?("pdf")
  35. ".pdf"
  36. else
  37. ".bin"
  38. end
  39. "#{base}#{ext}"
  40. end
  41. def download_pdf(url, council_reference)
  42. return nil if url.to_s.strip.empty?
  43. dir = File.join(DOWNLOAD_DIR, "centralcoast", safe_name(council_reference))
  44. FileUtils.mkdir_p(dir)
  45. res = (Http.get_response(url) rescue nil)
  46. body = if res && res.respond_to?(:body)
  47. res.body
  48. else
  49. Http.get(url)
  50. end
  51. fname = filename_from_response(res, File.basename(URI.parse(url).path))
  52. path = File.join(dir, fname)
  53. File.binwrite(path, body.to_s)
  54. puts " saved #{File.basename(path)} (#{body.to_s.bytesize} bytes)"
  55. # adjust if your web container mounts differently
  56. "/downloads/centralcoast/#{safe_name(council_reference)}/#{fname}"
  57. rescue => e
  58. warn "Download failed for #{url}: #{e.class} #{e.message}"
  59. nil
  60. end
  61. # Normalize refs like "DA2025123" or "DA 2025/123" -> "DA 2025 / 123"
  62. def normalize_ref(s)
  63. t = s.to_s
  64. if (m = t.match(/\bDA\s*([12]\d{3})\s*[-\/]?\s*0*([A-Za-z0-9]{2,})\b/i))
  65. return "DA #{m[1]} / #{m[2]}"
  66. elsif (m = t.match(/\bDA(20\d{2})\s*0*([A-Za-z0-9]{2,})\b/i))
  67. return "DA #{m[1]} / #{m[2]}"
  68. end
  69. t.split(/\s*-\s*/, 2).first # fallback
  70. end
  71. # Parse dd-mm-yyyy shown in "Date modified"
  72. def parse_dd_mm_yyyy(s)
  73. s = s.to_s.strip
  74. if s =~ /\A\d{2}-\d{2}-\d{4}\z/
  75. Date.strptime(s, "%d-%m-%Y") rescue nil
  76. else
  77. # fall back to your common AUS parser
  78. Util.parse_aus_date(s)
  79. end
  80. end
  81. html = Http.get(URL)
  82. doc = Nokogiri::HTML(html)
  83. rows = doc.css("table.wpfd-search-result tbody tr.file")
  84. puts "Found #{rows.length} items for #{TABLE}"
  85. saved = 0
  86. rows.each do |tr|
  87. # Primary link + title from the first column
  88. a = tr.at_css("td.file_title a.wpfd_downloadlink")
  89. next unless a
  90. document_url = abs_url(URL, a["href"].to_s)
  91. title_reference = a["title"].to_s.strip
  92. title_text = tr.at_css("input.wpfd_file_preview_link_download")&.[]("data-filetitle").to_s.strip
  93. title_reference = title_reference.empty? ? title_text : title_reference
  94. # Title looks like: "DA2025123 - 148 Main Street, Ulverstone"
  95. council_reference_raw = title_reference.split(" - ").first.to_s.strip
  96. council_reference = normalize_ref(council_reference_raw)
  97. address = if title_reference.include?(" - ")
  98. title_reference.split(" - ", 2)[1].to_s.strip
  99. else
  100. # crude fallback to a street-like token
  101. title_reference[/\b\d+[A-Za-z]*\s[\w\s,]+/, 0].to_s.strip
  102. end
  103. address = title_reference[0, 140] if address.empty?
  104. # Optional description column (often blank)
  105. description = tr.at_css("td.file_desc")&.text.to_s.strip
  106. description = "Development Application" if description.empty?
  107. # Date modified -> date_received
  108. date_received_raw = tr.at_css("td.file_modified")&.text.to_s.strip
  109. date_received = parse_dd_mm_yyyy(date_received_raw)
  110. # Minimal required fields
  111. next if council_reference.to_s.strip.empty? || address.to_s.strip.empty?
  112. DB.upsert(TABLE, {
  113. description: description,
  114. date_received: date_received,
  115. date_received_raw: date_received_raw,
  116. address: address,
  117. council_reference: council_reference,
  118. applicant: "",
  119. owner: ""
  120. })
  121. enrich_after_upsert!(
  122. table: TABLE,
  123. council_reference: council_reference,
  124. address: address
  125. )
  126. local_doc_url = nil
  127. if DOWNLOAD_ATTACHMENTS && !document_url.empty?
  128. local_doc_url = download_pdf(document_url, council_reference)
  129. end
  130. begin
  131. upd = DB.client.prepare(
  132. "UPDATE `#{DB.client.escape(TABLE)}` " \
  133. "SET document_url = ?, local_document_url = ?, title_reference = ? " \
  134. "WHERE council_reference = ? AND address = ?"
  135. )
  136. upd.execute(document_url, local_doc_url, title_reference, council_reference, address)
  137. rescue => e
  138. warn "Extras update skipped for #{council_reference}: #{e.class} #{e.message}"
  139. end
  140. puts "Upserted #{council_reference} -> #{address} #{local_doc_url ? '(downloaded)' : ''}"
  141. saved += 1
  142. end
  143. puts "Done #{TABLE}. Saved #{saved} item(s)."