From 6773b02377f54973cd34d692c365526460fc88de Mon Sep 17 00:00:00 2001 From: Marco Milanesi Date: Tue, 14 Apr 2026 20:58:25 +0200 Subject: [PATCH] add jazzity scraper. normalize payloads --- lib/gigex.ex | 2 +- lib/scraper.ex | 24 ++- lib/scraper/jazzity.ex | 371 ++++++++++++++++++++++++++++++++++++++++ lib/scraper/lido.ex | 34 +++- lib/scraper/songkick.ex | 87 ++++++++-- mix.exs | 4 +- mix.lock | 16 +- 7 files changed, 504 insertions(+), 34 deletions(-) create mode 100644 lib/scraper/jazzity.ex diff --git a/lib/gigex.ex b/lib/gigex.ex index 86e8217..e3e07da 100644 --- a/lib/gigex.ex +++ b/lib/gigex.ex @@ -6,7 +6,7 @@ defmodule Gigex do """ @doc """ - Get the latest gigs from Songkick in Berlin + Get the latest gigs from configured sources (songkick, lido, jazzity) ## Example diff --git a/lib/scraper.ex b/lib/scraper.ex index 433b05e..ff2f856 100644 --- a/lib/scraper.ex +++ b/lib/scraper.ex @@ -6,18 +6,36 @@ defmodule Gigex.Scraper do alias __MODULE__ - @spec run_for(site :: :all | :songkick | :lido, opts :: list()) :: concert :: list() + @spec run_for(site :: :all | :songkick | :lido | :jazzity, opts :: list()) :: concert :: list() def run_for(:all, opts) do # Get all the gigs from the scrapers and sort them by date so that they # appear like one stream of data. Enum.sort_by( - Scraper.Songkick.get(opts) ++ Scraper.Lido.get(opts), - &Date.from_iso8601!(&1.date), + Scraper.Songkick.get(opts) ++ Scraper.Lido.get(opts) ++ Scraper.Jazzity.get(opts), + fn item -> + case Map.get(item, :date) || Map.get(item, "date") do + nil -> + Date.utc_today() + + date when is_binary(date) -> + case Date.from_iso8601(date) do + {:ok, d} -> d + _ -> Date.utc_today() + end + + %Date{} = d -> + d + + _ -> + Date.utc_today() + end + end, Date ) end def run_for(:songkick, opts), do: Scraper.Songkick.get(opts) def run_for(:lido, opts), do: Scraper.Lido.get(opts) + def run_for(:jazzity, opts), do: Scraper.Jazzity.get(opts) end diff --git a/lib/scraper/jazzity.ex b/lib/scraper/jazzity.ex new file mode 100644 index 0000000..935ee36 --- /dev/null +++ b/lib/scraper/jazzity.ex @@ -0,0 +1,371 @@ +defmodule Gigex.Scraper.Jazzity do + @moduledoc """ + Scraper for https://jazzity.net/prog.php that extracts concert listings. + + Usage: + Gigex.Scraper.Jazzity.fetch() + + Returns {:ok, events} or {:error, reason}. + Each event is a map with :title, :venue, :date, :time, :url, :description when available. + """ + + require Logger + + @default_url "https://jazzity.net/prog.php" + + @doc "Fetch and parse concerts from the given URL (defaults to site prog page)." + def fetch(url \\ @default_url) when is_binary(url) do + case HTTPoison.get(url, [], follow_redirect: true, recv_timeout: 15_000) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + parse(body, url) + + {:ok, %HTTPoison.Response{status_code: code}} -> + {:error, {:http_status, code}} + + {:error, reason} -> + {:error, reason} + end + end + + defp parse(body, base_url) do + case Floki.parse_document(body) do + {:ok, doc} -> + nodes = extract_nodes(doc) + events = Enum.map(nodes, &node_to_event(&1, base_url)) + + {:ok, + Enum.reject(events, fn e -> + (Map.get(e, :name, "") == "" or is_nil(Map.get(e, :name))) and + is_nil(Map.get(e, :link)) + end)} + + {:error, err} -> + {:error, {:parse_error, err}} + end + end + + # Find candidate nodes then filter by content heuristics (time, image, clubs link, month names) + defp extract_nodes(doc) do + # Prefer the programme rows used on jazzity: div.row.prog_list contains a single event each + rows = Floki.find(doc, "div.row.prog_list") + rows = if rows == [], do: Floki.find(doc, ".prog_list"), else: rows + + if rows != [] do + rows + else + selectors = ["article", ".prog", ".program", ".event", ".post", "li", "div"] + + found = + selectors + |> Enum.map(&Floki.find(doc, &1)) + |> Enum.find(fn nodes -> nodes != [] end) + + candidates = + case found do + nil -> + Floki.find(doc, "a") + |> Enum.filter(fn n -> String.trim(Floki.text(n || "")) != "" end) + + nodes -> + nodes + end + + Enum.filter(candidates, fn node -> + text = Floki.text(node) |> to_string() |> String.replace(~r/\s+/, " ") |> String.trim() + hrefs = Floki.attribute(node, "href") + + cond do + text =~ ~r/\b\d{1,2}:\d{2}\b/ -> + true + + Enum.any?(hrefs, &String.contains?(&1, "clubs.php")) -> + true + + String.contains?(text, "/img/") -> + true + + text =~ + ~r/(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|Januar|Februar|März|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember)/i -> + true + + true -> + false + end + end) + end + end + + defp node_to_event(node, base_url) do + # Raw text and split lines for heuristics + raw = Floki.raw_html(node) |> to_string() + text = Floki.text(node) |> to_string() |> String.replace(~r/\s+/, " ") |> String.trim() + + lines = + text + |> String.split(~r/\s{2,}|\n/) + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "")) + + time = + case Regex.run(~r/\b\d{1,2}:\d{2}\b/, text) do + nil -> nil + [m | _] -> m + end + + date = parse_date(text, lines) + + # Collect anchors: {href, text} + anchors = + Floki.find(node, "a") + |> Enum.map(fn a -> + {Floki.attribute(a, "href") |> List.first(), Floki.text(a) |> String.trim()} + end) + + # Prefer external hrefs, then club links, then first anchor + external = + Enum.find(anchors, fn {h, _} -> is_binary(h) and String.starts_with?(h, "http") end) + + club = Enum.find(anchors, fn {h, _} -> is_binary(h) and String.contains?(h, "clubs.php") end) + first_anchor = List.first(anchors) + + href = + (external && elem(external, 0)) || (club && elem(club, 0)) || + (first_anchor && elem(first_anchor, 0)) + + url = if href, do: build_absolute_url(base_url, href), else: nil + + # Location: prefer explicit club anchor text (a[href*='clubs.php']), else fallback to first short font-size anchor + club_node = Floki.find(node, "a[href*='clubs.php']") |> List.first() + + location = + if club_node do + # try to get the small-font child (club name) inside the anchor, else full anchor text + Floki.find(club_node, "[style*='font-size:1.0em']") + |> Enum.map(&Floki.text/1) + |> List.first() + |> case do + nil -> Floki.text(club_node) + v -> v + end + else + Floki.find(node, "[style*='font-size:1.0em'] a") + |> Enum.map(&Floki.text/1) + |> Enum.find(fn t -> t && String.length(String.trim(t)) > 1 end) + end + + location = (location || "") |> to_string() |> String.trim() + + # Name: prefer an anchor with substantial text that is not the club/tag link + name_anchor = + Enum.find(anchors, fn {h, t} -> + (t && String.length(t) > 6) and not (is_binary(h) and String.contains?(h, "clubs.php")) and + not (is_binary(h) and String.contains?(h, "tags.php")) + end) + + name = + cond do + name_anchor -> + elem(name_anchor, 1) + + true -> + # fallback: look for styled title elements (font-size:1.4em) + Floki.find(node, "[style*='font-size:1.4em']") + |> Enum.map(&Floki.text/1) + |> Enum.find(fn t -> t && String.length(String.trim(t)) > 3 end) + end + + name = (name || "") |> String.trim() + + # Infos: try to pick the descriptive block (font-size:1.0em) excluding the club/name lines + desc_candidates = + Floki.find(node, "[style*='font-size:1.0em']") + |> Enum.map(&Floki.text/1) + |> Enum.map(&String.trim/1) + |> Enum.reject(&(&1 == "" or &1 == location or &1 == name)) + + infos = + Enum.find(desc_candidates, fn d -> String.length(d) > 20 end) || + lines + |> Enum.reject(fn l -> + l == name or l == location or Regex.match?(~r/^\d{1,2}:\d{2}$/, l) or + Regex.match?(~r/^\d{1,2}$/, l) + end) + |> Enum.join(" | ") + |> String.trim() + + # dotw from date if possible + dotw = + case date do + nil -> + nil + + iso when is_binary(iso) -> + case Date.from_iso8601(iso) do + {:ok, d} -> + ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + |> Enum.at(Date.day_of_week(d) - 1) + + _ -> + nil + end + + _ -> + nil + end + + %{ + name: name, + date: date, + link: url, + location: location, + infos: infos, + datasource: "jazzity", + dotw: dotw + } + end + + defp find_first_anchor_href(node) do + case Floki.find(node, "a") do + [{_, attrs, _} | _] -> + case List.keyfind(attrs, "href", 0) do + {"href", v} -> v + _ -> nil + end + + _ -> + nil + end + end + + defp parse_date(text, lines) do + # Normalize whitespace + t = String.replace(text || "", ~r/\s+/, " ") |> String.trim() + + # month name mapping (english and german short/long) + months = %{ + "jan" => 1, + "january" => 1, + "januar" => 1, + "feb" => 2, + "february" => 2, + "februar" => 2, + "mar" => 3, + "march" => 3, + "märz" => 3, + "maerz" => 3, + "apr" => 4, + "april" => 4, + "may" => 5, + "mai" => 5, + "jun" => 6, + "june" => 6, + "juni" => 6, + "jul" => 7, + "july" => 7, + "juli" => 7, + "aug" => 8, + "august" => 8, + "sep" => 9, + "sept" => 9, + "september" => 9, + "oct" => 10, + "october" => 10, + "oktober" => 10, + "nov" => 11, + "november" => 11, + "dec" => 12, + "december" => 12, + "dezember" => 12 + } + + # Try several patterns: day month year (e.g., 14 April 2026) + patterns = [ + ~r/(\d{1,2})\s+(January|Jan|Januar|Februar|Feb|März|Mar|Maerz|April|Apr|Mai|May|Juni|Jun|Juli|Jul|August|Aug|September|Sep|Oktober|Oct|November|Nov|Dezember|Dec)\s+(\d{4})/i, + ~r/(January|Jan|Januar|Februar|Feb|März|Mar|Maerz|April|Apr|Mai|May|Juni|Jun|Juli|Jul|August|Aug|September|Sep|Oktober|Oct|November|Nov|Dezember|Dec)\s+(\d{1,2}),?\s*(\d{4})/i, + ~r/(\d{1,2})\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{4})/i + ] + + Enum.reduce_while(patterns, nil, fn pat, _acc -> + case Regex.run(pat, t) do + nil -> + {:cont, nil} + + [_, a, b, c] -> + # patterns may return day, month, year in different order + {day, month_name, year} = + if Regex.match?(~r/^\d{1,2}$/, a) do + {String.to_integer(a), b, c} + else + {String.to_integer(b), a, c} + end + + mon = String.downcase(String.replace(month_name, "ä", "ae")) + mon_key = String.slice(mon, 0..2) + month = Map.get(months, mon_key) || Map.get(months, mon) + + if month do + iso = + "#{year}-#{String.pad_leading(Integer.to_string(month), 2, "0")}-#{String.pad_leading(Integer.to_string(day), 2, "0")}" + + {:halt, iso} + else + {:cont, nil} + end + end + end) + |> case do + nil -> + # try to detect day and month split across lines (e.g., "14" and "April 2026") + idx_day = Enum.find_index(lines, fn l -> Regex.match?(~r/^\d{1,2}$/, l) end) + + if idx_day do + day = Enum.at(lines, idx_day) |> String.trim() + next = Enum.at(lines, idx_day + 1) || "" + + case Regex.run( + ~r/(January|Jan|Januar|Februar|Feb|März|Mar|Maerz|April|Apr|Mai|May|Juni|Jun|Juli|Jul|August|Aug|September|Sep|Oktober|Oct|November|Nov|Dezember|Dec)\s*(\d{4})?/i, + next + ) do + nil -> + nil + + [_, m, y] -> + mon = String.downcase(String.replace(m, "ä", "ae")) + mon_key = String.slice(mon, 0..2) + month = Map.get(months, mon_key) || Map.get(months, mon) + year = if y in [nil, ""], do: Date.utc_today().year |> Integer.to_string(), else: y + + if month do + "#{year}-#{String.pad_leading(Integer.to_string(month), 2, "0")}-#{String.pad_leading(day, 2, "0")}" + else + nil + end + end + else + nil + end + + iso -> + iso + end + end + + defp build_absolute_url(base, href) when is_binary(href) do + try do + case URI.parse(href) do + %URI{scheme: nil} -> URI.merge(base, href) |> to_string() + _ -> href + end + rescue + _ -> href + end + end + + @doc "Return list of events (wrapper for fetch)." + def get(_opts \\ []) do + case fetch() do + {:ok, events} -> events + {:error, _} -> [] + end + end +end diff --git a/lib/scraper/lido.ex b/lib/scraper/lido.ex index 412ec6e..c2fb02f 100644 --- a/lib/scraper/lido.ex +++ b/lib/scraper/lido.ex @@ -11,13 +11,28 @@ defmodule Gigex.Scraper.Lido do @location_name "Lido" @short_days_to_human_days %{ - "Mo" => "Monday", - "Tu" => "Tuesday", - "We" => "Wednesday", - "Th" => "Thursday", - "Fr" => "Friday", - "Sa" => "Saturday", - "Su" => "Sunday" + "mo" => "Monday", + "mon" => "Monday", + "di" => "Tuesday", + "tu" => "Tuesday", + "tue" => "Tuesday", + "dienstag" => "Tuesday", + "mi" => "Wednesday", + "we" => "Wednesday", + "wed" => "Wednesday", + "mittwoch" => "Wednesday", + "do" => "Thursday", + "thu" => "Thursday", + "donnerstag" => "Thursday", + "fr" => "Friday", + "fri" => "Friday", + "freitag" => "Friday", + "sa" => "Saturday", + "sat" => "Saturday", + "samstag" => "Saturday", + "so" => "Sunday", + "sun" => "Sunday", + "sonntag" => "Sunday" } @default_entries_limit 10 @@ -75,8 +90,11 @@ defmodule Gigex.Scraper.Lido do event |> Floki.find(".event-ticket__meta__day") |> Floki.text() + |> String.replace(".", "") + |> String.trim() + |> String.downcase() |> then(fn short_day -> - Map.get(@short_days_to_human_days, short_day, short_day) + Map.get(@short_days_to_human_days, short_day, String.capitalize(short_day)) end) end diff --git a/lib/scraper/songkick.ex b/lib/scraper/songkick.ex index 8ad236c..9ce7f41 100644 --- a/lib/scraper/songkick.ex +++ b/lib/scraper/songkick.ex @@ -27,14 +27,20 @@ defmodule Gigex.Scraper.Songkick do {:halt, Enum.reverse(entries)} event, {entries, acc} -> + raw_date = extract_date_from_event(event) + raw_day = extract_day_of_the_week(event) + + date = normalize_date(raw_date) + dotw = compute_dotw(date, raw_day) + entry = %{ name: extract_name(event), - date: extract_date_from_event(event), - location: extract_location(event), - dotw: extract_day_of_the_week(event), + date: date, link: extract_event_link(event), + location: extract_location(event), infos: extract_event_infos(event), - datasource: "songkick" + datasource: "songkick", + dotw: dotw } {:cont, {[entry | entries], acc + 1}} @@ -42,8 +48,6 @@ defmodule Gigex.Scraper.Songkick do ) end - # normalize()? - defp extract_date_from_event(event) do datetime = event @@ -51,17 +55,76 @@ defmodule Gigex.Scraper.Songkick do |> Floki.attribute("datetime") |> Floki.text() - case DateTime.from_iso8601(datetime) do - {:ok, datetime, _} -> - datetime - |> DateTime.to_date() - |> to_string() + datetime + end + + defp normalize_date(date) when is_binary(date) do + case Date.from_iso8601(date) do + {:ok, d} -> + to_string(d) _ -> - datetime + case DateTime.from_iso8601(date) do + {:ok, dt, _} -> dt |> DateTime.to_date() |> to_string() + _ -> nil + end end end + defp normalize_date(_), do: nil + + defp compute_dotw(date_iso, raw_day) do + cond do + is_binary(date_iso) and date_iso != "" -> + case Date.from_iso8601(date_iso) do + {:ok, d} -> + ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + |> Enum.at(Date.day_of_week(d) - 1) + + _ -> + nil + end + + true -> + raw_day + |> to_string() + |> String.replace(".", "") + |> String.trim() + |> String.downcase() + |> songkick_day_map() + end + end + + defp songkick_day_map(day) do + map = %{ + "mo" => "Monday", + "mon" => "Monday", + "monday" => "Monday", + "di" => "Tuesday", + "tu" => "Tuesday", + "tue" => "Tuesday", + "tuesday" => "Tuesday", + "mi" => "Wednesday", + "wed" => "Wednesday", + "wednesday" => "Wednesday", + "do" => "Thursday", + "th" => "Thursday", + "thu" => "Thursday", + "thursday" => "Thursday", + "fr" => "Friday", + "fri" => "Friday", + "friday" => "Friday", + "sa" => "Saturday", + "sat" => "Saturday", + "saturday" => "Saturday", + "so" => "Sunday", + "sun" => "Sunday", + "sunday" => "Sunday" + } + + Map.get(map, day, String.capitalize(day)) + end + defp extract_location(event) do event |> Floki.find(".location") diff --git a/mix.exs b/mix.exs index 38f01f6..3c06cc0 100644 --- a/mix.exs +++ b/mix.exs @@ -21,8 +21,8 @@ defmodule Gigex.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:httpoison, "~> 1.8"}, - {:floki, "~> 0.34.0"} + {:httpoison, "~> 2.3"}, + {:floki, "~> 0.38.0"} ] end end diff --git a/mix.lock b/mix.lock index 8e60afc..b2ccc77 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,12 @@ %{ - "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, - "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"}, - "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, - "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, + "floki": {:hex, :floki, "0.38.1", "f002ccac94b3bcb21d40d9b34cc2cc9fd88a8311879120330075b5dde657ebee", [:mix], [], "hexpm", "e744bf0db7ee34b2c8b62767f04071107af0516a81144b9a2f73fe0494200e5b"}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, + "httpoison": {:hex, :httpoison, "2.3.0", "10eef046405bc44ba77dc5b48957944df8952cc4966364b3cf6aa71dce6de587", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d388ee70be56d31a901e333dbcdab3682d356f651f93cf492ba9f06056436a2c"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, - "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, }