Add OPML importing
This commit is contained in:
parent
e55a694194
commit
c42c93e0db
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue