defmodule Frenzy.OPML.Importer do import Record @typedoc """ The data created from an OPML import. A list of groups. Each group has a name (String) or :default (if the OPML specified no group) and a list of feed URLs. """ @type import_data() :: %{optional(String.t() | :default) => [String.t()]} defrecord :xmlElement, extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl") defrecord :xmlAttribute, extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl") @spec parse_opml(String.t()) :: import_data() def parse_opml(text) do {doc, _} = text |> String.trim() |> :binary.bin_to_list() |> :xmerl_scan.string() outline_elements = :xmerl_xpath.string('/opml/body/outline', doc) outline_elements |> Enum.flat_map(&get_feeds/1) |> Enum.reduce(%{}, fn {group, feed_url}, acc -> Map.update(acc, group, [feed_url], fn feeds -> [feed_url | feeds] end) end) end defp get_feeds(outline_el) do attributes = xmlElement(outline_el, :attributes) # if the contains a xmlUrl attribute, it is a top level feed (uses the :default group) # otherwise, it is a group of feeds if Enum.any?(attributes, fn attr -> xmlAttribute(attr, :name) == :xmlUrl end) do [{:default, get_feed_from_outline(outline_el)}] else get_feeds_from_group(outline_el) end end defp get_feed_from_outline(feed_el) do [attr] = :xmerl_xpath.string('//@xmlUrl', feed_el) xmlAttribute(attr, :value) |> List.to_string() end defp get_feeds_from_group(group_el) do [title_attr] = :xmerl_xpath.string('/outline/@title', group_el) group_title = xmlAttribute(title_attr, :value) |> List.to_string() :xmerl_xpath.string('/outline/outline/@xmlUrl', group_el) |> Enum.map(fn attr -> feed_url = xmlAttribute(attr, :value) |> List.to_string() {group_title, feed_url} end) end end