Add OPML importing

This commit is contained in:
Shadowfacts 2019-08-30 14:27:52 -04:00
parent e55a694194
commit c42c93e0db
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
4 changed files with 148 additions and 27 deletions

View File

@ -0,0 +1,66 @@
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, [], fn feeds -> [feed_url | feeds] end)
end)
end
defp get_feeds(outline_el) do
attributes = xmlElement(outline_el, :attributes)
# if the <outline> 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

View File

@ -1,6 +1,6 @@
defmodule FrenzyWeb.AccountController do
use FrenzyWeb, :controller
alias Frenzy.{Repo, User, FervorClient}
alias Frenzy.{Repo, User, FervorClient, Filter}
alias FrenzyWeb.Router.Helpers, as: Routes
alias FrenzyWeb.Endpoint
@ -102,4 +102,48 @@ defmodule FrenzyWeb.AccountController do
redirect(conn, to: Routes.account_path(Endpoint, :show))
end
def import(conn, %{"file" => %Plug.Upload{} = file}) do
user = conn.assigns[:user]
{:ok, content} = File.read(file.path)
parsed = Frenzy.OPML.Importer.parse_opml(content)
total_imported_feeds =
Enum.map(parsed, fn {group_id, feeds} ->
group_title =
case group_id do
:default -> "Default"
_ when is_binary(group_id) -> group_id
end
group_changeset = Ecto.build_assoc(user, :groups, %{title: group_title})
{:ok, group} = Repo.insert(group_changeset)
Enum.each(feeds, fn feed_url ->
feed_changeset =
Ecto.build_assoc(group, :feeds, %{
feed_url: feed_url,
filter: %Filter{
mode: "reject",
score: 0
}
})
{:ok, _feed} = Repo.insert(feed_changeset)
end)
Enum.count(feeds)
end)
|> Enum.sum()
conn
|> put_flash(
:info,
"Imported #{Enum.count(parsed)} groups and #{total_imported_feeds} feeds."
)
|> redirect(to: Routes.group_path(Endpoint, :index))
end
end

View File

@ -43,6 +43,7 @@ defmodule FrenzyWeb.Router do
get "/account/change_fever_password", AccountController, :change_fever_password
post "/account/change_fever_password", AccountController, :do_change_fever_password
post "/account/remove_client", AccountController, :remove_client
post "/account/import", AccountController, :import
get "/", GroupController, :index
resources "/groups", GroupController, except: [:edit, :update]

View File

@ -5,32 +5,42 @@
<a href="<%= Routes.account_path(@conn, :change_fever_password) %>" class="btn btn-secondary">Change Fever Password</a>
<h2 class="mt-4">Approved Clients</h2>
<section class="mt-4">
<h2>Import/Export Data</h2>
<%= form_for @conn, Routes.account_path(@conn, :import), [method: :post, multipart: true], fn f -> %>
<%= file_input f, :file %>
<%= submit "Import OPML", class: "btn btn-primary" %>
<% end %>
</section>
<table class="table table-striped">
<thead>
<tr>
<th>Client</th>
<th>Revoke Access</th>
</tr>
</thead>
<tbody>
<%= for {approved, fervor} <- @clients do %>
<section class="mt-4">
<h2>Approved Clients</h2>
<table class="table table-striped">
<thead>
<tr>
<td>
<%= if fervor.website do %>
<a href="<%= fervor.website %>"><%= fervor.client_name %></a>
<% else %>
<%= fervor.client_name %>
<% end %>
</td>
<td>
<%= form_tag Routes.account_path(@conn, :remove_client), method: :post do %>
<input type="hidden" name="client_id" value="<%= approved.client_id %>">
<%= submit "Revoke", class: "btn btn-danger" %>
<% end %>
</td>
<th>Client</th>
<th>Revoke Access</th>
</tr>
<% end %>
</tbody>
</table>
</thead>
<tbody>
<%= for {approved, fervor} <- @clients do %>
<tr>
<td>
<%= if fervor.website do %>
<a href="<%= fervor.website %>"><%= fervor.client_name %></a>
<% else %>
<%= fervor.client_name %>
<% end %>
</td>
<td>
<%= form_tag Routes.account_path(@conn, :remove_client), method: :post do %>
<input type="hidden" name="client_id" value="<%= approved.client_id %>">
<%= submit "Revoke", class: "btn btn-danger" %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</section>